Overview
Icon for Invoices

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

pnpm add @webbers/invoices-medusa

Configuration

Register the plugin in Copy to clipboardmedusa-config.ts:

import { 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 header
companyLogo: "<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

  1. Fulfillment created — the Copy to clipboardorder.fulfillment_created subscriber fires Copy to clipboardcreateInvoiceWorkflow, which:
    • Creates a debit invoice record with an auto-incremented Copy to clipboarddisplay_id
    • Links it to the order via the Copy to clipboardinvoice_order link table
    • Generates the PDF and uploads it to the private storage bucket
    • Stores the file ID in Copy to clipboardinvoice.pdf_url
  2. Refund processedCopy to clipboardcreateCreditInvoiceWorkflow follows the same steps for a credit invoice, referencing the original debit invoice as its parent.
  3. Download requested — the API route resolves Copy to clipboardinvoice.pdf_url to 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:

await orderModuleService.updateOrders(orderId, {
metadata: { locale: "en" },
})

API routes

Admin

GET /admin/orders/:id/invoice/:invoice_id

Requires admin authentication. Redirects to a presigned download URL for the invoice PDF.

Store

GET /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.

import { 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.

import { createCreditInvoiceWorkflow } from "@webbers/invoices-medusa/workflows"
await createCreditInvoiceWorkflow(container).run({
input: {
order_id: "order_01J...",
resource_id: "refund_01J...", // refund ID used as resource_id
parent_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.

import { 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:

# Dry run (default) — shows what would be processed
pnpm medusa exec ./src/scripts/backfill-invoice-pdf-urls.ts
# Execute
pnpm medusa exec ./src/scripts/backfill-invoice-pdf-urls.ts -- dry_run=false
# Smaller batches if memory is a concern
pnpm medusa exec ./src/scripts/backfill-invoice-pdf-urls.ts -- dry_run=false batch_size=10

You may also like

Browse all integrations

Build your own

Develop your own custom integration

Build your own integration with our API to speed up your processes. Make your integration available via npm for it to be shared in our Library with the broader Medusa community.

gift card interface

Ready to build your custom commerce setup?