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:
- A Stripe webhook listener
- A receipt PDF generator (we'll use Kagyz)
- 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);
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();
}
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"),
},
],
});
}
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}`);
}
When a Stripe payment succeeds:
- Webhook fires
- We build receipt data from the payment intent
- Kagyz generates the PDF
- 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
}
]
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
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 viaPOST /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.
Top comments (0)