PDFPipe

Use case

Receipt PDF API

Generate purchase receipts, subscription-renewal confirmations, and refund receipts from HTML with a single POST request. Built for high volume: one clean document for every transaction, no SDK and no browser to run.

Built for one receipt per transaction

Every charge, renewal, and refund can fire a single POST and get a PDF back. The endpoint is stateless, so you can fan out concurrent calls from a payment webhook without queuing or rate-limit gymnastics.

Save the template, send only data

Store your receipt design once and reference it by template_id. On each transaction you POST a compact data object instead of full HTML, which keeps payloads small and your render logic in one place.

Your brand, pixel for pixel

Send any HTML you already write for confirmation emails. The renderer runs a full browser, so CSS Grid, Flexbox, web fonts, and gradients render exactly as they do in preview. No proprietary template language to learn.

Refund on failure

If a render times out or fails, the document is not counted against your quota. Safe to retry inside an idempotent webhook handler without burning credits.

Store and email a link

Add store: true to get a stable download URL instead of bytes. Drop it into the order-confirmation or renewal email as a download link. Retention follows your plan (1 day free, 30 days Starter, 1 year Growth and up).

Refund and renewal receipts too

The same endpoint generates refund confirmations and subscription-renewal receipts. Swap the line items and total, change one heading, and the output matches the rest of your customer documents.

1. Write the HTML template

Any HTML template engine works. Here is a Handlebars template for a typical receipt: brand mark, a paid marker, the order number, who was billed, line items, and the total charged.

templates/receipt.html
<!-- templates/receipt.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <style>
    body { font-family: system-ui, sans-serif; color: #1a1a1a; margin: 0; padding: 40px; }
    .header { display: flex; justify-content: space-between; align-items: start; }
    .brand { font-size: 22px; font-weight: 700; }
    .paid { font-size: 12px; font-weight: 700; letter-spacing: 1px; color: #1a7f37; }
    .meta { text-align: right; font-size: 13px; color: #666; }
    .title { font-size: 26px; font-weight: 700; margin: 28px 0 4px; }
    .order { font-size: 13px; color: #888; }
    .ship-to { margin: 24px 0; font-size: 13px; }
    table { width: 100%; border-collapse: collapse; margin: 28px 0; }
    th { text-align: left; padding: 8px 12px; background: #f5f5f5; font-size: 12px; }
    td { padding: 10px 12px; border-bottom: 1px solid #eee; font-size: 13px; }
    .num { text-align: right; }
    .total-row td { font-weight: 700; border-top: 2px solid #1a1a1a; border-bottom: none; }
    .footer { font-size: 12px; color: #888; margin-top: 40px; }
  </style>
</head>
<body>
  <div class="header">
    <div class="brand">Acme Store</div>
    <div class="meta">
      <div class="paid">PAID</div>
      <div>{{date}}</div>
    </div>
  </div>
  <div class="title">Receipt</div>
  <div class="order">Order #{{orderNumber}}</div>
  <div class="ship-to">
    <div style="font-size:12px;color:#888;margin-bottom:4px">Billed to</div>
    <div>{{customer.name}}</div>
    <div>{{customer.email}}</div>
  </div>
  <table>
    <thead>
      <tr><th>Item</th><th class="num">Qty</th><th class="num">Unit price</th><th class="num">Amount</th></tr>
    </thead>
    <tbody>
      {{#each items}}
      <tr>
        <td>{{description}}</td>
        <td class="num">{{quantity}}</td>
        <td class="num">${{unitPrice}}</td>
        <td class="num">${{amount}}</td>
      </tr>
      {{/each}}
    </tbody>
    <tfoot>
      <tr class="total-row">
        <td colspan="3">Total charged</td>
        <td class="num">${{total}}</td>
      </tr>
    </tfoot>
  </table>
  <div class="footer">
    This receipt confirms a completed payment. Keep it for your records.
    Questions about this order? Reply to your confirmation email.
  </div>
</body>
</html>

2. Render and POST (Node.js)

Call this from the place that confirms a payment, typically a billing or payment webhook handler, so every successful charge produces a receipt.

receipt.ts
// Node.js: render the receipt and call the API on every successful charge

import Handlebars from "handlebars";
import { readFileSync } from "fs";

const tmpl = Handlebars.compile(readFileSync("./templates/receipt.html", "utf-8"));

export async function generateReceiptPdf(data: {
  orderNumber: string;
  date: string;
  customer: { name: string; email: string };
  items: { description: string; quantity: number; unitPrice: string; amount: string }[];
  total: string;
}): Promise<Uint8Array> {
  const html = tmpl(data);

  const resp = await fetch("https://api.pdfpipe.xyz/v1/pdf", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.PDFPIPE_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      html,
      options: {
        format: "A4",
        margin: { top: "16mm", bottom: "16mm", left: "16mm", right: "16mm" },
        print_background: true,
      },
    }),
  });

  if (!resp.ok) throw new Error(`Receipt generation failed: ${resp.status}`);
  return new Uint8Array(await resp.arrayBuffer());
}

// Call it from your payment webhook handler, e.g. on charge.succeeded

2. Render and POST (Python)

receipt.py
# Python: Jinja2 template + httpx, triggered from your billing event handler

import httpx
from jinja2 import Environment, FileSystemLoader

jinja = Environment(loader=FileSystemLoader("templates"))

def generate_receipt_pdf(data: dict) -> bytes:
    html = jinja.get_template("receipt.html").render(**data)

    resp = httpx.post(
        "https://api.pdfpipe.xyz/v1/pdf",
        headers={"Authorization": f"Bearer {settings.PDFPIPE_API_KEY}"},
        json={
            "html": html,
            "options": {
                "format": "A4",
                "margin": {"top": "16mm", "bottom": "16mm", "left": "16mm", "right": "16mm"},
                "print_background": True,
            },
        },
        timeout=60.0,
    )
    resp.raise_for_status()
    return resp.content

Save the template, send only the data

For high volume you do not want to ship the full HTML on every transaction. Store the receipt design once, then reference it with template_id and pass a compact data object on each call. Combine it with store: true to get a download URL back for the confirmation email.

Node.js
// Use a saved template: store the receipt design once, call with data every time.
// Ideal for high volume: you send only the order data, not the full HTML, on each charge.

const resp = await fetch("https://api.pdfpipe.xyz/v1/pdf", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.PDFPIPE_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    template_id: "tmpl_your_receipt_template_id",
    data: {
      orderNumber: "ORD-2026-8841",
      date: "June 13, 2026",
      customer: { name: "Priya Sharma", email: "priya@example.com" },
      items: [
        { description: "Pro Plan (monthly)", quantity: 1, unitPrice: "49.00", amount: "49.00" },
      ],
      total: "49.00",
    },
    store: true,
    filename: "receipt-ORD-2026-8841.pdf",
  }),
});

const { document_url } = await resp.json();
// Send document_url in the order-confirmation email

Pricing

PlanPriceReceipts / monthOverage
HobbyFree500Not available
Starter$193,000Not available
Growth$4915,000Not available
Scale$14950,000Not available
Business$499100,000Contact sales

Start generating receipts

500 receipts a month free. No credit card. Key issued instantly after signup.

Related guide

PDF in Node.js →

Related guide

PDF in Python →

Related guide

PDF in Django →