From a Stripe payment to a branded PDF receipt in ~20 lines
Wire a Stripe webhook to PDFInvoiceAPI: when a payment succeeds, render a branded PDF receipt and email it to the customer automatically. Real, copy-pasteable code.
The moment a Stripe payment succeeds is the canonical trigger for a receipt. The customer expects a PDF in their inbox; you’d rather not run a PDF service to produce it. This is the smallest possible version of that flow: a Stripe webhook fires, you render a branded PDF receipt with one API call, and you email it — start to finish in about twenty lines of handler code.
The shape of the flow
Three steps, all server-side:
- Listen for Stripe’s
checkout.session.completed(orpayment_intent.succeeded) webhook. - Render a receipt PDF from a template + the payment data via
POST /v1/render. - Email the PDF to the customer as an attachment.
You’ll need a PDFInvoiceAPI key (free to start) and your Stripe webhook secret.
A reusable receipt template
Save the layout once so the handler only ships data. The merge engine supports {{ }} (HTML-escaped), {{{ }}} (raw), and dotted paths — no loops — which is plenty for a receipt’s fixed fields. Store this in Dashboard → Templates and note its id (tpl_…):
<style>
body { font-family: system-ui, sans-serif; color: #1a1a23; padding: 40px; }
.brand { color: #5b3df5; font-weight: 700; font-size: 1.5rem; }
.row { display: flex; justify-content: space-between; margin-top: 8px; }
.total { font-size: 1.3rem; font-weight: 700; border-top: 2px solid #1a1a23; padding-top: 12px; }
</style>
<div class="brand">Acme Inc.</div>
<h1>Receipt {{receiptNumber}}</h1>
<p>Paid by {{customer.email}} on {{paidAt}}</p>
<div class="row"><span>{{description}}</span><span>{{amount}}</span></div>
<div class="row total"><span>Total paid</span><span>{{amount}}</span></div>
The webhook handler
This is the whole thing — verify the event, render the PDF by template id, email it. (Using Node with the Stripe SDK and Resend for email; swap in your stack.)
import Stripe from "stripe";
import { Resend } from "resend";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const resend = new Resend(process.env.RESEND_API_KEY);
export async function handleStripeWebhook(rawBody, signature) {
const event = stripe.webhooks.constructEvent(
rawBody,
signature,
process.env.STRIPE_WEBHOOK_SECRET,
);
if (event.type !== "checkout.session.completed") return;
const session = event.data.object;
const amount = (session.amount_total / 100).toLocaleString("en-IE", {
style: "currency",
currency: session.currency.toUpperCase(),
});
// 1. Render the receipt PDF — template id + data, one call.
const pdfRes = await fetch("https://api.pdfinvoiceapi.com/v1/render", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PDFINVOICEAPI_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
template: "tpl_your_receipt_template_id",
data: {
receiptNumber: session.id.slice(-8).toUpperCase(),
customer: { email: session.customer_details.email },
paidAt: new Date().toLocaleDateString("en-IE"),
description: "Pro plan — annual",
amount,
},
pdf: { format: "A4", printBackground: true },
}),
});
if (!pdfRes.ok) throw new Error(await pdfRes.text());
const pdf = Buffer.from(await pdfRes.arrayBuffer());
// 2. Email it as an attachment.
await resend.emails.send({
from: "receipts@acme.com",
to: session.customer_details.email,
subject: "Your Acme receipt",
text: "Thanks for your purchase — your receipt is attached.",
attachments: [{ filename: "receipt.pdf", content: pdf }],
});
}
That’s the entire path from payment to inbox. The render call is the only PDFInvoiceAPI-specific part, and it’s a single fetch.
A few things worth getting right
- Verify the signature on the raw body. Stripe’s
constructEventneeds the exact bytes — don’t let a JSON body-parser mutate the request first, or verification fails. - The response is binary. Read
arrayBuffer()and wrap it in aBufferfor the attachment; don’tawait res.json(). - Keep templates loop-free. A receipt’s fields are fixed, so the
{{ }}/{{{ }}}/ dotted-path engine covers it. If you ever need repeating line items, build that HTML in your handler and send it viatemplate(or the fullhtmlfield) — see how to render from HTML. - Make it idempotent. Stripe can deliver a webhook more than once; key off the event id so you don’t email two receipts.
From here
- The full render reference — every
pdfoption and thetemplate+datarules. - Stored templates — save once, render by id.
- Start free — wire your test-mode Stripe webhook to a real receipt today.