Tutorial
How to generate a PDF from JSON with a REST API
A step-by-step tutorial: send a JSON document to a REST API and get back a paginated PDF with tables, totals, headers and page numbers. Copy-paste cURL examples.
Updated · Every code sample on this page is run against the API before publishing.
How JSON to PDF works
Instead of designing a page in HTML and printing it, you describe the document as data: a title, a running header and footer, and an ordered list of blocks such as headings, paragraphs, key/value grids, tables and notes. You POST that JSON to /api/v1/generate, and a layout engine typesets it: it wraps text, breaks pages, repeats table headers on every page and numbers the pages. The response body is the PDF.
This tutorial uses cURL so it works from any terminal. The same requests in Node.js, Python and C# are in the language guides.
1. Your first PDF
Put your API key in an environment variable (export TRYDOCIFY_API_KEY=dfy_live_…) and send the smallest useful document: a title and one paragraph.
curl -X POST https://api.trydocify.com/api/v1/generate \
-H "X-API-Key: $TRYDOCIFY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"test": true,
"title": "Hello, PDF",
"blocks": [
{ "type": "paragraph", "text": "This PDF was generated from JSON." }
]
}' \
--output hello.pdf"test": true renders a free, watermarked PDF, so you can iterate on a layout without spending credits. Every plan includes 200 test renders a day. Open hello.pdf: the title, the paragraph, and page numbers in the footer, which are on by default.
2. A real document
Here is a complete invoice. It uses four ideas you will reuse in most documents:
- header and footer repeat on every page, with
showPageNumbersfor "Page X of Y". - A keyValue block lays out label/value pairs in up to three columns.
- A table with relative column
widths, right-aligned numbers, andfooterrows for totals; the last footer row is styled as the grand total. - A note with a
tone(info, success, warning or danger) for payment instructions.
{
"test": true,
"fileName": "invoice-1042.pdf",
"title": "Invoice #1042",
"subtitle": "Issued 3 November 2026 · Due 3 December 2026",
"header": { "left": "Acme Studio", "right": "INV-1042" },
"footer": { "text": "Acme Studio · 221B Baker Street, London", "showPageNumbers": true },
"theme": { "accentColor": "#1D4ED8" },
"blocks": [
{
"type": "keyValue",
"columns": 2,
"items": [
{ "key": "Billed to", "value": "Globex Corporation" },
{ "key": "Invoice no.", "value": "INV-1042" },
{ "key": "Email", "value": "[email protected]" },
{ "key": "Terms", "value": "Net 30" }
]
},
{
"type": "table",
"columns": [
{ "header": "Description", "width": 4 },
{ "header": "Qty", "align": "right" },
{ "header": "Unit price", "align": "right", "width": 2 },
{ "header": "Amount", "align": "right", "width": 2 }
],
"rows": [
["Design system audit", 1, "$1,200.00", "$1,200.00"],
["Component library (React)", 3, "$850.00", "$2,550.00"]
],
"footer": [
["Subtotal", "", "", "$3,750.00"],
["VAT (20%)", "", "", "$750.00"],
["Total due", "", "", "$4,500.00"]
]
},
{
"type": "note",
"tone": "info",
"title": "Payment",
"text": "Bank transfer to IBAN GB33 BUKB 2020 1555 5555 55. Reference INV-1042."
}
]
}Numbers and booleans are accepted wherever text is expected and are printed exactly as written, so ["Component library", 3, "$850.00"] is a valid row. Format money yourself before sending it; the API never rounds or converts values.
3. Send it from a file
Keep the document in invoice.json and send it with --data-binary, which preserves the file exactly. The response headers tell you about the result:
# Send the document from a file and keep the response headers.
curl -X POST https://api.trydocify.com/api/v1/generate \
-H "X-API-Key: $TRYDOCIFY_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @invoice.json \
--dump-header headers.txt \
--output invoice-1042.pdf
# X-Page-Count, X-Render-Time-Ms, X-Credits-Remaining (and in test mode X-Test-Renders-Remaining)
cat headers.txt| Header | Meaning |
|---|---|
X-Page-Count | Pages in the generated PDF. |
X-Render-Time-Ms | Time spent typesetting, in milliseconds. |
X-Credits-Remaining | Credits left in the current billing period. |
X-Test-Renders-Remaining | Free test renders left today (test mode only). |
4. Handle errors
Every error is an RFC 9457 problem document with a stable code. Validation errors point to the exact JSON path, and they are never charged: a credit is only used for a PDF you actually receive.
# A table row with the wrong number of cells is rejected before rendering, and costs nothing.
curl -X POST https://api.trydocify.com/api/v1/generate \
-H "X-API-Key: $TRYDOCIFY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"test": true,
"blocks": [
{ "type": "table", "columns": [{ "header": "Item" }, { "header": "Amount" }], "rows": [["Only one cell"]] }
]
}'{
"title": "The document payload is invalid.",
"status": 400,
"code": "validation_failed",
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"errors": {
"blocks[0].rows[0]": ["Each row must have exactly 2 cells (one per column)."]
}
}Branch on code, not on the message. The ones to handle in an integration are validation_failed (fix the document), quota_exceeded (upgrade or wait for the reset), and rate_limited / renderer_busy (retry after the Retry-After header). The full list is in the docs.
5. Get JSON instead of bytes
Some tools (queues, no-code platforms, webhooks) can't handle binary responses. Send Accept: application/json and the PDF comes back base64-encoded in data, together with fileName, pageCount, sizeBytes, creditsRemaining and renderTimeMs.
Going to production
- Remove
"test": true. Nothing else changes: same key, same endpoint. - Call the API from your server only. The key must never reach a browser or mobile app.
- Respect
Retry-Afteron429and503. Each key may sustain 120 requests a minute with bursts of 30. - Generate on demand rather than storing PDFs: TryDocify keeps no copy of your documents, and rendering is fast.
Try it with your own data
Every plan includes 200 free test renders a day. Add "test": true while you build, and remove it when you go live.