API documentation
Standard HTTP, PDF bytes out. Authenticate with Authorization: Bearer pm_…
Turn a Stripe event into a PDF
POST the webhook payload you already have.
Generate invoices from your JSON
One schema, five templates, any locale.
Add scan-to-pay QR codes
payment.url makes the PDF the checkout path.
Quickstart: PDF in 60 seconds
- 1. Get a key at /pricing, or skip this step and use the free watermarked
/api/previewendpoint while you build. - 2. Send an invoice. Save the complete example below as
invoice.json, then:curl -X POST https://papermint-phi.vercel.app/api/v1/invoice \ -H "Authorization: Bearer $PAPERMINT_KEY" \ -H "Content-Type: application/json" \ -d @invoice.json --output invoice.pdf
- 3. Open invoice.pdf. That is the whole integration. From here: add /from-stripe to your webhook, or grab the copy-paste SDK.
POST /api/v1/invoice
Renders invoice JSON to PDF. Response is the PDF file; check X-Papermint-Remaining for your quota.
curl -X POST https://papermint-phi.vercel.app/api/v1/invoice \ -H "Authorization: Bearer $PAPERMINT_KEY" \ -H "Content-Type: application/json" \ -d @invoice.json --output invoice.pdf
Invoice JSON fields
| Field | Type | Notes |
|---|---|---|
| template | string | "clean" (default), "classic", "bold", "minimal", or "receipt" |
| pageSize | string | "a4" (default) or "letter"; ignored by receipt |
| locale | string | BCP 47 tag for number/date formatting, e.g. "de-DE". Default "en-US" |
| currency | string | ISO 4217 code, e.g. "EUR". Default "USD". Two- and zero-decimal currencies; three-decimal (KWD, BHD, ...) not yet supported |
| documentTitle | string | Heading word on the document: "Invoice" (default), "Quote", "Receipt", "Estimate"... |
| filename | string | Sets the Content-Disposition filename on the PDF response |
| invoice.number | string | Required. Your invoice identifier |
| customFields | array | Up to 4 { name, value } pairs rendered with the invoice metadata (vendor number, cost center...) |
| invoice.date / dueDate | string | ISO date recommended; other strings pass through verbatim |
| seller / buyer | object | name (required), addressLines[], email, phone, vatId, website |
| items[] | array | Required, 1 to 300. description, quantity, unitPrice, taxRate (%), discount |
| discount / shipping | object | Order-level { label, amount } |
| amountPaid | number | Payments already received; renders Amount due |
| totals | object | Optional explicit overrides: subtotal, taxLines[], total, amountPaid, amountDue. Use when your source system already computed totals |
| payment | object | { url, label, qr } renders a scannable QR code and a clickable payment link on the PDF |
| reverseCharge | boolean | Prints the standard EU reverse-charge note |
| notes / terms / footerNote | string | Free text sections |
| branding.accentColor | string | #rrggbb hex |
| branding.logo | string | Base64 or data-URI PNG/JPEG; branding.logoWidth 20 to 220 pt |
Complete example
{
"template": "clean",
"pageSize": "a4",
"locale": "en-US",
"currency": "USD",
"invoice": {
"number": "INV-2026-0042",
"date": "2026-07-01",
"dueDate": "2026-07-15",
"purchaseOrder": "PO-8841"
},
"seller": {
"name": "Acme Studio LLC",
"addressLines": [
"100 Market Street",
"San Francisco, CA 94105",
"United States"
],
"email": "billing@acmestudio.com",
"website": "acmestudio.com",
"vatId": "US-EIN 88-1234567"
},
"buyer": {
"name": "Nordwind GmbH",
"addressLines": [
"Torstraße 145",
"10119 Berlin",
"Germany"
],
"email": "ap@nordwind.de",
"vatId": "DE 812345678"
},
"items": [
{
"description": "Design sprint: checkout flow redesign (2 weeks)",
"quantity": 1,
"unitPrice": 4800,
"taxRate": 19
},
{
"description": "Frontend implementation, Next.js + Stripe integration",
"quantity": 32,
"unitPrice": 120,
"taxRate": 19
},
{
"description": "Production support retainer, July",
"quantity": 1,
"unitPrice": 950,
"taxRate": 19,
"discount": 150
}
],
"amountPaid": 2000,
"notes": "Thank you for your business. Payment via bank transfer to the account listed in the contract.",
"terms": "Payment due within 14 days. Late payments accrue 1.5% monthly interest.",
"footerNote": "Acme Studio LLC · Registered in Delaware · billing@acmestudio.com",
"branding": {
"accentColor": "#10b981"
}
}POST /api/v1/from-stripe
Send the raw Stripe payload you already have (an invoice, payment_intent, or checkout.session object with its object field intact). Papermint maps line items, taxes, totals, and the customer for you. You supply seller (Stripe payloads do not include your company details) and optional overrides using any field from the invoice schema.
{
"stripe_object": { "object": "invoice", "...": "raw Stripe invoice JSON" },
"seller": {
"name": "Acme Studio LLC",
"addressLines": ["100 Market St", "San Francisco, CA 94105"],
"vatId": "US-EIN 88-1234567"
},
"overrides": {
"template": "classic",
"branding": { "accentColor": "#10b981" }
}
}Stripe amounts are mapped from smallest-unit integers, including zero-decimal currencies like JPY. Mapped totals are used verbatim so the PDF matches Stripe to the cent.
POST /api/v1/validate
Checks invoice JSON against the schema without rendering and returns { valid, issues[] }. Free, no auth. Wire it into CI so a schema regression in your integration never reaches production.
GET /api/v1/templates
Lists available templates. No auth required.
GET /api/health
Liveness endpoint for uptime monitors: status, version, and whether payments are configured.
TypeScript SDK, the copy-paste way
No package to install, no dependency to audit, no version to chase. The client is one typed, zero-dependency file: copy it into your project and own it. Works in Node 18+, browsers, edge runtimes, Deno, and Bun.
curl -O https://papermint-phi.vercel.app/papermint.ts
import { createPapermint } from "./papermint";
const pm = createPapermint({ apiKey: process.env.PAPERMINT_KEY! });
const pdf = await pm.invoice(invoiceJson); // Uint8Array
const check = await pm.validate(invoiceJson); // { valid, issues } - free, great in CI
const fromStripe = await pm.fromStripe({
stripe_object: event.data.object,
seller: { name: "Acme Studio LLC" },
});Machine-readable spec
An OpenAPI 3.1 description of every endpoint lives at /openapi.json, and a plain-text summary for AI coding agents at /llms.txt.
POST /api/preview
The free playground endpoint. Same schema as /api/v1/invoice, no auth, watermarked output, rate-limited per IP. Watermarked test renders are always free, never need a credit card, and never count against any plan: build the whole integration before paying.
Errors
| Status | Meaning |
|---|---|
| 400 | Body is not valid JSON |
| 401 | Missing, malformed, or unknown API key |
| 402 | No active subscription for this key |
| 413 | Body larger than 3 MB |
| 422 | Validation failed; response lists every issue with its path |
| 429 | Monthly quota reached (API) or playground rate limit (preview) |
| 500 | Rendering failed |
Limits and guarantees
- · Request bodies up to 3 MB (enough for a large embedded logo)
- · Up to 300 line items per invoice; long descriptions wrap and paginate
- · Rendering is stateless; invoice data is never stored
- · Same input, same bytes: rendering is fully deterministic