Auto-Generate Branded PDF Invoices from Stripe Payment Events

Stripe issues a PDF for every invoice, but the layout is locked. You can upload a logo and pick a brand color, yet you cannot add VAT IDs, custom fonts, or a…

Stripe issues a PDF for every invoice, but the layout is locked. You can upload a logo and pick a brand color, yet you cannot add VAT IDs, custom fonts, or a status badge that matches your design system. A webhook handler that calls MarkupGo's /pdf API with a reusable template gives you full control in one POST — no Puppeteer binary to wrestle into your serverless function, no font caching issues, no timeout risk.

What Stripe's built-in PDF gives you and where it stops

Stripe's invoice PDF customization allows an icon, logo, and brand color, with logo files capped at 512KB and minimum dimensions of 128x128px [1]. You also get four custom key-value fields, typically used for PO numbers, contractor numbers, or tax compliance labels [1]. Page size is restricted to A4 or Letter [1]. The hosted invoice page supports Adaptive Pricing in more than 150 countries, but the PDF layout itself does not adapt [2].

A more subtle limitation is tax display. With inclusive tax configured, the hosted invoice page and emails show tax-inclusive prices while the PDF can show tax-exclusive prices — a mismatch that confuses finance teams and customers alike [1]. Hosted invoice URLs expire 30 days after the due date, never longer than 120 days, which complicates long-term record keeping [2].

Receipts are even more constrained: logo and colors only, with the same 512KB logo cap [3].

Which Stripe event to listen for

Stripe recommends invoice.paid over invoice.payment_succeeded because it fires for both successful payments and out-of-band payments [4]. Both events carry identical invoice data, so handling only one prevents duplicate PDF generation [4].

If you use Checkout, the resulting paid invoices contain more detail than standard receipts [3].

Build the invoice template in MarkupGo

Create a reusable template in the MarkupGo dashboard editor. Use standard HTML and CSS with your brand fonts, colors, and layout rules. MarkupGo uses Handlebars for dynamic data, with {{ variable }} placeholder syntax [5].

For the line items table, wrap each row in {{#each items}} to repeat it for every entry in the array [5]. A minimal item loop looks like this:

{{#each items}}
<div class="grid grid-cols-5 gap-2">
  <div class="col-span-2">{{item_name}}</div>
  <div>{{qty}}</div>
  <div>{{rate}}</div>
  <div class="text-end">{{amount}}</div>
</div>
{{/each}}

Template fields including html, css, and context can be overridden at generation time if you need per-invoice tweaks [6].

Wire the webhook handler

Verify the Stripe signature with your webhook secret before parsing the event body. Then map the Stripe invoice object to a JSON context:

  • Convert amount_due, amount_paid, and line item amounts from smallest-unit integers to decimal strings. Handle zero-decimal currencies like JPY separately.
  • Format discounts as negative line items or a dedicated discount total.
  • Check lines.has_more and paginate if the invoice exceeds Stripe's default page size.

Post to MarkupGo's /pdf endpoint with source.type set to 'template', the template ID, and your constructed context object [5]. The response returns a task with a hosted PDF URL [7]. If you need the raw file instead, append /buffer to fetch it directly [7].

const response = await fetch('https://api.markupgo.com/api/v1/pdf', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': process.env.MARKUPGO_API_KEY
  },
  body: JSON.stringify({
    source: {
      type: 'template',
      data: {
        id: process.env.INVOICE_TEMPLATE_ID,
        context: {
          invoice_number: invoice.number,
          customer_email: invoice.customer_email,
          items: invoice.lines.data.map(line => ({
            item_name: line.description,
            qty: line.quantity,
            rate: (line.price.unit_amount / 100).toFixed(2),
            amount: (line.amount / 100).toFixed(2)
          })),
          total: (invoice.total / 100).toFixed(2)
          // see Markupgo's API reference for additional fields
        }
      }
    },
    options: {
      properties: {
        size: { width: 210, height: 297 },
        margins: { top: 10, bottom: 10, left: 10, right: 10 }
      }
    },
    expiration: { expiresInSeconds: 86400 }
  })
});

const task = await response.json();
// task.url contains the hosted PDF

Set expiresInSeconds to auto-delete the file after delivery if you do not want to host it long-term. The range runs from 60 seconds to 90 days [7].

Deliver the PDF and handle retries

Return 200 to Stripe immediately after queuing the PDF job. Let MarkupGo handle rendering asynchronously so your function stays under timeout even during traffic spikes.

For delivery, pass the task.url to your ESP for email attachment, or fetch /buffer and stream the bytes to S3 or R2 with a per-invoice key for idempotency. Stripe retries webhook delivery on failure, so idempotent storage prevents duplicate files.

Why not Puppeteer or pdfkit

Self-hosted rendering carries real operational cost. PageBolt estimates a Puppeteer-based invoice pipeline at $100-300 per month for modest volume. RenderPDFs quotes pdfkit at approximately 15MB and 2-3 days setup, versus Puppeteer in-app at roughly 300MB and 3-5 days.

Competitor PDF URLs from RenderPDFs expire after 24 hours. MarkupGo lets you set expiration from 60 seconds to 90 days, or leave files persistent [7].

Prototype it now

Sign up for MarkupGo's free trial — 100 credits per month, no credit card required. Build your invoice template in the dashboard, paste the webhook handler into your Stripe endpoint, and trigger a test event from the Stripe CLI. One POST to /pdf returns your branded invoice PDF [5].

MarkupGo also offers an image API (PNG, JPEG, WebP) and an Office-to-PDF API supporting 130+ document types [6].

Sources

  1. Customize invoices (docs.stripe.com)
  2. Hosted invoice page (docs.stripe.com)
  3. Email receipts and paid invoices (docs.stripe.com)
  4. Integrate with the Invoicing API (docs.stripe.com)
  5. markupgo.com
  6. markupgo.com
  7. markupgo.com