Skip to content
PPDFInvoiceAPI
All posts
stripereceiptswebhooksautomationtutorial

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 PDFInvoiceAPI team3 min read

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:

  1. Listen for Stripe’s checkout.session.completed (or payment_intent.succeeded) webhook.
  2. Render a receipt PDF from a template + the payment data via POST /v1/render.
  3. 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 constructEvent needs 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 a Buffer for the attachment; don’t await 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 via template (or the full html field) — 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

Related posts