Skip to content

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.

Terminal window
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.

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.

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.

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:

  1. Enter a Key name of up to 64 characters.
  2. Tick the scopes the application needs under Permissions.
  3. Pick when it Expires: Never, 30 days, 90 days or 365 days.
  4. 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.

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.

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.

  • 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_REQUEST instead 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 Created with the new resource. POST /v1/renders answers 200 when the render ended within waitMs and 202 otherwise; a batch answers 202.
  • Deletes answer 200 OK with "deleted": true and the deleted IDs.
  • Download and upload links last five minutes. Read the render or asset again for a fresh one.

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.

Terminal window
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=asc or desc.
  • Not paged (every item at once): reports, API keys, webhook endpoints, signing certificates, storage destinations, workspace references and a file’s usages.
  • You got an error response: retry only when retryable is true, after the Retry-After header’s seconds when it is sent.
  • You got no response (a timeout or dropped connection): you can always retry a GET. Retry a render POST only when it carries an Idempotency-Key header.

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.

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 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.

  1. 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.png
    curl "$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, an uploadUrl and the uploadHeaders to send.

  2. Within five minutes, PUT the file to uploadUrl with every header in uploadHeaders:

    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.

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 400 to a field it does not know and 404 to an endpoint it does not have.

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.
  • Someone changed it first. You saved a draft or file with an expectedRevision that 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. details lists 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.

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.