Use case
Webhook PDF API
Generate PDFs automatically when an event fires: a payment succeeds, a form is submitted, a contract is signed. One POST request from your webhook handler, a stored document and a URL back.
Stripe payment → invoice PDF
The most common pattern: listen for payment_intent.succeeded, render the invoice HTML, post to the API, store the URL in your database, and email it to the customer. The whole flow completes inside the webhook timeout.
// Express webhook handler: generate an invoice when payment succeeds
import express from "express";
import Stripe from "stripe";
import Mustache from "mustache";
import { readFileSync } from "fs";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const template = readFileSync("./templates/invoice.html", "utf-8");
app.post("/webhooks/stripe", express.raw({ type: "application/json" }), async (req, res) => {
const sig = req.headers["stripe-signature"]!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
} catch {
return res.status(400).send("Webhook signature verification failed.");
}
if (event.type === "payment_intent.succeeded") {
const intent = event.data.object as Stripe.PaymentIntent;
const order = await db.orders.findByPaymentIntent(intent.id);
const html = Mustache.render(template, {
invoiceNumber: order.invoiceNumber,
customerName: order.customer.name,
customerEmail: order.customer.email,
lineItems: order.lineItems,
subtotal: order.subtotal,
tax: order.tax,
total: order.total,
issueDate: new Date().toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }),
});
const pdfRes = await fetch("https://api.pdfpipe.xyz/v1/pdf", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.PDFPIPE_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ html, store: true, options: { format: "A4" } }),
});
const { document_url, document_id } = await pdfRes.json();
await db.invoices.create({
orderId: order.id,
pdfId: document_id,
pdfUrl: document_url,
stripePaymentId: intent.id,
});
await mailer.send({
to: order.customer.email,
subject: `Your invoice #${order.invoiceNumber}`,
html: `<p>Thanks for your order! <a href="${document_url}">Download your invoice</a>.</p>`,
});
}
res.json({ received: true });
});No-code: Zapier, Make, or n8n
The API is just an HTTP POST. Any automation tool that can make HTTP requests can trigger a PDF generation: Zapier, Make (Integromat), n8n, or a simple cURL in a shell script.
// No-code: Zapier / Make / n8n
// Trigger: "Stripe payment succeeded" (or any form submission, CRM event, etc.)
// Action: HTTP POST to https://api.pdfpipe.xyz/v1/pdf
//
// Headers:
// Authorization: Bearer pp_live_...
// Content-Type: application/json
//
// Body (JSON):
// {
// "html": "<!DOCTYPE html>...", <- or build it with a Text step
// "store": true,
// "options": { "format": "A4" }
// }
//
// Response: { "document_url": "https://...", "document_id": "...", ... }
// Use document_url in the next step to email, Slack, or save to Drive.Scheduled generation with GitHub Actions
For monthly reports or recurring documents, a cron schedule in GitHub Actions is often the simplest infrastructure. No server to maintain, no queue to run.
# .github/workflows/monthly-report.yml
# Runs on the 1st of every month to generate reports for all accounts
name: Monthly report generation
on:
schedule:
- cron: "0 6 1 * *" # 06:00 UTC on the 1st
jobs:
generate:
runs-on: ubuntu-latest
env:
PDFPIPE_KEY: ${{ secrets.PDFPIPE_KEY }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: "20" }
- run: npm ci
- run: node scripts/generate-monthly-reports.jsForm submission → contract PDF (Python)
Generate a contract the moment a form is submitted. Verify the webhook, render the template, store the document, email the link.
# FastAPI webhook handler
from fastapi import FastAPI, Header, Request, HTTPException
import httpx, os, hashlib, hmac
app = FastAPI()
PDFPIPE_KEY = os.environ["PDFPIPE_KEY"]
@app.post("/webhooks/form-submission")
async def on_form_submission(
request: Request,
x_webhook_secret: str = Header(None),
):
# Verify the webhook sender
if not hmac.compare_digest(x_webhook_secret or "", os.environ["FORM_WEBHOOK_SECRET"]):
raise HTTPException(status_code=401, detail="Invalid webhook secret")
body = await request.json()
html = render_contract_template(body) # fill your template
async with httpx.AsyncClient(timeout=60) as client:
res = await client.post(
"https://api.pdfpipe.xyz/v1/pdf",
headers={"Authorization": f"Bearer {PDFPIPE_KEY}"},
json={"html": html, "store": True, "options": {"format": "Letter"}},
)
res.raise_for_status()
data = res.json()
await db.save_document(
form_id=body["id"],
pdf_id=data["document_id"],
pdf_url=data["document_url"],
)
await send_email(body["email"], data["document_url"])
return {"ok": True}What you get back
When you pass store: true, the API returns JSON:
document_id: unique ID, also the storage keydocument_url: time-limited public URL, ready to email or linkdocument_expires: epoch ms when the URL and file expiresize_bytes: PDF sizeused / limit: current quota usage
Plans
| Plan | Documents/mo | Storage | Price |
|---|---|---|---|
| Hobby | 500 | 1 day | Free |
| Starter | 3,000 | 30 days | $19/mo |
| Growth | 15,000 | 365 days | $49/mo |
| Scale | 50,000 | 2 years | $149/mo |
| Business | 100,000 | 2 years | $499/mo |