Receipt PDF API: generate receipts from HTML
Receipt PDF API: turn HTML into 80 mm thermal or A4 receipts from a Stripe or Shopify webhook, escaped safely and emailed to the customer.
Quick answer. Build the receipt as HTML, then POST it with a custom page size. For an 80 mm thermal slip, set width to 80mm and pick a height that fits the lines.
curl -X POST https://html-pdf.com/v1/pdf \
-H "Authorization: Bearer $HTML_PDF_KEY" \
-H "Content-Type: application/json" \
-d '{"html": "<h1>Corner Coffee</h1><p>Total: $12.20</p>", "width": "80mm", "height": "100mm", "margin": "0"}' \
-o receipt.pdf
A receipt confirms a payment that already happened. It shows what was bought, how much was paid, how, and when.
Payment platforms send their own receipts, but many businesses want one with their branding, tax details or language. Others need a printable slip for a counter or a delivery bag. HTML is a comfortable way to design it, and an HTML-to-PDF API turns it into a file you can email or print.
Thermal (80 mm) vs A4 receipts
This table helps you pick the right format for where the receipt ends up.
| 80 mm thermal | A4 / Letter | |
|---|---|---|
| Use | POS, kitchen, delivery slips | Email attachments, accounting, B2B |
| Page size | width: "80mm" + a height you choose |
format: "A4" or "Letter" |
| Layout | One narrow column, monospace or compact sans | Header, line-item table, totals block |
| Margins | 0 to a few mm |
15 to 20 mm |
For a thermal receipt, set the width to the roll width. 80 mm is common, and 58 mm also exists. Keep content inside the printer's printable area, which is narrower than the paper, so check your printer's spec.
PDF pages need a fixed height. Estimate it from the number of lines, for example 70 mm plus 6 mm per item, and pass it as height.
For A4, the free receipt template is a ready layout. Set the size in CSS with @page { size: A4; margin: 16mm; } and send prefer_css_page_size: true, or pass format and margin as options.
A compact receipt template
Here is a minimal thermal receipt layout with placeholders you fill in from your order data.
<!doctype html><html><head><meta charset="utf-8"><style>
body { margin: 0; padding: 4mm; font: 11px/1.4 ui-monospace, Menlo, monospace; color: #000; }
h1 { font-size: 14px; text-align: center; margin: 0 0 2mm; }
.row { display: flex; justify-content: space-between; }
hr { border: 0; border-top: 1px dashed #000; margin: 2mm 0; }
.total { font-weight: bold; font-size: 13px; }
</style></head><body>
<h1>Corner Coffee</h1>
<div style="text-align:center">Receipt __NUMBER__<br>__DATE__</div><hr>
__ITEMS__
<hr><div class="row total"><span>Total</span><span>__TOTAL__</span></div>
<div style="text-align:center;margin-top:3mm">Paid by __METHOD__ · Thank you</div>
</body></html>
Webhook → HTML → PDF → email (Node.js)
This example gives you a complete receipt pipeline. It listens for Stripe's checkout.session.completed event, builds the receipt, renders it and emails it.
The same pattern works for a Shopify orders/paid webhook. Verify the signature, read the order and build the HTML.
import express from "express";
import Stripe from "stripe";
import nodemailer from "nodemailer";
import fs from "node:fs";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const mailer = nodemailer.createTransport(process.env.SMTP_URL);
const tpl = fs.readFileSync("receipt.html", "utf8");
const esc = s => String(s ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
const money = (cents, cur) => new Intl.NumberFormat("en-US", { style: "currency", currency: cur }).format(cents / 100);
const app = express();
app.post("/webhooks/stripe", express.raw({ type: "application/json" }), async (req, res) => {
let event;
try {
event = stripe.webhooks.constructEvent(req.body, req.headers["stripe-signature"], process.env.STRIPE_WEBHOOK_SECRET);
} catch { return res.sendStatus(400); }
if (event.type !== "checkout.session.completed") return res.sendStatus(200);
const s = event.data.object;
const { data: lines } = await stripe.checkout.sessions.listLineItems(s.id);
const items = lines.map(l =>
`<div class="row"><span>${l.quantity}× ${esc(l.description)}</span><span>${money(l.amount_total, s.currency)}</span></div>`).join("");
const html = tpl.replace("__NUMBER__", esc(s.id.slice(-8).toUpperCase()))
.replace("__DATE__", new Date(s.created * 1000).toISOString().slice(0, 16).replace("T", " "))
.replace("__ITEMS__", items).replace("__TOTAL__", money(s.amount_total, s.currency))
.replace("__METHOD__", "card");
const pdf = await fetch("https://html-pdf.com/v1/pdf", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.HTML_PDF_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ html, width: "80mm", height: `${70 + lines.length * 6}mm`, margin: "0", max_pages: 2 }),
});
if (!pdf.ok) return res.sendStatus(500); // non-2xx makes Stripe retry later
await mailer.sendMail({
from: "receipts@example.com",
to: s.customer_details?.email,
subject: "Your receipt",
text: "Thanks for your purchase. Your receipt is attached.",
attachments: [{ filename: `receipt-${s.id.slice(-8)}.pdf`, content: Buffer.from(await pdf.arrayBuffer()) }],
});
res.sendStatus(200);
});
app.listen(3000);
Webhooks can be delivered more than once. In production, store the event ID and skip duplicates before sending another email.
Escape customer data
This section keeps customer input from breaking your receipt. Product names, customer names and addresses come from users, so if you paste them into HTML unescaped, a name like <img src=x onerror=...> becomes markup.
Escape every dynamic value (the esc helper above), or use a template engine that escapes by default, such as Handlebars, Nunjucks or Jinja2. Never insert raw HTML from an order note.
Python: an A4 receipt
This snippet renders a filled A4 receipt and returns a download link instead of the file.
import os, requests
html = open("receipt-a4.html", encoding="utf-8").read() # already filled and escaped
r = requests.post("https://html-pdf.com/v1/pdf",
headers={"Authorization": f"Bearer {os.environ['HTML_PDF_KEY']}"},
json={"html": html, "prefer_css_page_size": True, "response": "url"}, timeout=60)
r.raise_for_status()
print(r.json()) # contains a download URL valid for 24 hours
A link (valid for 24 hours) is handy for an order-confirmation page. For email, attach the binary so the receipt doesn't expire.
Generate receipts with html-pdf.com
Here is what you get with the hosted API. html-pdf.com renders HTML or a URL to PDF with Chromium, with custom sizes such as 80mm wide.
Each receipt page costs one credit. You get 100 free credits once you confirm your email, then from $0.005 per page, and purchased credits never expire. Your HTML is not stored.
With the MCP server, an AI agent can make one on request:
"Create an 80 mm receipt PDF for Corner Coffee: 2 lattes at $4.50 and 1 croissant at $3.20, paid by card, today's date."
Get an API key · Docs · Invoice guide
FAQ
How do I generate a receipt PDF from HTML?
Fill an HTML receipt template with your order data, escaping every value, and send it to POST https://html-pdf.com/v1/pdf as {"html":"..."}. The response is the PDF file, ready to email or print.
What page size should a thermal receipt PDF use?
Set width to your roll width, usually 80mm (sometimes 58mm), and margin to 0 or a few mm. Pass a height based on the number of lines, for example 70 mm plus 6 mm per item.
Can I send a PDF receipt automatically after a Stripe payment?
Yes. Listen for the checkout.session.completed webhook, build the receipt HTML from the session's line items, render it with the API, and attach the PDF to an email.
How much does a receipt PDF API cost?
With html-pdf.com, 1 credit = 1 PDF page, and you get 100 free credits once with no card. Packs run from $10 for 1,000 pages to $250 for 50,000, purchased credits never expire, and failed renders cost nothing.
