HTTP API
Any language that can send JSON over HTTPS can render with typefold. The .NET, Python and TypeScript clients wrap the same API. Every endpoint, the scope it needs and its request and response fields are in the API reference.
export PDF_URL='https://pdf.example.com'export API_KEY='paste-the-secret-here'
curl "$PDF_URL/v1/renders?limit=1" -H "x-api-key: $API_KEY"A working key with render:read answers { "items": [...] }. To render, follow the
quickstart, Render a report by name or
Render with the Renders API.
Base URL
Section titled “Base URL”The API answers at the address you open Studio at, such as https://pdf.example.com, and every
endpoint is under /v1. On a self-hosted installation the address is PUBLIC_URL in .env.
Authentication
Section titled “Authentication”Send an API key in the x-api-key header of every request. A key belongs to one workspace and
only sees what is in it.
| Situation | Result |
|---|---|
No x-api-key header, or a wrong, revoked or expired key |
401 UNAUTHORIZED |
| The key lacks the scope the endpoint needs | 403 FORBIDDEN, with a message naming the refused action |
| An ID that belongs to another workspace | 404 NOT_FOUND, as if it did not exist |
A few endpoints, such as creating API keys, are only for people signed in to Studio and refuse
any API key. These need no credential: GET /v1/instance, the upload and download links under
/v1/storage/, and the health checks.
API keys
Section titled “API keys”Workspace owners and admins manage keys in Studio under Manage → API keys. Members do not see that page.
To create a key, select Create API key, then:
- Enter a Key name of up to 64 characters.
- Tick the scopes the application needs under Permissions.
- Pick when it Expires: Never, 30 days, 90 days or 365 days.
- Select Create API key and copy the secret. It is shown only once.
Select keys in the list to change them:
| Action | Effect |
|---|---|
| Rotate | Replaces one key with a new secret that has the same name, scopes and lifetime, counted from now. The old secret keeps working for 15 minutes, or until its own expiry if that comes first. |
| Revoke | Stops the key. It stays in the list as Revoked. |
| Delete | Removes the key. Its audit history stays. |
A revoked or deleted key stops working within about a second. To rotate without downtime, rotate the key, put the new secret in your application, and restart it within the 15 minutes.
The same actions are the /v1/api-keys endpoints in the API reference, where
expiresInDays takes any whole number from 1 to 365. Creating, rotating, revoking and deleting
keys needs a Studio sign-in. An API key with admin:manage can only list keys, never their secrets.
Deploy key
Section titled “Deploy key”On a self-hosted installation, the deploy key set by
DEPLOY_API_KEY in .env has template:read, template:write, template:publish and
asset:manage, and never expires. Studio marks it Set by server. You can revoke it, but
rotating or deleting it answers 409 CONFLICT.
Scopes
Section titled “Scopes”Give each key only the scopes its application needs.
| Scope | Allows |
|---|---|
render:create |
Create renders, batches and string renders, render reports, and preview template drafts. |
render:read |
List and read renders and batches, get download links, read render limits, and receive a PDF from the Reports API. |
render:cancel |
Cancel a queued or processing render. |
template:read |
Read templates, drafts, revisions and versions; list reports and read their data schemas; read and export workspace folders and files; inspect report archives. |
template:write |
Create, edit and delete templates and save drafts; create and change workspace folders and files; import report archives and workspace exports. |
template:publish |
Publish a draft as a new version. |
template:content |
Save the values of a template’s content fields, and upload images for them. |
asset:manage |
Upload, list, read and delete fonts, images, stylesheets and XLSX workbooks, and use them as workspace files. |
audit:read |
Read the workspace’s audit history. |
usage:read |
Read daily render usage. |
webhook:manage |
Manage webhook endpoints and their signing secrets, and read their deliveries. |
certificate:manage |
Upload and delete signing certificates. Listing and reading them needs only template:read. |
destination:manage |
Save, list, change and delete storage destinations. Rendering to one needs only render:create. |
admin:manage |
List the workspace’s API keys. |
Some operations need more than one:
| Operation | Scopes |
|---|---|
| Render a template by name (Reports API) | render:create and template:read, plus render:read for a PDF |
| Preview a draft as a file | render:create and template:read, plus render:read for a PDF |
Render a published version with POST /v1/renders |
render:create |
Render a draft with POST /v1/renders (preview body) |
render:create and template:read |
| Import a report archive, folder or workspace export | template:write, plus asset:manage to bring its uploaded files, and template:publish to publish its templates |
| Export a folder or the workspace | template:read, plus asset:manage when it holds uploaded files |
Common sets:
| Key for | Scopes |
|---|---|
| A backend that renders templates by name | render:create, render:read, template:read |
| A backend that renders its own HTML | render:create, render:read |
| A release pipeline that publishes templates | template:read, template:write, template:publish, plus asset:manage for fonts and images |
| A monitoring job | render:read, usage:read |
People in a workspace have one role each. In Studio, owners and admins have every scope; members
have every scope except admin:manage, webhook:manage, certificate:manage and
destination:manage. Content
editors have template:read, template:content, template:publish, render:create and
render:read, and publish only content changes; see
content fields.
| Action | Member | Admin | Owner |
|---|---|---|---|
| Read, edit and publish templates and workspace files | Yes | Yes | Yes |
| Create, read and cancel renders | Yes | Yes | Yes |
| Upload and delete assets | Yes | Yes | Yes |
| Read render limits, usage and the audit log | Yes | Yes | Yes |
| Create, rotate, revoke and delete API keys | No | Yes | Yes |
| Manage webhooks | No | Yes | Yes |
| Upload and delete signing certificates | No | Yes | Yes |
| Manage storage destinations | No | Yes | Yes |
| Invite people, cancel invitations, change roles, remove members | No | Yes | Yes |
| Make someone an owner, or change or remove an owner | No | No | Yes |
Members do not see API keys, Webhooks and Storage destinations in the Manage menu. They see Signing certificates but cannot upload or delete them. A workspace always keeps at least one owner. To invite people, see invite someone.
Requests and responses
Section titled “Requests and responses”- Bodies are JSON. Send
content-type: application/json. - Unknown fields are refused. A misspelled or unsupported field in a body or query string
answers
400 BAD_REQUESTinstead of being ignored. - Body sizes are limited per endpoint, 1 MiB for most; see limits.
- IDs are opaque strings of up to 128 characters.
- Timestamps are ISO 8601 in UTC, such as
2026-10-08T07:41:05.112Z. - Creates answer
201 Createdwith the new resource.POST /v1/rendersanswers200when the render ended withinwaitMsand202otherwise; a batch answers202. - Deletes answer
200 OKwith"deleted": trueand the deleted IDs. - Download and upload links last five minutes. Read the render or asset again for a fresh one.
Pagination
Section titled “Pagination”Lists that can grow large take a cursor:
| Parameter | Default | Description |
|---|---|---|
limit |
25 | Items per page, 1 to 100 |
cursor |
none | The nextCursor from the previous page |
The response is { "items": [...], "nextCursor": "..." }. While nextCursor is present, send it
back as cursor with the same filters for the next page. When it is absent, you have the last
page.
curl "$PDF_URL/v1/renders?limit=100&cursor=$NEXT_CURSOR" -H "x-api-key: $API_KEY"- Paged: renders, templates and their revisions and versions, assets, audit events, workspace
folders and files, template resources, and webhook deliveries. Renders, assets, audit events
and deliveries come newest first; revisions and versions take
?order=ascordesc. - Not paged (every item at once): reports, API keys, webhook endpoints, signing certificates, storage destinations, workspace references and a file’s usages.
Retries and idempotency
Section titled “Retries and idempotency”- You got an error response: retry only when
retryableistrue, after theRetry-Afterheader’s seconds when it is sent. - You got no response (a timeout or dropped connection): you can always retry a
GET. Retry a renderPOSTonly when it carries anIdempotency-Keyheader.
An Idempotency-Key header makes POST /v1/renders, POST /v1/renders/batch and
POST /v1/reports/render safe to send twice: the repeat returns what the first request created
instead of rendering again. Use a value that names the document, such as invoice-2026-0042, and
send the same value on every retry. How long a key is remembered, and what counts as the same
request, are in Render with the Renders API.
Rate limits
Section titled “Rate limits”429 RATE_LIMITED means you sent too many requests in a minute, or your workspace reached its
render allowances. It is always retryable: wait the seconds in Retry-After, or a few seconds
with backoff when there is none. The numbers are in limits.
Upload fonts, images and workbooks
Section titled “Upload fonts, images and workbooks”Upload a file once, then use its ID in a direct document’s assetIds (see
Render with the Renders API) or add it as a
workspace file. The .NET, Python and TypeScript clients do not upload
files, so use HTTP. It needs the asset:manage scope.
-
Register the file with its size in bytes and its lowercase hex SHA-256:
Terminal window SIZE=$(wc -c < logo.png)SHA=$(sha256sum logo.png | cut -d' ' -f1) # on macOS: shasum -a 256 logo.pngcurl "$PDF_URL/v1/assets" \-H "x-api-key: $API_KEY" \-H "content-type: application/json" \-d "{\"kind\": \"image\", \"filename\": \"logo.png\", \"mediaType\": \"image/png\", \"size\": $SIZE, \"sha256\": \"$SHA\"}"The response has the asset’s
id, anuploadUrland theuploadHeadersto send. -
Within five minutes,
PUTthe file touploadUrlwith every header inuploadHeaders:Terminal window # Repeat -H for each entry in uploadHeaders.curl -X PUT "$UPLOAD_URL" -H "<header name>: <header value>" --data-binary @logo.png
kind |
mediaType |
|---|---|
image |
image/png, image/jpeg |
svg |
image/svg+xml |
font |
font/woff2, font/ttf, font/otf |
stylesheet |
text/css |
workbook |
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet (XLSX) |
A file can be up to 50,000,000 bytes. An upload that does not match the registered size and
checksum is refused, and a file whose contents are not its mediaType answers ASSET_INVALID
when a render uses it.
Compatibility
Section titled “Compatibility”Changes within /v1 only add things: new endpoints, and new optional request or response fields.
To keep working across releases:
- Ignore response fields you do not know.
- Handle unknown enum values and error codes, falling back on the HTTP status.
- Send only fields the server knows. A self-hosted server older than your client answers
400to a field it does not know and404to an endpoint it does not have.
Errors
Section titled “Errors”Every failed request answers with the same shape:
{ "error": { "code": "ASSET_INVALID", "message": "Missing workspace files: shared/logo.png (referenced by template.html).", "retryable": false, "details": [ { "path": "shared/logo.png", "message": "Referenced by template.html but not found in the workspace." } ] }}| Field | Description |
|---|---|
code |
One of the codes below. Branch on this, never on message. |
message |
Readable text that says what went wrong. |
retryable |
true when the same request may succeed later without changes. |
details |
Optional. Each specific problem, as { path?, message }. |
renderId |
Optional. On RENDER_FAILED and RENDER_TIMEOUT, the render the request created. |
| Code | Status | Retry? | What to do |
|---|---|---|---|
BAD_REQUEST |
400 or 415 | No | Fix the request. Compare it with the API reference. 415 means the body was not sent as application/json. A template syntax error or a script without beforeRender also answers this. |
UNAUTHORIZED |
401 | No | Send a valid, active key in x-api-key. Check it under Manage → API keys. |
FORBIDDEN |
403 | No | Give the key the scope the message names, or do the action in Studio. An expired upload or download link also answers 403, and so does a suspended workspace. |
NOT_FOUND |
404 | No | Check the ID or name, and the key’s workspace. The Reports API also answers this when no published template with that name renders the requested format. Deleted renders are gone after retention. |
CONFLICT |
409 | Sometimes | Read the current state and try again. See conflicts. |
ASSET_INVALID |
400 | No | Upload or fix each file named in details. |
UNSUPPORTED_FEATURE |
400 | No | Change the template or request; see what’s not supported. |
LIMIT_EXCEEDED |
400 or 413 | No | Make the input smaller or split the work. 413 is a body over its size limit. See limits. |
QUOTA_EXCEEDED |
402 | No | The workspace used its documents for this month, its file storage, or its test renders for today. Send test renders, delete unused files, wait for 00:00 UTC, or move to a bigger plan. See limits. |
LICENSE_REQUIRED |
403 | No | Self-hosted only: the installation has no valid license, so it cannot render; reading and editing still work. See when rendering stops. |
RATE_LIMITED |
429 | Yes | Wait, then retry. See rate limits. |
RENDER_FAILED |
422 | No | A request that waits for a PDF made a render that failed or was cancelled. Read GET /v1/renders/{renderId}; see render failures. |
RENDER_TIMEOUT |
504 | Yes | The PDF was not ready in time, but the render carries on. Follow it with GET /v1/renders/{renderId}, or repeat the request with the same Idempotency-Key to wait again without a second render. |
INTERNAL |
500 | No | Retry GET requests, and requests with an Idempotency-Key, a few times with backoff. If it keeps happening, check the status page on the hosted service, or tell whoever runs the installation. |
Conflicts
Section titled “Conflicts”- Someone changed it first. You saved a draft or file with an
expectedRevisionthat is no longer current. Read the latest, reapply your change and save again. - Idempotency key reused with a different request. Use a new key for a new request.
- Already exists. A file or folder with that name is already there.
- Render already ended. You cancelled a render that had finished.
- Folder not empty, or still in use.
detailslists the files and what uses them. - Batch not finished. You asked for a combined batch PDF while
items are still rendering (
retryable: true), or after one failed.
Render failures
Section titled “Render failures”A render created with POST /v1/renders or in a batch can fail after the request returned. That
is not an HTTP error: the render’s state becomes failed, with an
error object whose code is one of:
ASSET_INVALID: a font, image or other file could not be loaded.UNSUPPORTED_FEATURE: the document uses something the renderer does not support.LIMIT_EXCEEDED: the document is over a render limit, or the render ran past its time limit.RENDER_FAILED: anything else.
Each diagnostic names one problem:
| Diagnostic | What to do |
|---|---|
max_pages |
The PDF has too many pages. Shorten the document or split it. |
max_output_bytes |
The PDF is too large. Use smaller images or split the document. |
max_input_bytes |
The document, its fonts, images and attachments together are too large. Use smaller images or fonts. |
render_memory_limit |
The document is too complex. Split long tables or reduce images. |
render_process_exited |
The render stopped, for example out of working memory (code RENDER_FAILED). Reduce the document’s size or complexity. |
font_glyphs_missing |
No font covers some characters. Add a font that has them. |
font_invalid |
A font file is damaged. Replace it with a valid WOFF2, TTF or OTF. |
image_invalid |
An image is damaged. Replace it with a valid PNG, JPEG or SVG. |
document_invalid |
The content has an invalid value, or pageRanges selects none of the document’s pages. Fix what the message names. |
pdf_postprocess_failed |
The PDF could not be given its producer, password, signature or pages from other templates. Check those settings. |
pdfa_validation_failed |
The PDF cannot meet the requested PDF/A or PDF/UA level. Fix each problem the message lists; see PDF standards. |
image_page_missing |
The image asks for a page the document doesn’t have. Ask for one it has. |
image_too_large |
The image would be taller than 8192 pixels. Ask for a smaller width. |
watermark_text_unsupported |
The watermark text has characters its font cannot draw. Use Latin, Greek or Cyrillic letters. |
destination_upload_failed |
The file could not be uploaded to the render’s storage destination. Fix the cause the message names; see storage destinations. |
Other diagnostics carry their own advice in message.