papermint

API documentation

Standard HTTP, PDF bytes out. Authenticate with Authorization: Bearer pm_…

Quickstart: PDF in 60 seconds

  1. 1. Get a key at /pricing, or skip this step and use the free watermarked /api/preview endpoint while you build.
  2. 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. 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

FieldTypeNotes
templatestring"clean" (default), "classic", "bold", "minimal", or "receipt"
pageSizestring"a4" (default) or "letter"; ignored by receipt
localestringBCP 47 tag for number/date formatting, e.g. "de-DE". Default "en-US"
currencystringISO 4217 code, e.g. "EUR". Default "USD". Two- and zero-decimal currencies; three-decimal (KWD, BHD, ...) not yet supported
documentTitlestringHeading word on the document: "Invoice" (default), "Quote", "Receipt", "Estimate"...
filenamestringSets the Content-Disposition filename on the PDF response
invoice.numberstringRequired. Your invoice identifier
customFieldsarrayUp to 4 { name, value } pairs rendered with the invoice metadata (vendor number, cost center...)
invoice.date / dueDatestringISO date recommended; other strings pass through verbatim
seller / buyerobjectname (required), addressLines[], email, phone, vatId, website
items[]arrayRequired, 1 to 300. description, quantity, unitPrice, taxRate (%), discount
discount / shippingobjectOrder-level { label, amount }
amountPaidnumberPayments already received; renders Amount due
totalsobjectOptional explicit overrides: subtotal, taxLines[], total, amountPaid, amountDue. Use when your source system already computed totals
paymentobject{ url, label, qr } renders a scannable QR code and a clickable payment link on the PDF
reverseChargebooleanPrints the standard EU reverse-charge note
notes / terms / footerNotestringFree text sections
branding.accentColorstring#rrggbb hex
branding.logostringBase64 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

StatusMeaning
400Body is not valid JSON
401Missing, malformed, or unknown API key
402No active subscription for this key
413Body larger than 3 MB
422Validation failed; response lists every issue with its path
429Monthly quota reached (API) or playground rate limit (preview)
500Rendering failed

Limits and guarantees