{"id":13532997,"url":"https://github.com/Fullstak-nl/medusa-plugin-postmark","last_synced_at":"2025-04-01T21:31:37.707Z","repository":{"id":65389703,"uuid":"591504400","full_name":"Fullstak-nl/medusa-plugin-postmark","owner":"Fullstak-nl","description":"Postmark notification plugin for MedusaJS","archived":false,"fork":false,"pushed_at":"2024-06-17T01:02:42.000Z","size":5128,"stargazers_count":15,"open_issues_count":8,"forks_count":2,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-03-09T11:46:33.873Z","etag":null,"topics":["medusa-plugin","medusajs","plugin","postmark"],"latest_commit_sha":null,"homepage":"https://fullstak-nl.github.io/medusa-plugin-postmark/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Fullstak-nl.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-01-20T23:19:42.000Z","updated_at":"2025-02-28T11:41:26.000Z","dependencies_parsed_at":"2024-01-08T16:27:31.857Z","dependency_job_id":"b70500e3-69cb-4c26-ab12-0460c55dffa1","html_url":"https://github.com/Fullstak-nl/medusa-plugin-postmark","commit_stats":{"total_commits":24,"total_committers":2,"mean_commits":12.0,"dds":"0.16666666666666663","last_synced_commit":"33548069b62675fd0290c1ad262f39621247fdd2"},"previous_names":[],"tags_count":23,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Fullstak-nl%2Fmedusa-plugin-postmark","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Fullstak-nl%2Fmedusa-plugin-postmark/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Fullstak-nl%2Fmedusa-plugin-postmark/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Fullstak-nl%2Fmedusa-plugin-postmark/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Fullstak-nl","download_url":"https://codeload.github.com/Fullstak-nl/medusa-plugin-postmark/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246713168,"owners_count":20821851,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["medusa-plugin","medusajs","plugin","postmark"],"created_at":"2024-08-01T07:01:15.699Z","updated_at":"2025-04-01T21:31:36.626Z","avatar_url":"https://github.com/Fullstak-nl.png","language":"JavaScript","funding_links":[],"categories":["Uncategorized"],"sub_categories":["Uncategorized"],"readme":"# medusa-plugin-postmark\n\n[![stars - medusa-plugin-postmark](https://img.shields.io/github/stars/Fullstak-nl/medusa-plugin-postmark?style=social)](https://github.com/Fullstak-nl/medusa-plugin-postmark)\n[![forks - medusa-plugin-postmark](https://img.shields.io/github/forks/Fullstak-nl/medusa-plugin-postmark?style=social)](https://github.com/Fullstak-nl/medusa-plugin-postmark)\n[![CodeQL](https://github.com/Fullstak-nl/medusa-plugin-postmark/actions/workflows/codeql.yml/badge.svg)](https://github.com/Fullstak-nl/medusa-plugin-postmark/actions/workflows/codeql.yml)\n\n[![NPM Version (with dist tag)](https://img.shields.io/npm/v/medusa-plugin-postmark/latest)](https://www.npmjs.com/package/medusa-plugin-postmark)\n[![License](https://img.shields.io/badge/License-MIT-blue)](#license)\n[![issues - medusa-plugin-postmark](https://img.shields.io/github/issues/Fullstak-nl/medusa-plugin-postmark)](https://github.com/Fullstak-nl/medusa-plugin-postmark/issues)\n\nNotifications plugin for Medusa ecommerce server that sends transactional emails via [PostMark](https://postmarkapp.com/).\n\n## Features\n\n- Uses the email templating features built into Postmark\n- You can import/use tools like [stripo.email](https://stripo.email)\n- The plugin is in active development. If you have any feature requests, please open an issue.\n- Create PDF invoices and credit notes and attach them to the email\n- Send out upsell emails to customers that have recently placed an order with certain collections\n- Send out automated abandoned cart emails to customers that have abandoned their cart (based on last updated date of cart)\n\n## Configuration\n\nEnable in your medusa-config.js file similar to other plugins:  \n\n###### More events? (work in progress within the plugin!) [See here](https://docs.medusajs.com/advanced/backend/subscribers/events-list)\n\n```js\nconst plugins = [\n    // ... other plugins\n    {\n        resolve: `medusa-plugin-postmark`,\n        options: {\n            server_api: process.env.POSTMARK_SERVER_API,\n            from: process.env.POSTMARK_FROM,\n            bcc: process.env.POSTMARK_BCC || null,\n            pdf: {\n                enabled: process.env.POSTMARK_PDF_ENABLED || false,\n                settings: {\n                    font: process.env.POSTMARK_PDF_FONT || 'Helvetica', \n                    // [{file: 'yourfont.ttf', name: 'yourfont'},{file: 'yourfont-bold.ttf', name: 'yourfontbold'}]\n                    format: process.env.POSTMARK_PDF_FORMAT || 'A4', \n                    // see supported formats here: https://pdfkit.org/docs/paper_sizes.html\n                    margin: {\n                        top: process.env.POSTMARK_PDF_MARGIN_TOP || '50',\n                        right: process.env.POSTMARK_PDF_MARGIN_RIGHT || '50',\n                        bottom: process.env.POSTMARK_PDF_MARGIN_BOTTOM || '50',\n                        left: process.env.POSTMARK_PDF_MARGIN_LEFT || '50'\n                    },\n                    empty: \"\" // what to show if variable can't be found. Defaults to __UNDEFINED__\n                },\n                header: {\n                    enabled: process.env.POSTMARK_PDF_HEADER_ENABLED || false,\n                    content: process.env.POSTMARK_PDF_HEADER_CONTENT || null,\n                    // loads empty header if null, otherwise loads the file from `POSTMARK_PDF_HEADER_CONTENT`\n                    height: process.env.POSTMARK_PDF_HEADER_HEIGHT || '50'\n                },\n                footer: {\n                    enabled: process.env.POSTMARK_PDF_FOOTER_ENABLED || false,\n                    content: process.env.POSTMARK_PDF_FOOTER_CONTENT || null,\n                    // loads empty footer if null, otherwise loads the file from `POSTMARK_PDF_FOOTER_CONTENT`\n                },\n                templates: {\n                    invoice: process.env.POSTMARK_PDF_INVOICE_TEMPLATE || null,\n                    credit_note: process.env.POSTMARK_PDF_CREDIT_NOTE_TEMPLATE || null,\n                    return_invoice: process.env.POSTMARK_PDF_RETURN_INVOICE_TEMPLATE || null\n                }\n            },\n            events: {\n                order: {\n                    placed: process.env.POSTMARK_ORDER_PLACED || null,\n                    canceled: process.env.POSTMARK_ORDER_CANCELED || null,\n                    shipment_created: process.env.POSTMARK_ORDER_SHIPMENT_CREATED || null,\n                },\n                customer: {\n                    created: process.env.POSTMARK_CUSTOMER_CREATED || null,\n                    password_reset: process.env.POSTMARK_CUSTOMER_PASSWORD_RESET || null,\n                },\n                user: {\n                    created: process.env.POSTMARK_USER_CREATED || null,\n                    password_reset: process.env.POSTMARK_USER_PASSWORD_RESET || null,\n                },\n                auth: {\n                    password_reset: process.env.POSTMARK_AUTH_PASSWORD_RESET || null,\n                    verify_account: process.env.POSTMARK_AUTH_VERIFY_ACCOUNT || null,\n                },\n                activity: {\n                    inactive_user: process.env.POSTMARK_ACTIVITY_INACTIVE_USER || null,\n                    inactive_customer: process.env.POSTMARK_ACTIVITY_INACTIVE_CUSTOMER || null,\n                }\n            },\n            upsell: {\n                enabled: process.env.POSTMARK_UPSELL_ENABLED || false,\n                template: process.env.POSTMARK_UPSELL_TEMPLATE || null, // if you supply multiple templates (comma seperated), the plugin will pick one at random\n                delay: process.env.POSTMARK_UPSELL_DELAY || 9, // delay in days\n                valid: process.env.POSTMARK_UPSELL_VALID || 30, // valid in days\n                collection: process.env.POSTMARK_UPSELL_COLLECTION || null,\n            },\n            abandoned_cart: {\n                enabled: process.env.POSTMARK_ABANDONED_CART_ENABLED || false,\n                first: {\n                    delay: process.env.POSTMARK_ABANDONED_CART_FIRST_DELAY || 1, // delay in hours\n                    template: process.env.POSTMARK_ABANDONED_CART_FIRST_TEMPLATE || null, // if you supply multiple templates (comma seperated), the plugin will pick one at random\n                },\n                second: {\n                    delay: process.env.POSTMARK_ABANDONED_CART_SECOND_DELAY || 24, // delay in hours\n                    template: process.env.POSTMARK_ABANDONED_CART_SECOND_TEMPLATE || null, // if you supply multiple templates (comma seperated), the plugin will pick one at random\n                },\n                third: {\n                    delay: process.env.POSTMARK_ABANDONED_CART_THIRD_DELAY || 48, // delay in hours\n                    template: process.env.POSTMARK_ABANDONED_CART_THIRD_TEMPLATE || null, // if you supply multiple templates (comma seperated), the plugin will pick one at random\n                },\n            },\n            default_data: {\n                // ... default data to be passed to the email template\n                product_url: process.env.POSTMARK_PRODUCT_URL || '',\n                product_name: process.env.POSTMARK_PRODUCT_NAME || '',\n                company_name: process.env.POSTMARK_COMPANY_NAME || '',\n                company_address: process.env.POSTMARK_COMPANY_ADDRESS || '',\n            }\n        }\n    }\n]\n```\n\n### Templates\n\nThe plugin uses the Postmark template system for emails. For attachments the plugin relies on the [pdfkit](https://pdfkit.org/) library.  \nIn your JSON templates you can use several types (and [variables](#variables)):\n- `image` for (**local**) images\n- `text` for simple words, (long) sentences, paragraphs and links\n- `moveDown` for moving the cursor down one line\n- `hr` for a horizontal line\n- `tableRow` for a table(-like) row\n- `itemLoop` for looping over items in an order\n- `itemLoopEnd` for ending the item loop\n\n**Example:**\n```json\n[\n  {\n    \"type\": \"image\",\n    \"image\": \"image.png\",\n    \"x\": 100,\n    \"y\": 100,\n    \"fit\": [200, 50]\n  },\n  {\n    \"type\": \"text\",\n    \"text\": \"This is a text\",\n    \"size\": 20\n  },\n  {\n    \"type\": \"moveDown\",\n    \"lines\": 2\n  },\n  {\n    \"type\": \"hr\"\n  },\n  {\n    \"type\": \"moveDown\",\n    \"lines\": 2\n  },\n  {\n    \"type\": \"text\",\n    \"text\": \"Another text\"\n  }\n]\n```\n\n#### image\n\nImages are stored in `/src/images/` and can be used in the template like this:\n```json\n{\n    \"type\": \"image\",\n    \"image\": \"image.png\",\n    \"x\": 100,\n    \"y\": 100,\n    \"fit\": [200, 50]\n}\n```\n`fit` has multiple options, see [here](https://pdfkit.org/docs/images.html#fitting-images-to-a-frame) for more info.  \n\n##### Optional: \n- `align` horizontally align the image, the possible values are `left`, `center`, or `right`\n- `valign` vertically align the image, the possible values are `top`, `center`, or `bottom`\n\n#### text\n\nText can be used for words, sentences, paragraphs and links.  \n```json\n{\n    \"type\": \"text\",\n    \"text\": \"This is a text\"\n}\n```\nIf you use [`moveDown`](#movedown) correct you won't need to use `x` and `y` for the text.\n##### Optional:\nThese options can be used to style the text or to position it.\n- `x` the x position of the text\n- `y` the y position of the text\n- `font` the font of the text\n- `size` the font size of the text\n- `color` the color of the text (Hex codes `#ff0000`)\n- `width` the width of the text\n- `align` the alignment of the text, the possible values are `left`, `center`, `right`, or `justify`.  \n\nFor more styling options, see [here](https://pdfkit.org/docs/text.html#text_styling) for more info.\n\n#### moveDown\n\nThis is used to move the cursor down one or more line(s).  \n```json\n{\n    \"type\": \"moveDown\",\n    \"lines\": 1\n}\n```\n\n#### hr\n\nThis is used to draw a horizontal line.  \n```json\n{\n    \"type\": \"hr\"\n}\n```\n\n##### Optional:\n- `color` the color of the line (Hex codes `#ff0000`)\n- `width` the width of the line if you don't want it to be the full width of the page\n- `height` the height of the line element, including padding\n- `y` the y position of the line if you can not rely on the cursor (affected by [`moveDown`](#movedown))\n\n#### tableRow\n\nThis is used to draw a table row.  \n```json\n{\n    \"type\": \"tableRow\",\n    \"columns\": [\n        {\n            \"text\": \"Column 1\",\n            \"width\": 200\n        },\n        {\n            \"text\": \"Column 2\",\n            \"width\": 150\n        }\n    ]\n}\n```\n##### Optional:\nYou can use the same options as for `text` to style the text in the table row. If you want a special column styled, you can add the options to the column object.\n\n#### itemLoop\n\nThis is used to start the loop of items in an order.  \nTo access item variables, use the `item` object, for example `{{ item.title }}`.\n```json\n{\n    \"type\": \"itemLoop\"\n}\n```\n\n#### itemLoopEnd\n\nThis is used to end the loop of items in an order.  \n```json\n{\n    \"type\": \"itemLoopEnd\"\n}\n```\n\n### Variables\n\nIn the template you can use variables. These are replaced by the plugin with the correct value.  \nTo use a variable, use the following syntax: `{{ variable_name }}`, for example `{{ order.customer.first_name }}`.  \nOrder item variables are available inside the `itemLoop` and `itemLoopEnd` elements, for example `{{ item.title }}`.  \nIf you want to include (simple) if statements, use the following syntax: `{{ if variable_name }}...{{ endif }}`, or as a negative `{{ if not variable_name }}...{{ endif }}`.  \n**Possible variables depend on your notification system.**\nYou can use the `options` object and every template has his own `data` object.   \nDepending on the plugin you use, _(almost)_ every plugin **that supports attachments** based on `medusa-plugin-sendgrid` has the same variable `order` after the `options` variable which holds all the plugin variables.  \nMore information on the possible values that `order` can have can be found [here](https://docs.medusajs.com/add-plugins/sendgrid/#template-reference).\n\n#### Variable functions\n\nAt the moment the only variable you can use functions with is dates and currency.   \n- **Dates** are formatted using the `toLocaleDateString` function and can be used like this: `{{ order.placed_at | date('en-US',{'year': 'numeric', 'month': 'long', 'day': 'numeric'}) }}`.  \n- **Currency** is formatted using the `new Intl.NumberFormat()` function and can be used like this: `{{ order.total_price | currency('en-US') }}`.\n- **Country** can be formatted from ISO to the full country name and can be used like this: `{{ order.shipping_address.country_code | country }}`.  \n**Please make sure that the options are wrapped in single quotes.**\n\n### Localisation\n\nWant separate templates for different languages?  \nAlter medusa-config.js plugin options:\n\n```js\n// medusa config including the postmark plugin\nevents: {\n    order: {\n        placed: { nl: 1234, en: 1235 },\n// rest of the events...\n```\n\nThe api key and templates are pulled from env variables.  \n```\nPOSTMARK_SERVER_API=\"\"\nPOSTMARK_FROM=\"\"\nPOSTMARK_ORDER_PLACED=1234\n```\n\nThe `POSTMARK_FROM` email address must be a verified sender in your Postmark account.\n\n## Default templates\n\nWe've created a few default templates _(thanks to [pdfkit invoice example](https://github.com/PSPDFKit-labs/pdfkit-invoice/tree/master))_ which can be altered to your needs:\n`header.json`\n```json\n[\n  {\n    \"type\": \"text\",\n    \"text\": \"ACME Inc.\",\n    \"size\": 20,\n    \"color\": \"#444444\"\n  },\n  {\n    \"type\": \"text\",\n    \"text\": \"ACME Inc.\",\n    \"size\": 10,\n    \"color\": \"#444444\",\n    \"align\": \"right\",\n    \"x\": 200\n  },\n  {\n    \"type\": \"moveDown\"\n  },\n  {\n    \"type\": \"text\",\n    \"text\": \"123 Main Street\",\n    \"size\": 10,\n    \"color\": \"#444444\",\n    \"align\": \"right\",\n    \"x\": 200\n  },\n  {\n    \"type\": \"moveDown\"\n  },\n  {\n    \"type\": \"text\",\n    \"text\": \"New York, NY, 10025\",\n    \"size\": 10,\n    \"color\": \"#444444\",\n    \"align\": \"right\",\n    \"x\": 200\n  }\n]\n\n```\n\n`createInvoice.json`\n\n```json\n[\n  {\n    \"type\": \"text\",\n    \"text\": \"Invoice\",\n    \"size\": 20,\n    \"color\": \"#444444\"\n  },\n  {\n    \"type\": \"moveDown\"\n  },\n  {\n    \"type\": \"hr\"\n  },\n  {\n    \"type\": \"text\",\n    \"text\": \"Invoice Number:\",\n    \"size\": 10\n  },\n  {\n    \"type\": \"text\",\n    \"font\": \"Helvetica-Bold\",\n    \"text\": \"#{{ order.display_id }}\",\n    \"size\": 10,\n    \"x\": 100\n  },\n  {\n    \"type\": \"text\",\n    \"font\": \"Helvetica-Bold\",\n    \"text\": \"{{ order.shipping_address.first_name }} {{ order.shipping_address.last_name }}\",\n    \"size\": 10,\n    \"x\": 300\n  },\n  {\n    \"type\": \"moveDown\"\n  },\n  {\n    \"type\": \"text\",\n    \"font\": \"Helvetica\",\n    \"text\": \"Invoice Date:\",\n    \"size\": 10\n  },\n  {\n    \"type\": \"text\",\n    \"text\": \"{{ order.created_at | date('en-US',{'year': 'numeric', 'month': 'long', 'day': 'numeric'}) }}\",\n    \"size\": 10,\n    \"x\": 100\n  },\n  {\n    \"type\": \"text\",\n    \"text\": \"{{ order.shipping_address.address_1 }} {{ order.shipping_address.address_2 }}\",\n    \"size\": 10,\n    \"x\": 300\n  },\n  {\n    \"type\": \"moveDown\"\n  },\n  {\n    \"type\": \"text\",\n    \"text\": \"{{ order.shipping_address.postal_code }}, {{ order.shipping_address.city }}, {{ order.shipping_address.country_code }}\",\n    \"size\": 10,\n    \"x\": 300\n  },\n  {\n    \"type\": \"moveDown\"\n  },\n  {\n    \"type\": \"hr\"\n  },\n  {\n    \"type\": \"moveDown\",\n    \"lines\": 1\n  },\n  {\n    \"type\": \"tableRow\",\n    \"font\": \"Helvetica-Bold\",\n    \"columns\": [\n      {\n        \"text\": \"Item\",\n        \"width\": 200\n      },\n      {\n        \"text\": \"Quantity\",\n        \"width\": 50\n      },\n      {\n        \"text\": \"Price\",\n        \"width\": 50\n      },\n      {\n        \"text\": \"Total\",\n        \"width\": 50\n      }\n    ]\n  },\n  {\n    \"type\": \"hr\"\n  },\n  {\n    \"type\": \"itemLoop\"\n  },\n  {\n    \"type\": \"tableRow\",\n    \"font\": \"Helvetica\",\n    \"columns\": [\n      {\n        \"text\": \"{{ item.title }}\",\n        \"width\": 200\n      },\n      {\n        \"text\": \"{{ item.quantity }}\",\n        \"width\": 50\n      },\n      {\n        \"text\": \"{{ item.unit_price | currency('en-US') }}\",\n        \"width\": 50\n      },\n      {\n        \"text\": \"{{ item.totals.total | currency('en-US') }}\",\n        \"width\": 50\n      }\n    ]\n  },\n  {\n    \"type\": \"hr\"\n  },\n  {\n    \"type\": \"itemLoopEnd\"\n  },\n  {\n    \"type\": \"tableRow\",\n    \"columns\": [\n      {\n        \"text\": \"\",\n        \"width\": 200\n      },\n      {\n        \"text\": \"\",\n        \"width\": 50\n      },\n      {\n        \"text\": \"Subtotal\",\n        \"width\": 50\n      },\n      {\n        \"text\": \"{{ order.subtotal | currency('en-US') }}\",\n        \"width\": 50\n      }\n    ]\n  },\n  {\n    \"type\": \"tableRow\",\n    \"columns\": [\n      {\n        \"text\": \"\",\n        \"width\": 200\n      },\n      {\n        \"text\": \"\",\n        \"width\": 50\n      },\n      {\n        \"text\": \"Shipping\",\n        \"width\": 50\n      },\n      {\n        \"text\": \"{{ order.shipping_total | currency('en-US') }}\",\n        \"width\": 50\n      }\n    ]\n  },\n  {\n    \"type\": \"tableRow\",\n    \"columns\": [\n      {\n        \"text\": \"\",\n        \"width\": 200\n      },\n      {\n        \"text\": \"\",\n        \"width\": 50\n      },\n      {\n        \"text\": \"TAX\",\n        \"width\": 50\n      },\n      {\n        \"text\": \"{{ order.tax_total | currency('en-US') }}\",\n        \"width\": 50\n      }\n    ]\n  },\n  {\n    \"type\": \"tableRow\",\n    \"font\": \"Helvetica-Bold\",\n    \"columns\": [\n      {\n        \"text\": \"\",\n        \"width\": 200\n      },\n      {\n        \"text\": \"\",\n        \"width\": 50\n      },\n      {\n        \"text\": \"Total\",\n        \"width\": 50\n      },\n      {\n        \"text\": \"{{ order.total | currency('en-US') }}\",\n        \"width\": 50\n      }\n    ]\n  }\n]\n\n```\n`footer.json`\n```json\n[\n  {\n    \"type\": \"text\",\n    \"text\": \"Thank you for your business!\",\n    \"size\": 10,\n    \"color\": \"#444444\",\n    \"width\": \"full\",\n    \"align\": \"center\"\n  }\n]\n\n```\n\n## Acknowledgement\n\nThis plugin is originally based on medusa-plugin-sendgrid by Oliver Juhl.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FFullstak-nl%2Fmedusa-plugin-postmark","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FFullstak-nl%2Fmedusa-plugin-postmark","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FFullstak-nl%2Fmedusa-plugin-postmark/lists"}