Skip to main content

Automation · 3 min read

By Murtaza Aziz

PDF Generation API: Sync vs Async Jobs and When to Use Each

POST /api/public/templates/{templateId}/generate returns a fileUrl immediately. Pass async=true to queue a job and poll GET /api/public/jobs/{jobId}. Same template, two timings.

The public generate endpoint is one URL. Timing is a query parameter. Default is synchronous: you wait, you get fileUrl. Add async=true and you get a jobId to poll. Both paths render the same template with the same JSON. Both need a Bearer df_ key on Pro or Business.

OpenAPI lives at /openapi.json. The PDF generation API page is the product explanation. This article is the timing choice.

Sync: wait for fileUrl

POST /api/public/templates/{templateId}/generate with Authorization: Bearer df_… and a JSON body { "data": { … } }. A 200 response looks like:

{"fileUrl":"https://…/api/downloads/….pdf?exp=…&sig=…","warnings":[],"errors":[]}

Use this from a backend that can wait a couple of seconds: an order-paid handler, a "Download invoice" button on a server-rendered page, a Zapier POST. Omit data only for smoke tests; production calls should send the payload. Empty errors and a fileUrl means you can fetch the PDF. Warnings are layout nags (missing optional fields), not failures.

Async: queue, then poll

Same POST, query string async=true. The 200 body is a job acceptance: jobId and status pending. Poll GET /api/public/jobs/{jobId} with the same Bearer token until the job record shows a finished result (fileUrl) or a failure.

Use async when a request cannot sit open: a webhook that must return 200 to Shopify in a hurry, a batch of 50 invoices from a worker, a UI that should not spin for a heavy multi-page render. Your worker owns the polling loop. Docuplate does not push a callback on the public job API; you pull.

Status codes that are not timing

401: missing or invalid key. 403: the workspace plan does not include API keys (Free and Starter). 404: wrong templateId. Those happen on both sync and async. Do not retry 403 hoping it becomes 200.

Keep the key in an environment variable. The security page covers account and API controls. Rotate a key from the API Keys screen if it leaked; last-used timestamps help you see which integration is still calling.

A 200 with a non-empty errors array is still a failed generate. Check errors before you email fileUrl. Warnings can be logged; they do not mean the PDF is missing. If async returns pending for minutes, the render likely failed server-side: GET the job, read the error, fix the payload.

Picking one and not mixing them blindly

If a human is waiting on a tab, sync is simpler. If a queue already exists in your app, async fits the queue. Calling sync in a tight loop of 200 invoices will hold 200 HTTP connections; that is the case for async plus a small worker pool.

Webhooks and public forms still exist if you do not want to run either mode yourself. They generate the same PDF without an API key. The API is for systems that already have a backend. Copy a curl from the builder's API example tab; it is filled with the open template's sample JSON.

A practical poll loop: wait 500ms, GET the job, retry while status is pending, cap at 30 seconds, then fail the worker and let it retry the whole generate. Do not fire 30 parallel polls on one jobId. fileUrl is a signed download; fetch it soon and store the PDF in your own bucket if you need it next month. The Guides page has cURL, Node, and Python samples for the sync path.

Common questions

Which plans include the generate API?

Pro ($39/mo) and Business ($129/mo). Webhooks and public forms work on Free and Starter without keys.

Does async send a webhook when the job finishes?

Not on the public job endpoint. Poll GET /api/public/jobs/{jobId} until the record includes a fileUrl or an error. Outbound webhooks on Pro are a separate product feature for your own notify URLs.

Can I omit the data object?

Yes, and the template sample payload is used. That is for smoke tests. Real invoices should send the order JSON.

How long is fileUrl valid?

It is a signed download URL with an expiry in the query string. Fetch the PDF in the same job and keep your own copy if you need the file next month.

Keep reading