Invoices
Auto-generate PDF invoices and credit notes
@webbers/invoices-medusa
A Medusa v2 plugin that automatically generates sequential PDF invoices for orders and credit notes for refunds. PDFs are uploaded to your private file storage bucket and served to customers and admins via presigned URLs.
Requirements
- Medusa Copy to clipboard
>= 2.4.0 - A configured File Module Provider (e.g. S3/R2) with private bucket support
Installation
1pnpm add @webbers/invoices-medusa
Configuration
Register the plugin in Copy to clipboardmedusa-config.ts:
1234567891011121314151617181920import { defineConfig } from "@medusajs/framework/utils"export default defineConfig({plugins: [{resolve: "@webbers/invoices-medusa",options: {// Optional: locale to fall back to when order.metadata.locale is absent// or not a supported value. Defaults to "nl".defaultLocale: "en",addressInfo: {// Optional: SVG string or base64 data URL shown in the default headercompanyLogo: "<svg>...</svg>",// Optional: rendered width of the logo in points (default: 110)companyLogoWidth: 110,companyName: "Acme B.V.",// Receives the i18n countries object for the resolved locale so you
Configuration reference
Option Type Required Description Copy to clipboarddefaultLocale Copy to clipboardLocale No Fallback locale when Copy to clipboardorder.metadata.locale is absent or invalid. Defaults to Copy to clipboard"nl". Copy to clipboardaddressInfo.companyName Copy to clipboardstring Yes Company name shown on the invoice. Copy to clipboardaddressInfo.address Copy to clipboard(countries: Record<string, string>) => string Yes Function returning the company address. Receives the i18n country name map for the resolved locale. Copy to clipboardaddressInfo.cocNumber Copy to clipboardstring Yes Chamber of Commerce number. Copy to clipboardaddressInfo.vatNumber Copy to clipboardstring Yes VAT registration number. Copy to clipboardaddressInfo.iban Copy to clipboardstring Yes Bank account number. Copy to clipboardaddressInfo.email Copy to clipboardstring Yes Billing contact e-mail. Copy to clipboardaddressInfo.companyLogo Copy to clipboardstring No SVG string or data URL used in the default header. Copy to clipboardaddressInfo.companyLogoWidth Copy to clipboardnumber No Rendered logo width in points. Copy to clipboardcolors.background Copy to clipboardstring No Table header fill color (CSS hex). Copy to clipboardcolors.text Copy to clipboardstring No Table header text color (CSS hex). Copy to clipboardheader Copy to clipboardContent No pdfmake content block rendered as page header. Replaces the default logo header. Copy to clipboardfooter Copy to clipboardContent No pdfmake content block rendered as page footer.
How it works
Invoice lifecycle
- Fulfillment created — the Copy to clipboard
order.fulfillment_createdsubscriber fires Copy to clipboardcreateInvoiceWorkflow, which:- Creates a debit invoice record with an auto-incremented Copy to clipboard
display_id - Links it to the order via the Copy to clipboard
invoice_orderlink table - Generates the PDF and uploads it to the private storage bucket
- Stores the file ID in Copy to clipboard
invoice.pdf_url
- Creates a debit invoice record with an auto-incremented Copy to clipboard
- Refund processed — Copy to clipboard
createCreditInvoiceWorkflowfollows the same steps for a credit invoice, referencing the original debit invoice as its parent. - Download requested — the API route resolves Copy to clipboard
invoice.pdf_urlto a presigned download URL via Copy to clipboardfileModuleService.retrieveFile()and redirects the client. Invoices without a stored PDF (created before this feature) are generated on-demand as a fallback.
Invoice numbering
Copy to clipboarddisplay_id is a PostgreSQL auto-increment sequence. Voided invoices (created by workflow compensation on failure) preserve their sequence number to avoid gaps.
Localization
The PDF language is determined by Copy to clipboardorder.metadata.locale. The value is validated against the list of supported locales; if it is absent or unrecognised, Copy to clipboarddefaultLocale from the plugin config is used.
Supported locales:
Value Language Copy to clipboardnl Dutch Copy to clipboardnl-be Dutch (Belgium) — uses Dutch translations Copy to clipboarden English Copy to clipboardde German Copy to clipboardfr French Copy to clipboardit Italian
Set the locale on an order at creation time:
123await orderModuleService.updateOrders(orderId, {metadata: { locale: "en" },})
API routes
Admin
1GET /admin/orders/:id/invoice/:invoice_id
Requires admin authentication. Redirects to a presigned download URL for the invoice PDF.
Store
1GET /store/orders/:id/invoice
Requires customer authentication. Returns the debit invoice PDF for the order. The order must belong to the authenticated customer.
Workflows
The workflows can be called directly from your own subscribers, jobs, or API routes.
Copy to clipboardcreateInvoiceWorkflow
Creates a debit invoice, links it to an order, and generates + uploads the PDF.
12345import { createInvoiceWorkflow } from "@webbers/invoices-medusa/workflows"await createInvoiceWorkflow(container).run({input: { order_id: "order_01J..." },})
Copy to clipboardcreateCreditInvoiceWorkflow
Creates a credit invoice for a refund.
123456789import { createCreditInvoiceWorkflow } from "@webbers/invoices-medusa/workflows"await createCreditInvoiceWorkflow(container).run({input: {order_id: "order_01J...",resource_id: "refund_01J...", // refund ID used as resource_idparent_invoice_id: "inv_01J...", // optional: the debit invoice this offsets},})
Copy to clipboardgenerateInvoicePdfWorkflow
Generates an invoice PDF on-demand and returns it as a base64 string. Useful for attaching to notification emails.
1234567891011import { generateInvoicePdfWorkflow } from "@webbers/invoices-medusa/workflows"const { result } = await generateInvoicePdfWorkflow(container).run({input: {order_id: "order_01J...",invoice_id: "inv_01J...", // optional; omit to generate the debit invoice},})// result.fileName — e.g. "invoice-42.pdf"// result.data — base64-encoded PDF
Backfill script
To generate and upload PDFs for invoices created before file storage was introduced:
12345678# Dry run (default) — shows what would be processedpnpm medusa exec ./src/scripts/backfill-invoice-pdf-urls.ts# Executepnpm medusa exec ./src/scripts/backfill-invoice-pdf-urls.ts -- dry_run=false# Smaller batches if memory is a concernpnpm medusa exec ./src/scripts/backfill-invoice-pdf-urls.ts -- dry_run=false batch_size=10

