Skip to content

Render and save a PDF

The single most common thing you’ll do with Pagr: take a published template, feed it a JSON object, and end up with a PDF on disk. This guide walks the whole path, including the two decisions that trip people up — whether to get the bytes inline, and whether to store the result.

You need three things:

  • A published template version. A template with nothing published renders as 404 NoPublishedVersion — see Versions & publishing.
  • The template’s id — copy it from the template list or the editor URL.
  • An API key. Use a pagr_test_ key while you build: output is watermarked and consumes no credit. See API Keys.
  1. Construct a client with your API key.

    The base URL defaults to the hosted Pagr API, so the key is the only required argument. The key’s prefix decides test vs. production — there is no separate flag.

  2. Call render with the template id and your data.

    Your data is a plain object whose keys match the template’s bindings. Not sure what those are? Fetch the version’s sampleData — it matches the bindings by construction. See Templates & versions.

  3. Ask for the bytes inline with includeDocument.

    By default the response carries metadata only and the PDF stays server-side. Pass includeDocument: true and the PDF rides along as Base64, which the SDK decodes for you. Without it, save() has nothing to write.

  4. Check result.ok before touching result.document.

    A document that fails validation is a normal outcome, not an exception: result.ok is false, result.document is null, and result.issues explains why. Only protocol failures raise.

  5. Save it.

    Pass a directory and the SDK names the file from documentName, appending .pdf. Pass a full path and it writes exactly there.

Terminal window
curl -X POST "https://pagr-prd-api-public.azurewebsites.net/v1/render/8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90" \
-H "Authorization: Bearer pagr_test_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"documents": [
{ "Title": "Acme Q3 Invoice", "Amount": 42 }
],
"includeDocument": true
}' | jq -r '.documents[0].documentBase64' | base64 -d > invoice.pdf

Raw HTTP gives you Base64 in a JSON field — you decode it yourself. To skip that step entirely, send Accept: application/pdf instead and write the body straight to a file: see Render a raw PDF.

Set persist to false when the PDF is transient — a download you stream straight to a user, a preview you throw away. Nothing is stored: the render doesn’t appear in Renders, and id / viewUrl come back null.

Terminal window
curl -X POST ".../v1/render/{templateId}?persist=false" \
-H "Authorization: Bearer pagr_test_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "documents": [ { "Title": "Preview" } ] }'

Rendering defaults to the latest published version, so template updates will immediately affect your output. When strict reproducibility matters (e.g., generating exact copies of past invoices), specify the exact version number in your request.

Terminal window
curl -X POST ".../v1/render/{templateId}/versions/3"
  • documentName is data, not a path. It’s generated from the version’s document-name template, so it can embed values bound from your payload — including slashes and dots. TODO: how is it sanitized??
  • When .pdf gets appended differs slightly by SDK. TODO: SHOULD BE CHANGED; Python, TypeScript, Ruby and C++ check the .pdf suffix, so Invoice 2024.10 becomes Invoice 2024.10.pdf. C# and Java check for any extension / dot, so the same name is left as Invoice 2024.10. Pass an explicit full path when the exact filename matters.
  • Render output is always PDF. documentName carries no extension because there’s no other format to distinguish.
  • A slow template needs a bigger timeout, not a retry. The client default is 30 seconds; a document may legitimately take up to the server’s 60-second budget. Pass a per-call timeout rather than raising the client-wide default — see Configure the client. Writes are never retried, so a timed-out render must not be blindly re-sent: it may have rendered and charged already.
  • Warnings block production but not test. A payload that renders on your test key can be rejected on a production key, because production also blocks on Warning-severity issues. See Validate before rendering.
  • Reuse the client. In C# it owns a pooled HttpClient; in Python it owns an httpx connection pool. Create one per process, not one per render.