Docs
SnappyPDF turns a template plus JSON into a PDF. Design the document once in the editor, mark the parts that change as variables, publish it, then render it by API. Here’s the whole loop.
Overview
Design it
Drag blocks onto the page in the editor, or start from a ready-made starter.
Variable-ize it
Swap anything that changes per document for a {{ variable }}.
Publish it
Snapshot the draft into an immutable version. The API serves only published versions.
Render it
POST your JSON with an API key and get the finished PDF back on the same request.
Create a template
From Templates, create a new template (or duplicate a starter) and open the editor. The editor has three parts: a toolbox of blocks on the left, the page canvas in the middle, and a properties panel on the right for whatever block is selected.
Drag blocks onto the page and arrange them. Set the page size and orientation for the whole document, and optionally turn on a header and footer band that repeat on every page. Use the Preview to see the exact rendered output with sample data at any time — the preview and the final PDF come from the same renderer, so what you see is what you get.
When it’s ready, hit Publish. Publishing takes an immutable snapshot — a numbered version — and the render API only ever serves published versions. That means you can keep editing the draft without ever breaking a live integration. Publish again to cut a new version; pin a specific version from the API when you need reproducible output.
AI assistant
The editor has an AI assistant that builds and changes templates in conversation. Open it with the AI assistant button in the editor, or choose Create with AI on your templates list to start from a blank page.
Describe what you need, or give it a document you already use: paste a screenshot, drag in an image, or attach a PDF (up to 5 files per message, 10 MB each; PNG, JPEG, WebP, GIF or PDF). It builds the template, renders it, looks at the pages, and fixes what’s off before it replies. Then keep chatting to refine it: “make the header navy”, “add a VAT column”, “move the logo to the right”.
Anything that changes per document becomes a variable, and repeating rows become a table bound to a list. Values from your reference become the sample data, so the preview looks like the original.
Changes appear in the editor as they’re made. The editor is read-only while the assistant works, so your edits and its edits never collide.
Undo puts the template back to how it was before the assistant’s last turn, as long as nobody has edited it since.
All changes go to your draft. You publish from the editor when you’re happy, so your live integration is never affected.
What to expect when rebuilding a document
- Layout, structure, colours, tables and the data fields match closely.
- Typography is approximate: templates use one font family throughout, a text block has one style (bold or not), and line height isn’t adjustable. The assistant tells you what it couldn’t reproduce.
- Logos in a screenshot can’t be cut out. Attach the logo as its own image and the assistant will use it, or it will leave an image variable you fill in per document.
- Templates don’t calculate. Totals and other derived values come from your data.
The assistant uses Anthropic’s Claude. Your messages, attachments and the template are sent to Claude to do the work; attachments expire from Anthropic’s storage after 30 days. Using it needs permission to edit templates (organization owners and admins).
How layout works
SnappyPDF uses flow layout, not absolute positioning. Blocks stack top to bottom and flow onto new pages as content grows. There are no x/y coordinates — which is exactly what lets a document with ten line items and one with ten thousand both paginate correctly without you touching the design.
Dimensions like page size, margins and spacing are in millimetres; font sizes are in points — the same units you’d use in print.
Headings and paragraphs, with {{ variable }} tokens anywhere inside.
A fixed or variable image — upload one, or pass a URL or data URI at render time.
QR codes, Code 128, EAN-13, Data Matrix and PDF417, from fixed text or a variable.
One row per item in a list variable; the header repeats across pages.
Holds columns for side-by-side layout (e.g. logo left, address right).
A container. Can repeat over a list, or fill the page to centre content.
A horizontal rule between blocks.
Fixed vertical gap, in millimetres.
Forces everything after it onto a new page.
Repeating content
Two blocks turn a list variable into repeating output. A table renders one row per item, and its header row repeats automatically when the table spills onto the next page. A section set to repeat re-renders its whole contents once per item — useful for cards, grouped blocks or anything more elaborate than a table row. Inside either, you reference the current item’s fields directly (see Variables below).
Headers & footers
Header and footer bands are fixed-height (in mm) and render on every page. They take a restricted set of blocks — no tables or page breaks, since a fixed band can’t paginate. They’re the only place page counters work: drop {{ page }} and {{ pages }} to get “Page 1 of 4”.
Variables
A variable is any part of the document that changes between renders. Define your variables in the Variables panel — each has a name and a type. A template’s variables are its data contract: they’re exactly the fields your API payload provides.
Names use letters, numbers and underscores and must start with a letter. Reference a variable anywhere text is allowed with a {{ token }}. Values are HTML-escaped, and you can reach into nested data with dot paths like {{ client.name }}.
Tip: type “/” to insert a variable
In any content input, press / to pop up a menu of every variable on the template. Keep typing to filter it, use the arrow keys to move, and press enter (or click) to drop the token in at your cursor — no need to remember exact names or type the braces yourself.
stringAny text — names, addresses, notes.
numberAmounts, quantities, totals.
dateDates you interpolate into the document.
booleanTrue/false, for showing or hiding content.
imageA URL or data URI rendered as an image.
listAn array of items — each with its own typed fields — for tables and repeating sections.
A list variable holds an array of items, and you give each item its own typed fields (say description, qty, price). Bind a table or a repeating section to the list; inside it, refer to the current item’s fields — the item and its index are in scope for each iteration.
Every variable gets sample data so the editor preview shows a realistic document before you’ve wired up any real payload. The sample data also becomes the fallback the API renders when a request omits data.
API keys
The render API authenticates with an organization API key. Create one under API Keys in your organization settings (owners and admins only). The full key is shown once, at creation — we only ever store a hash of it, so copy it then and keep it somewhere safe. If you lose it, revoke it and make a new one.
Keys are prefixed spdf_. Send yours on every request as either header:
Authorization: Bearer spdf_your_key_here
# — or —
x-api-key: spdf_your_key_herePermissions
Each key has permissions, so it can do only what it’s for. New keys can render and read templates; turn on the others when a key needs them. A request a key isn’t allowed to make gets a 403 naming the missing permission.
renderRender published templates and markdown. On for new keys.
templates:readList templates and read their data contracts. On for new keys.
templates:writeCreate templates and change drafts. AI clients need this to design templates.
templates:publishPublish drafts as new versions the render API serves. Give it only to keys that should go live.
usage:readSee render counts, failures and remaining quota.
To change a key’s name or permissions, use the pencil next to it on the API Keys page. Changes apply to the key’s very next request, and the key itself stays the same, so there’s nothing to roll out.
Keep keys server-side. Anyone holding a key can do whatever its permissions allow, including spending your render allowance.
AI clients (MCP)
SnappyPDF runs a Model Context Protocol (MCP) server, so AI coding assistants such as Claude Code and Cursor can design and edit your templates, check the rendered pages, and report on usage, from wherever you already work. Share a screenshot with your assistant and ask it to rebuild the document as a Snappy template.
The server is at https://snappypdf.co/api/mcp and authenticates with an organization API key. Create one under API Keys with the permissions the assistant should have: usually Edit templates, plus Read usage for usage questions. Add Publish templates only if it should be able to make versions live.
Connect Claude Code
claude mcp add --transport http snappy-pdf https://snappypdf.co/api/mcp \
--header "Authorization: Bearer spdf_your_key_here"When you create a key with Edit templates, the API Keys page shows this command with your key already filled in.
Connect Cursor
Add the server to .cursor/mcp.json in your project (or ~/.cursor/mcp.json for every project):
{
"mcpServers": {
"snappy-pdf": {
"url": "https://snappypdf.co/api/mcp",
"headers": {
"Authorization": "Bearer spdf_your_key_here"
}
}
}
}Any MCP client that supports Streamable HTTP with a custom header works the same way. Claude.ai and Claude Desktop connectors need OAuth sign-in, which isn’t available yet.
Available tools
get_authoring_guideThe guide to Snappy’s template format: blocks, layout, variables, edit ops and examples. Assistants read it before building anything. Also available as the snappy://authoring-guide resource.
list_templatesYour organization’s templates, drafts included, with status and published version.
get_templateA template’s draft (or a published version) in full: blocks with their ids, variables, sample data and page settings, plus an outline and lint warnings.
create_templateCreates a new draft from a short template description format. Variables are worked out from the {{ tokens }} used.
update_templateChanges a draft with small edits addressed by block id (insert, update, move, remove, page and header/footer settings, sample data). All of a batch applies, or none of it.
preview_templateRenders the draft and returns the pages as images, exactly as the render API would produce them, so the assistant can check its work.
import_imageCopies an image from a public https link (PNG, JPEG or WebP, up to 5 MB) into Snappy for use in a template, such as your logo.
publish_templatePublishes the draft as a new version. Assistants are told to do this only when you ask.
get_usageRender counts, error rate and speed, failures by error code, per-template and daily breakdowns, and the quota left right now.
Good to know
- Every change goes to a draft. Nothing reaches your render API until a template is published.
- Previews count against your organization’s editor render allowance, never the API allowance your integration uses. Reading templates and usage is free.
- When a request isn’t allowed or something is wrong with it, the assistant gets a plain message saying why (for example a missing permission, or exactly which field of a template is invalid) and can usually fix it itself.
- Ask in plain words. Things that work well:
- “Build me an invoice template like this one [screenshot]”
- “In the delivery note, add a column for batch numbers and make the heading navy”
- “Preview the certificate and fix anything that overflows the page”
- “How many renders did the invoice template do this month, and why are some failing?”
Discovery API
Two read-only endpoints let an integration find your templates and learn their data contract at runtime, instead of you hard-coding ids and field names. They’re what makes it possible to build a “pick a template, map your fields” UI on top of SnappyPDF.
Both authenticate with the same API key as the render API, and neither counts against your render allowance — that budget exists to protect the PDF renderer, which these never touch.
List your templates
GET /api/v1/templates returns every template with a published version — exactly the ones the render API will serve.
GET /api/v1/templates?search=invoice&page=1&pageSize=50
Authorization: Bearer spdf_your_key_here
→ 200 · application/json
{
"data": [
{
"id": "6512f3a9c4d21b0f7e8a4c19",
"name": "Invoice",
"status": "active",
"publishedVersion": 3,
"lastPublishedAt": "2025-11-04T09:12:44.001Z",
"pageSize": "A4",
"orientation": "portrait"
}
],
"total": 1,
"page": 1,
"pageSize": 50
}pageOptional. Defaults to 1.
pageSizeOptional. Defaults to 50, maximum 100.
searchOptional. Case-insensitive match on the template name.
Templates you’ve never published don’t appear, since there’d be nothing to render. Archived ones do — they still render, and quietly hiding them would make this list disagree with the render API. Use status if you want to filter or grey them out.
Fetch a template’s data contract
GET /api/v1/templates/:id returns the variables a render payload should provide, plus the template’s sample data — a payload that’s guaranteed to render, so you can test the wiring before you have real data. Pass ?version= to read the contract of a specific published version; omit it for the latest.
GET /api/v1/templates/6512f3a9c4d21b0f7e8a4c19?version=3
Authorization: Bearer spdf_your_key_here
→ 200 · application/json
{
"data": {
"id": "6512f3a9c4d21b0f7e8a4c19",
"name": "Invoice",
"version": 3,
"publishedAt": "2025-11-04T09:12:44.001Z",
"pageSize": "A4",
"orientation": "portrait",
"variables": [
{ "name": "clientName", "label": "Client name", "type": "string" },
{ "name": "total", "type": "string" },
{
"name": "lineItems",
"type": "list",
"itemSchema": [
{ "name": "description", "type": "string" },
{ "name": "qty", "type": "number" },
{ "name": "price", "type": "string" }
]
}
],
"sampleData": {
"clientName": "Acme Studio",
"total": "$1,130.50",
"lineItems": [
{ "description": "Design", "qty": 2, "price": "$400.00" }
]
}
}
} If you pin a version when rendering, read the contract for that same version — a later publish can add or rename variables, and you want the contract that matches the document you’re actually producing.
The response describes your data contract, not the design. Block layout, styling and page setup stay internal to the editor, so we can keep improving them without breaking your integration.
The render API
POST /api/v1/render takes a small JSON body and returns the finished PDF bytes (application/pdf) on the same request — no polling, no webhooks.
POST /api/v1/render
Authorization: Bearer spdf_your_key_here
Content-Type: application/json
{
"templateId": "6512f3a9c4d21b0f7e8a4c19",
"version": 3,
"data": {
"clientName": "Acme Studio",
"total": "$1,130.50",
"lineItems": [
{ "description": "Design", "qty": 2, "price": "$400.00" }
]
}
}
→ 200 · application/pdftemplateIdRequired. The template to render.
versionOptional. Pin a published version; omit for the latest.
dataOptional. Values for your variables; falls back to the template’s sample data.
Every response carries rate-limit headers so integrations can self-throttle: X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset for the per-minute window, plus -Month variants for the monthly window. During the free beta the defaults are 10 renders/minute and 1,000 renders/month per organization.
Markdown to PDF
No template needed: POST /api/v1/render-markdown takes Markdown and returns a typeset PDF, styled by one of ten built-in themes. It is the keyed version of the free converter — same renderer, same options, but metered against your organization’s render quota instead of the anonymous per-IP limit, and counted in your usage dashboard.
POST /api/v1/render-markdown
Authorization: Bearer spdf_your_key_here
Content-Type: application/json
{
"markdown": "# Release notes\n\nShipped **v2.4** today…",
"theme": "technical",
"pageSize": "A4",
"margin": "normal",
"pageNumbers": true,
"accent": "#0284c7",
"font": "Inter",
"headingFont": "Space Grotesk"
}
→ 200 · application/pdfmarkdownRequired. CommonMark + tables, up to 200,000 characters. Raw HTML is escaped.
themeclean, academic, technical, editorial, minimal, contract, brutalist, swiss, newspaper, midnight. Default clean.
pageSize · marginA4 or Letter; narrow, normal or wide.
pageNumbersBoolean. Adds a “1 / n” footer on every page.
accentHex colour for rules, links and highlights. Defaults to #ec4899.
font · headingFont Google Fonts family names from the supported list: Inter, Roboto, Open Sans, Lato, Montserrat, Poppins, Nunito, Work Sans, DM Sans, IBM Plex Sans, Space Grotesk, Source Sans 3, Merriweather, Lora, Playfair Display, Libre Baskerville, EB Garamond, Crimson Text, IBM Plex Serif, Source Serif 4, Bitter, Oswald, Raleway, Josefin Sans. Omit either to use the theme’s own face.
Error states
Errors come back as standard HTTP status codes with a message. The common ones — 400, 401, 403 and 404 apply to every endpoint; the rest are specific to rendering:
400Invalid requestThe request failed validation — usually a missing templateId, malformed data, or an out-of-range query parameter. The message lists each problem as path: problem, and data.issues has the full list.
401Auth failedNo API key was sent, or the key is unknown/revoked. Send it as a Bearer token or x-api-key header.
403Missing permissionThe key is valid but lacks the permission this endpoint needs (e.g. Render PDFs). Keys created before permissions existed can render and read templates.
404Not found / not publishedThe templateId does not exist, or it exists but has no published version (publish it in the editor first).
429Rate limitedYou exceeded the per-minute or monthly render allowance. Check the Retry-After and X-RateLimit-* headers, then back off.
500Render failedThe template rendered but PDF generation errored. Retry; if it persists, get in touch.
503Capacity exhaustedThe render pool is momentarily saturated. The template is fine — retry after a short delay.
401, 403, 404 and 429 mean fix the request (key, template, or back off). 500 and 503 are safe to retry — the template itself is fine.
Usage dashboard
Your organization’s Usage page reports API render activity over the last 7, 30 or 90 days. Editor previews and Download-PDF clicks are metered separately and never counted against your API numbers.
Total successful API renders in the period, and the volume of PDF generated.
Share of requests that failed, with the raw failed count.
p95 and average render time, so you can see the slow tail, not just the average.
How many requests were rejected with a 429 — a sign you’re near your allowance.
Renders, failures, latency and last-render time broken down per template.
A count of each failure type (not found, not published, invalid request, and so on).
A Beta allowance panel shows how much of your monthly and per-minute render budget is left and when each window resets, so you can see the ceiling before you hit it. Render history is retained for 90 days.
Stuck on something not covered here? Email us — we’re happy to help.
Stop hand-crafting PDFs
Your first template takes five minutes. The ten-thousandth PDF takes a fraction of a second.
Try it free