DEV Community

jalalbmnf
jalalbmnf

Posted on

Auto-Generate PDF Receipts After Stripe Payments

Every app that takes payments needs to send receipts. Stripe has built-in receipts, but they're Stripe-branded and limited. If you need custom receipts — with your company info, tax breakdowns, payment details — you have to generate them yourself.

The typical approach is setting up Puppeteer, writing an HTML template, managing CSS, and rendering PDFs on your server. It works, but it's heavy and annoying to maintain.

Here's a simpler approach: listen for Stripe's payment_intent.succeeded webhook, call a receipt API, and email the PDF to your customer. No HTML templates. No Puppeteer. Under 50 lines of code.

The Setup

We need three things:

  1. A Stripe webhook listener
  2. A receipt PDF generator (we'll use Kagyz)
  3. An email sender (SendGrid, Resend, or any SMTP)

Step 1: Listen for Stripe Payments

import Stripe from "stripe";
import express from "express";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const app = express();

app.post(
  "/webhook/stripe",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const sig = req.headers["stripe-signature"];

    let event;
    try {
      event = stripe.webhooks.constructEvent(
        req.body,
        sig,
        process.env.STRIPE_WEBHOOK_SECRET
      );
    } catch (err) {
      return res.status(400).send(`Webhook Error: ${err.message}`);
    }

    if (event.type === "payment_intent.succeeded") {
      await handlePayment(event.data.object);
    }

    res.json({ received: true });
  }
);

app.listen(3000);
Enter fullscreen mode Exit fullscreen mode

Standard Stripe webhook setup. Nothing special here — we verify the signature and handle payment_intent.succeeded.

Step 2: Generate the Receipt PDF

This is where it gets simple. Instead of rendering HTML with Puppeteer, we send JSON to Kagyz and get a PDF back:

async function generateReceipt(paymentData) {
  const response = await fetch("https://api.kagyz.com/v1/receipt", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.KAGYZ_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      receipt_number: `REC-${paymentData.id.slice(-8)}`,
      date: new Date().toISOString().split("T")[0],
      currency: paymentData.currency.toUpperCase(),
      payment_method: paymentData.payment_method_types[0],
      transaction_id: paymentData.id,
      from: {
        name: "Your Company Name",
        email: "billing@yourcompany.com",
        address: "123 Business St\nNew York, NY 10001",
      },
      to: {
        name: paymentData.metadata.customer_name || "Customer",
        email: paymentData.receipt_email,
      },
      items: [
        {
          description: paymentData.description || "Payment",
          quantity: 1,
          unit_price: paymentData.amount / 100,
        },
      ],
      notes: "Thank you for your purchase!",
    }),
  });

  return await response.arrayBuffer();
}
Enter fullscreen mode Exit fullscreen mode

That's it. No HTML template. No CSS. No Puppeteer running on your server. The API returns a professionally formatted PDF with a PAID badge, payment method, transaction ID, and all the details your customer needs.

Step 3: Email it to the Customer

import { Resend } from "resend";

const resend = new Resend(process.env.RESEND_API_KEY);

async function emailReceipt(email, pdfBuffer, receiptNumber) {
  await resend.emails.send({
    from: "billing@yourcompany.com",
    to: email,
    subject: `Receipt ${receiptNumber} — Thank you for your payment`,
    html: `<p>Hi,</p><p>Thanks for your payment. Your receipt is attached.</p><p>— Your Company</p>`,
    attachments: [
      {
        filename: `receipt-${receiptNumber}.pdf`,
        content: Buffer.from(pdfBuffer).toString("base64"),
      },
    ],
  });
}
Enter fullscreen mode Exit fullscreen mode

Putting It All Together

async function handlePayment(paymentIntent) {
  // Skip if no email
  if (!paymentIntent.receipt_email) return;

  const receiptNumber = `REC-${paymentIntent.id.slice(-8)}`;

  // Generate PDF
  const pdfBuffer = await generateReceipt(paymentIntent);

  // Email to customer
  await emailReceipt(
    paymentIntent.receipt_email,
    pdfBuffer,
    receiptNumber
  );

  console.log(`Receipt ${receiptNumber} sent to ${paymentIntent.receipt_email}`);
}
Enter fullscreen mode Exit fullscreen mode

When a Stripe payment succeeds:

  1. Webhook fires
  2. We build receipt data from the payment intent
  3. Kagyz generates the PDF
  4. We email it to the customer

The whole flow runs in under a second.

Adding Tax Support

If you need tax on receipts (VAT, GST, sales tax), add a tax field:

// Invoice-level tax — applies to all items
tax: { rate: 20 }  // 20% VAT

// Or item-level tax — different rates per item
items: [
  {
    description: "Software License",
    quantity: 1,
    unit_price: 99,
    tax: { rate: 20 }  // 20% on this item
  },
  {
    description: "Support Plan",
    quantity: 1,
    unit_price: 29,
    tax: { rate: 0 }   // Tax-exempt
  }
]
Enter fullscreen mode Exit fullscreen mode

Item-level tax overrides invoice-level tax for that item — useful for EU VAT where digital services and physical goods have different rates.

Why Not Just Use Stripe's Built-in Receipts?

Stripe's receipts work, but they have limitations:

  • Stripe branding — the receipt says Stripe, not your company
  • Limited customization — you can't add custom fields, detailed tax breakdowns, or your own notes
  • No PDF file — Stripe sends an email with a link, not a downloadable PDF attachment
  • Inconsistent with your other documents — if you also generate invoices and quotes, they'll look different from Stripe's receipts

With Kagyz, your receipts match your invoices. Same design, same layout, same professional look — just different content.

Testing Locally

Use the Stripe CLI to forward webhooks to your local server:

# Install Stripe CLI, then:
stripe listen --forward-to localhost:3000/webhook/stripe

# In another terminal, trigger a test payment:
stripe trigger payment_intent.succeeded
Enter fullscreen mode Exit fullscreen mode

The Full Picture

Once this is running, you have automated receipts with zero manual work. But the same pattern works for other documents too:

  • payment_intent.succeeded → generate receipt via POST /v1/receipt
  • Subscription created → generate invoice via POST /v1/invoice
  • Deal closed in your CRM → generate quote via POST /v1/quote
  • Refund issued → generate credit note via POST /v1/credit-note

Same API, same design language, four document types. Full docs here.


Kagyz is a JSON-to-PDF API for business documents. Free tier: 100 PDFs/month, no credit card needed.

kagyz.com
API docs
Receipt endpoint docs

Top comments (0)