API Reference
Generate beautiful, PDF/A-2A + PDF/UA-1 compliant documents from structured data. Designed for AI agents and API clients — no templates to learn, no design decisions to make.
Before hand-writing requests, read the canonical skill file — it teaches the compact DSL used by /api/v1/preview and /api/v1/render, plus document recipes (invoice, receipt, CV, report). Also useful: /llms.txt (machine-readable site manifest) and /ai (per-tool setup for Claude Code, Cursor, ChatGPT, etc.).
Choosing an endpoint
Four ways to get from data to a PDF. Pick by what you have, not by what sounds most powerful:
What do you have?
│
├── Markdown content, want a PDF with default styling?
│ → POST /api/v1/md (sub-second, no AI)
│
├── Structured data for a common doc type
│ (invoice, receipt, quote, cv, statement, certificate, letter)?
│ → POST /api/v1/render { type, data } (~20s, AI auto-generates template)
│ Fastest path to a working PDF — no DSL to write.
│
└── Custom layout, brand-specific, or data doesn't fit a standard type?
→ POST /api/v1/preview { dsl, data } (sub-second, deterministic)
Full control. Write the DSL taught in the skill file.Rule of thumb: /md for docs, /render for quick starts on standard types, /preview when you want pixel control. Reuse a generated template via POST /api/v1/templates + /render with templateId if you need repeatable, fast renders with consistent design.
Overview
The API is split into two stages — one creative, one mechanical:
| Endpoint | Purpose | Speed | Rate limit |
|---|---|---|---|
| POST /api/v1/templates | Create a reusable template (AI) | ~20s | 30/hour |
| GET /api/v1/templates | List your templates | fast | — |
| GET /api/v1/templates?id=... | Get a single template | fast | — |
| POST /api/v1/render | Render PDF from template | sub-second | 100/hour |
| POST /api/v1/render | One-shot generate + render | ~20s | 50/hour |
| POST /api/v1/md | Markdown to PDF | sub-second | 200/hour |
Recommended workflow: Create a template once, then render repeatedly with different data. This gives you consistent design, fast renders, and the ability to generate alternatives and pick the one you like.
Authentication
Endpoints require authentication by default. Two methods are supported:
- Bearer token — pass your API key in the Authorization header:
Authorization: Bearer your-api-key- Session cookie — if you're logged in to the web app, your session cookie is accepted automatically.
Unauthenticated requests normally receive a 401 response. Manage your API keys →
Carve-outs. POST /api/v1/pdf/validate (the hosted veraPDF utility — JSON-only, no PDF output) and POST /api/v1/md (Markdown → PDF, when the anonymous-renders promo is on) accept unauthenticated requests with stricter per-IP rate limits. A stale or invalid Bearer token still returns 401 on every endpoint — we never silently downgrade an authentication attempt.
Device Authorization (for AI agents & CLIs)
If your agent or CLI needs to authenticate without pre-configured API keys, use the OAuth Device Authorization Grant flow. The agent opens the user's browser, the user approves, and the agent receives a persistent API key.
- Request a device code — POST /api/v1/device/code with an optional client_name. Returns device_code, user_code, verification_uri_complete, expires_in, and interval.
- Open the browser — direct the user to verification_uri_complete (code pre-filled) or display the user_code for manual entry.
- Poll for the token — POST /api/v1/device/token with device_code every 5 seconds.
- Handle responses — authorization_pending (keep polling), slow_down (increase interval by 5s), access_denied (abort), expired_token (restart).
- On success — the response includes an access_token. Store it and use as Authorization: Bearer <token> for all subsequent API calls. The token is a persistent API key.
# 1. Request device code
curl -X POST https://makespdf.com/api/v1/device/code \
-H "Content-Type: application/json" \
-d '{"client_name": "My AI Agent"}'
# 2. Open the verification_uri_complete in the user's browser
# 3. Poll for the token
curl -X POST https://makespdf.com/api/v1/device/token \
-H "Content-Type: application/json" \
-d '{"device_code": "<device_code from step 1>"}'Endpoints
Generate a reusable PDF template via AI. The template is a design tailored to your data shape, with {{variable}} placeholders that can be filled with different data on each render. Call multiple times with the same input to get alternative designs.
| Field | Type | Description |
|---|---|---|
| type required | string | Document type: "invoice", "receipt", "quote", "cv", "resume", "statement", "certificate", or "letter". |
| data required | object | The structured data for your document. The AI designs the template around this shape. Max 50KB. |
| style optional | string | Style hint: "modern", "classic", "minimal", "bold", or a freeform description. Max 200 chars. |
| brand optional | object | Brand customisation with optional logoUrl, primaryColor, secondaryColor, and fontPreference. |
POST /api/v1/templates
Content-Type: application/json
Authorization: Bearer your-api-key
{
"type": "invoice",
"style": "modern",
"brand": {
"primaryColor": "#2563eb",
"logoUrl": "https://example.com/logo.png"
},
"data": {
"company": "Acme Corp",
"invoiceNumber": "INV-001",
"date": "2025-03-15",
"items": [
{ "description": "Web design", "quantity": 1, "unitPrice": 2500 },
{ "description": "Hosting (annual)", "quantity": 1, "unitPrice": 300 }
],
"subtotal": 2800,
"tax": 280,
"total": 3080
}
}201 Created
{
"templateId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"type": "invoice",
"style": "modern",
"model": "claude",
"version": 1,
"dataShape": "company,date,invoiceNumber,items[description,quantity,unitPrice],subtotal,tax,total",
"generationMs": 18420,
"costUsd": 0.032,
"createdAt": "2025-03-15T10:30:00.000Z"
}List your saved templates, newest first. Only returns templates owned by the authenticated caller.
| Field | Type | Description |
|---|---|---|
| id optional | string (UUID) | If provided, returns a single template by ID (including the full content/definition). |
| type optional | string | Filter by document type: "invoice", "receipt", "quote", "cv", "statement", "certificate", or "letter". |
| limit optional | number | Max results to return. Default 20, max 100. |
| offset optional | number | Pagination offset. Default 0. |
GET /api/v1/templates?type=invoice&limit=5
Authorization: Bearer your-api-key200 OK
{
"templates": [
{
"templateId": "a1b2c3d4-...",
"type": "invoice",
"style": "modern",
"dataShape": "company,date,items[...],total",
"model": "claude",
"version": 1,
"createdAt": "2025-03-15T10:30:00"
}
],
"total": 1,
"limit": 5,
"offset": 0
}GET /api/v1/templates?id=a1b2c3d4-e5f6-7890-abcd-ef1234567890
Authorization: Bearer your-api-keyReturns the full template including the content field (the DocumentDefinition JSON). The list endpoint omits content to keep responses compact.
Render a PDF. Supports two modes depending on the request body:
Pass a templateId from a previously created template, plus fresh data. No AI call — sub-second wall-clock renders.
| Field | Type | Description |
|---|---|---|
| templateId required | string (UUID) | The ID of a template created via POST /api/v1/templates. |
| data required | object | Fresh data to populate the template. Should match the same shape as the original. Max 50KB. |
POST /api/v1/render
Content-Type: application/json
Authorization: Bearer your-api-key
{
"templateId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"data": {
"company": "Different Corp",
"invoiceNumber": "INV-002",
"date": "2025-03-20",
"items": [
{ "description": "Consulting", "quantity": 10, "unitPrice": 150 }
],
"subtotal": 1500,
"tax": 150,
"total": 1650
}
}Pass a type and data to generate a template and render in a single request. Convenient but slower (~20s) and the template is not saved for reuse.
| Field | Type | Description |
|---|---|---|
| type required | string | Document type: "invoice", "receipt", "quote", "cv", "resume", "statement", "certificate", or "letter". |
| data required | object | The structured data for your document. Max 50KB. |
| style optional | string | Style hint. Max 200 chars. |
| brand optional | object | Brand customisation (logoUrl, primaryColor, secondaryColor, fontPreference). |
POST /api/v1/render
Content-Type: application/json
Authorization: Bearer your-api-key
{
"type": "receipt",
"data": {
"store": "Corner Shop",
"date": "2025-03-15",
"items": [
{ "name": "Coffee", "quantity": 2, "price": 4.50 },
{ "name": "Croissant", "quantity": 1, "price": 3.00 }
],
"total": 12.00,
"paymentMethod": "Card ending 4242"
}
}By default, the response is the PDF binary with Content-Type: application/pdf. Metadata is returned in response headers:
| Field | Type | Description |
|---|---|---|
| X-Pages required | header | Number of pages in the PDF. |
| X-Render-Ms required | header | Total render time in milliseconds. |
| X-Model optional | header | AI model used (only present for one-shot mode). |
| X-Rate-Limit-Remaining required | header | Remaining requests in the current rate limit window. |
Convert GitHub Flavored Markdown to PDF. No AI call — pure computation, typically under 200ms.
Anonymous usage. When the promo is on, you can call this endpoint without an Authorization header. Anonymous limits: 60 requests/hour and 200/day per IP, and 20 pages per render. Anonymous responses carry an X-MakesPDF-Tip header nudging sign-up; rate-limit and page-cap rejections include the same nudge in a tip field. Sign up for higher limits, saved renders, and access to the rest of the API.
| Field | Type | Description |
|---|---|---|
| markdown required | string | GitHub Flavored Markdown content. Max 200KB. |
| options.pageSize optional | string | "A3", "A4" (default), "A5", "Letter", or "Legal". |
| options.fontFamily optional | string | "Inter" (default) or "NotoSans". |
| options.fontSize optional | number | Base font size in points (6–24, default 10). |
| options.margins optional | array | [top, right, bottom, left] in points. Default [40, 40, 40, 40]. |
| options.title optional | string | PDF document title. Max 200 chars. |
| options.splitMode optional | string | "default" keeps headings with their content; "balanced" splits large sections to fill pages. |
| options.theme optional | string | "default" (current style) or "github" — GitHub markdown styling with lighter semibold headings, h1/h2 underlines, and GitHub colours. Matches the VS Code Markdown PDF extension. |
POST /api/v1/md
Content-Type: application/json
Authorization: Bearer your-api-key
{
"markdown": "# Hello\n\nSome **bold** text.",
"options": {
"pageSize": "Letter",
"fontFamily": "NotoSans"
}
}POST /api/v1/md
Content-Type: application/json
{
"markdown": "# Hello\n\nSome **bold** text."
}Remediate an existing PDF (alt text, structure tags, accessibility metadata). Async — submitting returns 202 with a jobId in under a second; clients poll GET /api/v1/enhance/jobs/:id until status is terminal. We also email you when the job is ready and surface the PDF on /settings/renders.
| Field | Type | Description |
|---|---|---|
| pdfBase64 required | string | The PDF to remediate, base64-encoded. Max 25MB decoded. |
| options.mode optional | string | "in-place" (default and only supported value in v1). "regenerate" / "both" 400 with a pointer to the v1 restriction. |
| options.title optional | string | Override the PDF /Title metadata. Max 200 chars. |
| options.altTextOverrides optional | array | Per-figure alt-text overrides keyed by (page, mcid). Max 500 entries. Lets you correct vision output without re-paying for vision. |
| Field | Type | Description |
|---|---|---|
| Idempotency-Key optional | string | Opaque caller-supplied dedup key. Re-submitting the same key returns the existing jobId without enqueuing a duplicate. Max 200 chars. |
{
"jobId": "f8e1…",
"status": "queued",
"statusUrl": "/api/v1/enhance/jobs/f8e1…",
"estimatedSeconds": 240
}Start at a 2-second cadence and back off to 8 seconds after the first 30 seconds. Give up after 30 minutes. The status response carries phase and a progress: { current, total } object you can render as a real progress bar.
# 1. Submit
curl -s -X POST https://makespdf.com/api/v1/enhance \
-H "Authorization: Bearer $MAKESPDF_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: my-unique-key-001" \
-d "{\"pdfBase64\":\"$(base64 < input.pdf)\"}"
# → { "jobId": "f8e1…", "status": "queued",
# "statusUrl": "/api/v1/enhance/jobs/f8e1…", "estimatedSeconds": 240 }
# 2. Poll until terminal
while :; do
resp=$(curl -s -H "Authorization: Bearer $MAKESPDF_API_KEY" \
"https://makespdf.com/api/v1/enhance/jobs/f8e1…")
status=$(echo "$resp" | jq -r .status)
echo "$resp" | jq -c '{status, phase, progress}'
case "$status" in
succeeded|partial|failed|cancelled) break ;;
esac
sleep 2
done
# 3. Download on success
artifactId=$(echo "$resp" | jq -r .artifactId)
curl -s -H "Authorization: Bearer $MAKESPDF_API_KEY" \
"https://makespdf.com/api/v1/artifacts/$artifactId/pdf" -o enhanced.pdf{
"jobId": "f8e1…",
"status": "processing",
"phase": "in-place",
"progress": { "current": 17, "total": 50 },
"mode": "in-place",
"pageCount": 50,
"inputFilename": "civic-cip.pdf",
"artifactId": null,
"downloadUrl": null,
"creditsCharged": null,
"errorMessage": null,
"attemptCount": 1,
"createdAt": "2026-05-06T10:00:00.000Z",
"startedAt": "2026-05-06T10:00:01.500Z",
"completedAt": null
}Terminal states: succeeded (PDF ready, billed), partial (some pages failed, no charge), failed (no charge), cancelled. On succeeded, artifactId and downloadUrl are populated and a completion email is sent to your account address.
GET /api/v1/enhance/jobs?status=…&limit=…&offset=… lists your jobs newest-first (owner-scoped, paginated; authed callers only). POST /api/v1/enhance/jobs/:id/cancel flags a job for cancellation (best-effort — pages already in flight finish).
When you settle POST /api/v1/enhance with USDC via x402 (PAYMENT-SIGNATURE header) instead of a Bearer key, the 202 envelope additionally carries a pollToken — an opaque 32-byte bearer credential scoped to that one job's lifetime. Re-present it as Authorization: Bearer <pollToken> on the status, cancel, and artifact-download calls. Token mismatch returns 404 (same shape as authed owner-mismatch). Idempotent PAYMENT-SIGNATURE replays within 5 minutes return the cached envelope verbatim, including the original pollToken. The list endpoint GET /api/v1/enhance/jobs is not available to wallet callers — you know your jobIds because the producer returned them.
Data formatting
Dates in your data are automatically pre-formatted before rendering. Money values are formatted by the template's | currency filter:
- Dates: 2025-03-15 becomes "15 March 2025". 2025-03 becomes "March 2025".
- Money: {{amount | currency}} renders as $1,234.50 by default (USD). Override per call with {{amount | currency:AUD}} → A$1,234.50, or {{amount | currency:EUR:de-DE}} for locale-specific formatting. Set a doc-wide default via doc({ currency: "AUD" }, …). Accepts any ISO 4217 code (AUD, GBP, EUR, JPY, NZD, CAD, CHF, INR, …); unknown codes fall back to USD with a warning.
Send raw numeric values — the | currency filter handles symbols, thousands separators, and decimal places.
Supported document types
| Type | Description | Typical data |
|---|---|---|
| invoice | Business invoices | Line items, totals, payment details, addresses |
| receipt | Transaction confirmations | Items, totals, payment method, store info |
| quote | Quotes and estimates | Itemised pricing, validity, terms |
| cv / resume | CVs and resumes | Experience, education, skills, contact info |
| statement | Account statements | Transactions, balances, date ranges |
| certificate | Certificates | Recipient, issuer, date, details |
| letter | Formal letters | Sender, recipient, subject, body paragraphs |
Fillable forms
Templates can include real fillable AcroForm widgets that stay interactive in Adobe Reader, Apple Preview, and any modern browser PDF viewer. Combined with tagged: true on the document, the widgets land inside the structure tree and become accessible to screen readers — the combination most form-capable PDF APIs don't deliver.
Three DSL builders cover the common field types. name must be unique across the document, and a tooltip is the field's accessible name (/TU) and required for PDF/UA-1 conformance.
| Builder | PDF field | Required options |
|---|---|---|
| input(opts) | /Tx text field | name, width, height. Optional: value, tooltip, maxLength, multiline. |
| checkbox(opts) | /Btn checkbox | name, size. Optional: checked, tooltip, label (renders box + text in a row). |
| select(opts) | /Ch combo dropdown | name, width, height, options ([{ label, value }]). Optional: value, tooltip. |
Minimal example — a name field, a single-select contact preference, and a consent checkbox:
const template = doc(
{ size: "A4", title: "Consent", tagged: true },
page(
s("h1", "Data processing consent"),
s("label", "Full name"),
input({ name: "fullName", width: 400, height: 18, tooltip: "Full legal name", maxLength: 120 }),
gap(8),
s("label", "Preferred contact"),
select({
name: "contactMethod", width: 200, height: 20,
options: [
{ label: "Email", value: "email" },
{ label: "Phone", value: "phone" },
{ label: "Postal", value: "post" },
],
value: "email",
tooltip: "How we should reach you",
}),
gap(8),
checkbox({
name: "consentProcessing", size: 12,
tooltip: "Consent to data processing (required)",
label: "I consent to the processing of my personal data.",
}),
),
);
const sampleData = {};A full government-style consent form, including the rendered PDF used as a fixture in the per-release veraPDF gate, lives in the makesPDF repo at assets/samples/forms/. Out of scope for v1: /Sig digital-signature fields, radio-button groups (model with a select instead), and Acrobat-style JavaScript field calculations.
Errors
All errors return JSON with an error field:
{
"error": "Invalid request",
"details": ["type: Required"]
}| Status | Meaning |
|---|---|
| 400 | Invalid request body. Check the details array for field-level errors. |
| 404 | Template not found (wrong ID or not owned by your API key). |
| 429 | Rate limit exceeded. Check the Retry-After header for seconds until reset. |
| 500 | Internal error (AI failure, rendering error, etc.) |
| 503 | Service unavailable — no AI providers configured. |