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.

For AI agents

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:

EndpointPurposeSpeedRate limit
POST /api/v1/templatesCreate a reusable template (AI)~20s30/hour
GET /api/v1/templatesList your templatesfast
GET /api/v1/templates?id=...Get a single templatefast
POST /api/v1/renderRender PDF from templatesub-second100/hour
POST /api/v1/renderOne-shot generate + render~20s50/hour
POST /api/v1/mdMarkdown to PDFsub-second200/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:

  1. Bearer token — pass your API key in the Authorization header:
Authorization: Bearer your-api-key
  1. 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.

  1. 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.
  2. Open the browser — direct the user to verification_uri_complete (code pre-filled) or display the user_code for manual entry.
  3. Poll for the token POST /api/v1/device/token with device_code every 5 seconds.
  4. Handle responses authorization_pending (keep polling), slow_down (increase interval by 5s), access_denied (abort), expired_token (restart).
  5. 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

POST/api/v1/templates

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.

Request body
FieldTypeDescription
type requiredstringDocument type: "invoice", "receipt", "quote", "cv", "resume", "statement", "certificate", or "letter".
data requiredobjectThe structured data for your document. The AI designs the template around this shape. Max 50KB.
style optionalstringStyle hint: "modern", "classic", "minimal", "bold", or a freeform description. Max 200 chars.
brand optionalobjectBrand customisation with optional logoUrl, primaryColor, secondaryColor, and fontPreference.
Example request
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 } }
Example response
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" }
Tip: The dataShape field describes the structure of your data. Templates work best when re-used with data of the same shape — same keys, same array structure.
GET/api/v1/templates

List your saved templates, newest first. Only returns templates owned by the authenticated caller.

Query parameters
FieldTypeDescription
id optionalstring (UUID)If provided, returns a single template by ID (including the full content/definition).
type optionalstringFilter by document type: "invoice", "receipt", "quote", "cv", "statement", "certificate", or "letter".
limit optionalnumberMax results to return. Default 20, max 100.
offset optionalnumberPagination offset. Default 0.
Example: list templates
GET /api/v1/templates?type=invoice&limit=5 Authorization: Bearer your-api-key
200 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 }
Example: get single template
GET /api/v1/templates?id=a1b2c3d4-e5f6-7890-abcd-ef1234567890 Authorization: Bearer your-api-key

Returns the full template including the content field (the DocumentDefinition JSON). The list endpoint omits content to keep responses compact.

POST/api/v1/render

Render a PDF. Supports two modes depending on the request body:

Mode 1: Render from template (fast)

Pass a templateId from a previously created template, plus fresh data. No AI call — sub-second wall-clock renders.

FieldTypeDescription
templateId requiredstring (UUID)The ID of a template created via POST /api/v1/templates.
data requiredobjectFresh data to populate the template. Should match the same shape as the original. Max 50KB.
Example request
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 } }
Mode 2: One-shot render (slow)

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.

FieldTypeDescription
type requiredstringDocument type: "invoice", "receipt", "quote", "cv", "resume", "statement", "certificate", or "letter".
data requiredobjectThe structured data for your document. Max 50KB.
style optionalstringStyle hint. Max 200 chars.
brand optionalobjectBrand customisation (logoUrl, primaryColor, secondaryColor, fontPreference).
Example request
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" } }
Response

By default, the response is the PDF binary with Content-Type: application/pdf. Metadata is returned in response headers:

FieldTypeDescription
X-Pages requiredheaderNumber of pages in the PDF.
X-Render-Ms requiredheaderTotal render time in milliseconds.
X-Model optionalheaderAI model used (only present for one-shot mode).
X-Rate-Limit-Remaining requiredheaderRemaining requests in the current rate limit window.
JSON metadata: Send Accept: application/json to receive render metadata as JSON instead of the PDF binary. Useful for checking page count and timing without downloading the file.
POST/api/v1/md

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.

Request body
FieldTypeDescription
markdown requiredstringGitHub Flavored Markdown content. Max 200KB.
options.pageSize optionalstring"A3", "A4" (default), "A5", "Letter", or "Legal".
options.fontFamily optionalstring"Inter" (default) or "NotoSans".
options.fontSize optionalnumberBase font size in points (6–24, default 10).
options.margins optionalarray[top, right, bottom, left] in points. Default [40, 40, 40, 40].
options.title optionalstringPDF document title. Max 200 chars.
options.splitMode optionalstring"default" keeps headings with their content; "balanced" splits large sections to fill pages.
options.theme optionalstring"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.
Example request
POST /api/v1/md Content-Type: application/json Authorization: Bearer your-api-key { "markdown": "# Hello\n\nSome **bold** text.", "options": { "pageSize": "Letter", "fontFamily": "NotoSans" } }
Anonymous example (when promo is on)
POST /api/v1/md Content-Type: application/json { "markdown": "# Hello\n\nSome **bold** text." }
POST/api/v1/enhance

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.

Request body
FieldTypeDescription
pdfBase64 requiredstringThe PDF to remediate, base64-encoded. Max 25MB decoded.
options.mode optionalstring"in-place" (default and only supported value in v1). "regenerate" / "both" 400 with a pointer to the v1 restriction.
options.title optionalstringOverride the PDF /Title metadata. Max 200 chars.
options.altTextOverrides optionalarrayPer-figure alt-text overrides keyed by (page, mcid). Max 500 entries. Lets you correct vision output without re-paying for vision.
Optional headers
FieldTypeDescription
Idempotency-Key optionalstringOpaque caller-supplied dedup key. Re-submitting the same key returns the existing jobId without enqueuing a duplicate. Max 200 chars.
202 response
{ "jobId": "f8e1…", "status": "queued", "statusUrl": "/api/v1/enhance/jobs/f8e1…", "estimatedSeconds": 240 }
Polling recipe

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
Status response
{ "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.

Related endpoints

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).

x402-paid wallet flow

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

TypeDescriptionTypical data
invoiceBusiness invoicesLine items, totals, payment details, addresses
receiptTransaction confirmationsItems, totals, payment method, store info
quoteQuotes and estimatesItemised pricing, validity, terms
cv / resumeCVs and resumesExperience, education, skills, contact info
statementAccount statementsTransactions, balances, date ranges
certificateCertificatesRecipient, issuer, date, details
letterFormal lettersSender, 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.

BuilderPDF fieldRequired options
input(opts)/Tx text fieldname, width, height. Optional: value, tooltip, maxLength, multiline.
checkbox(opts)/Btn checkboxname, size. Optional: checked, tooltip, label (renders box + text in a row).
select(opts)/Ch combo dropdownname, 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"] }
StatusMeaning
400Invalid request body. Check the details array for field-level errors.
404Template not found (wrong ID or not owned by your API key).
429Rate limit exceeded. Check the Retry-After header for seconds until reset.
500Internal error (AI failure, rendering error, etc.)
503Service unavailable — no AI providers configured.