PDF renders
Create PDFs from HTML or saved templates, check each render, and read the result fields and limits.
Give this prompt to your AI agent. The agent does the task for you.
Read https://thirds.ai/docs/api-reference/pdf and help me choose the right thirds.ai endpoint and write a request. Check the fields and credentials in this reference.
See all API resources or read the PDF generation API guide.
Endpoints
createPdf
POST /v1/pdf
Create a PDF
Render one PDF from raw HTML, an inline stateless template, or one owned saved-template version. An omitted saved version resolves to one exact immutable version before data validation, admission, idempotency, queueing, or billing. Template evaluation happens before storage and only evaluated HTML enters render input. Returns 200 for a terminal job or 202 while the accepted job remains queued or running. A terminal job can be succeeded, failed, or cancelled; check status before downloading, then use file downloads to read the finished file. Send wait=false to skip the bounded wait. Every mode uses the same queue, renderer, retention rules, webhook, and fixed one-credit success price.
Authentication: bearer API key. See authentication.
| Parameter | Location | Required | Rule |
|---|---|---|---|
Idempotency-Key | header | No | 1 to 255 printable ASCII characters. A repeated key with the same logical request replays one job; a different logical request conflicts. Stateless fingerprints include original source, sorted data, and effective render options. Saved-template fingerprints include the exact resolved template version, sorted data, and effective options. wait, request IDs, and transport choices do not affect the fingerprint. {"type":"string","minLength":1,"maxLength":255,"pattern":"^[ -~]+$"} |
Request body: application/json, PdfRequest.
| Status | Meaning | Body |
|---|---|---|
| 200 | The job is terminal. Check status for succeeded, failed, or cancelled; HTTP 200 does not mean the render succeeded. | application/json: PdfJob |
| 202 | The job was accepted and is still queued or running. Poll GET /v1/pdf/{id} for the terminal result. | application/json: PdfJob |
| 400 | The request failed validation, saved-template data did not match its schema, bounded template evaluation failed, or the Idempotency-Key header was the wrong shape. | application/json: ErrorEnvelope |
| 401 | The request did not carry a valid, active API key. | application/json: ErrorEnvelope |
| 402 | The account cannot pay the one credit required for this render. The code is insufficient_credits, overage_limit_reached, spend_cap_reached, or overage_unavailable. The reservation settles at one credit on success and is fully released on failure. | application/json: ErrorEnvelope |
| 403 | The authenticated account is suspended. The code is account_suspended. | application/json: ErrorEnvelope |
| 404 | The one error envelope every backend response uses. | application/json: ErrorEnvelope |
| 405 | The method is not allowed on this route. | application/json: ErrorEnvelope |
| 409 | This idempotency key was already used for a different logical request, or the selected saved-template version changed before admission completed. | application/json: ErrorEnvelope |
| 413 | The request is larger than this build accepts. | application/json: ErrorEnvelope |
| 415 | The request did not carry a JSON content type. | application/json: ErrorEnvelope |
| 422 | retention_days is longer than the account's plan keeps a file. The code is retention_limit. | application/json: ErrorEnvelope |
| 429 | A direct fleet-protection or abuse limit was hit. The code is rate_limited, account_concurrency_limited, key_concurrency_limited, or abuse_limited. Each key sustains five requests per second with an idle burst of twenty-one. abuse_limited means too many renders failed today and asks for a one-hour wait. | application/json: ErrorEnvelope |
| 431 | The request has more than 64 headers or more than 32 KiB of header names and values. | application/json: ErrorEnvelope |
| 500 | An internal error occurred. | application/json: ErrorEnvelope |
| 503 | The service is at capacity. | application/json: ErrorEnvelope |
listPdfHistory
GET /v1/pdf
List render history
List the authenticated account's render jobs, newest first. Failed or cancelled jobs removed through DELETE do not appear. Bounded by keyset: pass the previous page's next_cursor to continue, rather than an offset, so a page already read stays stable while new jobs are created.
Authentication: bearer API key. See authentication.
| Parameter | Location | Required | Rule |
|---|---|---|---|
q | query | No | Case-insensitive search of references, template names, and file types. {"type":"string","maxLength":200,"pattern":"^[^\\u0000-\\u001f\\u007f-\\u009f]*$"} |
limit | query | No | 1 to 100. Defaults to 20. A value outside this range is clamped rather than refused. {"type":"integer","default":20} |
cursor | query | No | An opaque value from an earlier page's next_cursor. A cursor that cannot be read is refused with 400. {"type":"string","maxLength":128} |
| Status | Meaning | Body |
|---|---|---|
| 200 | One page of the account's render history. | application/json: PdfHistoryPage |
| 400 | The limit parameter was not a whole number, the cursor could not be read, or the query held an unknown parameter. | application/json: ErrorEnvelope |
| 401 | The request did not carry a valid, active API key. | application/json: ErrorEnvelope |
| 405 | The method is not allowed on this route. | application/json: ErrorEnvelope |
| 431 | The request has more than 64 headers or more than 32 KiB of header names and values. | application/json: ErrorEnvelope |
| 500 | An internal error occurred. | application/json: ErrorEnvelope |
getPdf
GET /v1/pdf/{id}
Get a PDF's status
Return one render job's current status, in the same representation POST /v1/pdf answers with. Mints a fresh signed download link on every call while the PDF is still available.
Authentication: bearer API key. See authentication.
| Parameter | Location | Required | Rule |
|---|---|---|---|
id | path | Yes | The job's public identifier, such as "pdf_1f8b3c7d5e2a49061f8b3c7d5e2a4906". {"type":"string","pattern":"^pdf_[0-9a-f]{32}$"} |
| Status | Meaning | Body |
|---|---|---|
| 200 | The job's current status. | application/json: PdfJob |
| 401 | The request did not carry a valid, active API key. | application/json: ErrorEnvelope |
| 404 | No job with this identifier belongs to the authenticated account. This is also the answer for an identifier that does not exist at all, or that is not well formed, so a request can never learn which one is true. | application/json: ErrorEnvelope |
| 405 | The method is not allowed on this route. | application/json: ErrorEnvelope |
| 431 | The request has more than 64 headers or more than 32 KiB of header names and values. | application/json: ErrorEnvelope |
| 500 | An internal error occurred. | application/json: ErrorEnvelope |
deletePdf
DELETE /v1/pdf/{id}
Delete a PDF file or an unsuccessful render
Remove a successful job's PDF from storage, or remove a failed or cancelled job from history. The job stays readable by ID, and billing, abuse limits, and idempotency keys stay unchanged. The file is removed before its database marker; a file-removal failure returns 500 and leaves the record live for a safe retry. Repeated requests are safe. At or after the file's retention end, the removal reason remains expired.
Authentication: bearer API key. See authentication.
| Parameter | Location | Required | Rule |
|---|---|---|---|
id | path | Yes | The job's public identifier, such as "pdf_1f8b3c7d5e2a49061f8b3c7d5e2a4906". {"type":"string","pattern":"^pdf_[0-9a-f]{32}$"} |
| Status | Meaning | Body |
|---|---|---|
| 200 | The job's current status after file or history removal. | application/json: PdfJob |
| 401 | The request did not carry a valid, active API key. | application/json: ErrorEnvelope |
| 404 | No job with this identifier belongs to the authenticated account. This is also the answer for an identifier that does not exist at all, or that is not well formed, so a request can never learn which one is true. | application/json: ErrorEnvelope |
| 405 | The method is not allowed on this route. | application/json: ErrorEnvelope |
| 409 | The job is queued or running, so it cannot be deleted. | application/json: ErrorEnvelope |
| 431 | The request has more than 64 headers or more than 32 KiB of header names and values. | application/json: ErrorEnvelope |
| 500 | An internal error occurred. | application/json: ErrorEnvelope |
changePdfExpiry
PATCH /v1/pdf/{id}
Change how long a PDF file is kept
Set a new end for a successful job's file: the job's created_at plus retention_days. You can make the time shorter or longer, up to the plan maximum: 365 days on a paid plan, 30 on a free, pack-only, or trial account. The new end must be in the future. A later plan change keeps this choice, except that a smaller plan maximum still shortens it. A job without a live file, such as a queued, failed, deleted, or expired one, returns 404.
Authentication: bearer API key. See authentication.
| Parameter | Location | Required | Rule |
|---|---|---|---|
id | path | Yes | The job's public identifier, such as "pdf_1f8b3c7d5e2a49061f8b3c7d5e2a4906". {"type":"string","pattern":"^pdf_[0-9a-f]{32}$"} |
Request body: application/json, FileExpiryChange.
| Status | Meaning | Body |
|---|---|---|
| 200 | The job with its file's new end in artifact.expires_at. | application/json: PdfJob |
| 400 | The one error envelope every backend response uses. | application/json: ErrorEnvelope |
| 401 | The one error envelope every backend response uses. | application/json: ErrorEnvelope |
| 403 | The authenticated account is suspended. The code is account_suspended. | application/json: ErrorEnvelope |
| 404 | The one error envelope every backend response uses. | application/json: ErrorEnvelope |
| 405 | The method is not allowed on this route. | application/json: ErrorEnvelope |
| 413 | The one error envelope every backend response uses. | application/json: ErrorEnvelope |
| 415 | The one error envelope every backend response uses. | application/json: ErrorEnvelope |
| 422 | The one error envelope every backend response uses. | application/json: ErrorEnvelope |
| 431 | The request has more than 64 headers or more than 32 KiB of header names and values. | application/json: ErrorEnvelope |
| 500 | The one error envelope every backend response uses. | application/json: ErrorEnvelope |
Schemas
PdfRequestCommon
Render options shared by raw HTML, stateless-template, and saved-template requests. Every field is optional; an absent field takes the renderer's own default.
To send finished files to your app, build a webhook endpoint that receives them.
| Field | Required | Description and type |
|---|---|---|
filename | No | Download name. Surrounding spaces are refused. The server uses the real output extension. Omit it for a safe template-name and UTC creation-date name, or html-render and date for HTML. {"type":"string","minLength":1,"maxLength":120,"pattern":"^(?!.* $)(?!.*\\.\\.)[A-Za-z0-9][A-Za-z0-9 ._-]*$"} |
reference | No | Your reference, preserved in the job, history, and webhook. Omitted when not supplied. {"type":"string","minLength":1,"maxLength":200,"pattern":"^[^\\u0000-\\u001f\\u007f-\\u009f]+$"} |
retention_days | No | Days to keep the finished file after it succeeds. Omit it to use the account default, which starts at the plan maximum. A paid plan allows up to 365 days. A free, pack-only, or trial account allows up to 30 days, and a larger value is refused with retention_limit. artifact.expires_at shows the exact end. {"type":"integer","minimum":1,"maximum":365} |
brand_kit_id | No | Select an owned active brand kit. This uses template evaluation with a 1 MiB source limit and a 3 MiB data limit, even when data is omitted. Overrides the source thirds-brand-kit meta default. Supplies reserved brand.name, brand.palette, brand.colours.primary/secondary/accent (the kit colour that fits each role of this design, so the design keeps its own pattern of dark, pale, and bright colours), brand.colours.dark/light (a dark and a light surface in the kit's hue, each readable on the other), brand.ink.primary/secondary/accent/dark/light (#ffffff, #111111, or #000000, the text colour on each role colour, at least 4.5:1), brand.logo, brand.logos, brand.logo_ground (the plate colour on which the first logo shows well), brand.logo_tile.primary/secondary/accent/dark/light (transparent when the logo shows well on that role colour, or else the plate colour), brand.logo_aspect (the logo width divided by its height), brand.fonts entries with family and src, and brand.heading_font and brand.body_font (the family of the first and second font). data.brand is refused when a kit is selected. A kit with no colours supplies no colour roles. Only present logos and fonts are supplied. With no kit, brand is an empty value that is false in a test, so brand.colours.primary | default('#000000') gives the default. Assets are captured before enqueue. Schemas validate customer data before brand is added. {"type":["string","null"],"pattern":"^kit_[a-f0-9]{32}$"} |
pdf | No | PDF layout options. When neither "format" nor both "width" and "height" are given, a saved template uses its own page: its saved page, else its canvas size, else A4. Raw HTML and inline templates use A4. {"type":"object","properties":{"format":{"type":"string","enum":["A0","A1","A2","A3","A4","A5","A6","Letter","Legal","Ledger","Tabloid"],"description":"A named page size, such as \"A4\" or \"Letter\". Mutually exclusive with \"width\"/\"height\"."},"width":{"type":"string","pattern":"^(?:0\\.(?=[0-9]{1,4}(?:px|in|cm|mm)$)(?=[0-9]*[1-9])[0-9]{1,4}|[1-9][0-9]*(?:\\.[0-9]{1,4})?)(?:px|in|cm|mm)$","description":"An explicit page width with its unit, such as \"8.5in\". Requires \"height\"."},"height":{"type":"string","pattern":"^(?:0\\.(?=[0-9]{1,4}(?:px|in|cm|mm)$)(?=[0-9]*[1-9])[0-9]{1,4}|[1-9][0-9]*(?:\\.[0-9]{1,4})?)(?:px|in|cm|mm)$","description":"An explicit page height with its unit. Requires \"width\"."},"margins":{"type":"object","description":"Each side defaults to \"0mm\".","properties":{"top":{"type":"string","pattern":"^(?:0|[1-9][0-9]*)(?:\\.[0-9]{1,4})?(?:px|in|cm|mm)$"},"right":{"type":"string","pattern":"^(?:0|[1-9][0-9]*)(?:\\.[0-9]{1,4})?(?:px|in|cm|mm)$"},"bottom":{"type":"string","pattern":"^(?:0|[1-9][0-9]*)(?:\\.[0-9]{1,4})?(?:px|in|cm|mm)$"},"left":{"type":"string","pattern":"^(?:0|[1-9][0-9]*)(?:\\.[0-9]{1,4})?(?:px|in|cm|mm)$"}},"additionalProperties":false},"landscape":{"type":"boolean","description":"Defaults to false."},"print_background":{"type":"boolean","description":"Defaults to true."},"scale":{"type":"number","minimum":0.1,"maximum":2,"description":"Defaults to 1.0."},"display_header_footer":{"type":"boolean","description":"Defaults to false."},"header_template":{"type":"string","maxLength":100000,"description":"Defaults to empty."},"footer_template":{"type":"string","maxLength":100000,"description":"Defaults to empty."},"title":{"type":"string","maxLength":256,"description":"The PDF document title. An omitted title uses the saved template name, or an empty value when no saved template is selected."},"author":{"type":"string","maxLength":256,"description":"The PDF document author. An omitted author is empty."},"subject":{"type":"string","maxLength":256},"password":{"type":"string","minLength":1,"maxLength":127,"description":"PDF open password, at most 127 UTF-8 bytes. AES-256 encryption."}},"oneOf":[{"required":["format"],"not":{"anyOf":[{"required":["width"]},{"required":["height"]}]}},{"required":["width","height"],"not":{"required":["format"]}},{"not":{"anyOf":[{"required":["format"]},{"required":["width"]},{"required":["height"]}]}}],"additionalProperties":false} |
viewport | No | {"type":"object","properties":{"width":{"type":"integer","minimum":320,"maximum":7680,"description":"Defaults to 1280."},"height":{"type":"integer","minimum":200,"maximum":4320,"description":"Defaults to 720."},"device_scale_factor":{"type":"number","minimum":1,"maximum":3,"description":"Defaults to 1.0."}},"additionalProperties":false} |
javascript | No | {"type":"object","properties":{"mode":{"type":"string","enum":["disabled","enabled"],"description":"Defaults to \"disabled\"."}},"additionalProperties":false} |
wait | No | Defaults to true. False answers as soon as the job is durable instead of holding the bounded wait; poll GET /v1/pdf/{id} for the terminal result. A delivery preference only: it is not part of the idempotency fingerprint and changes nothing about the render or its cost. {"type":"boolean"} |
pages | No | Canvas data-thirds-page ids in output order. Omitted keeps every page in document order. Duplicate or unknown ids are refused. {"type":"array","items":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[A-Za-z][A-Za-z0-9_-]{0,63}$"},"minItems":1,"maxItems":100} |
Schema rules: {"type":"object"}.
RawPdfRequest
A raw HTML render. The HTML enters the existing render workflow unchanged.
Schema rules: {"allOf":[{"$ref":"#/components/schemas/PdfRequestCommon"},{"type":"object","properties":{"html":{"type":"string","minLength":1,"maxLength":5242880,"description":"The raw HTML document to render. Up to 5,242,880 bytes."}},"required":["html"]}],"unevaluatedProperties":false}. See PdfRequestCommon.
StatelessTemplatePdfRequest
A bounded stateless template evaluation followed by the existing PDF render workflow. The request saves no reusable template. Data must be exactly one JSON object. Only evaluated HTML and the effective render options enter short-lived render input storage.
Schema rules: {"allOf":[{"$ref":"#/components/schemas/PdfRequestCommon"},{"type":"object","properties":{"html":{"type":"string","minLength":1,"maxLength":1048576,"description":"The inline Jinja-style template source. The evaluator accepts at most 1,048,576 bytes."},"data":{"type":"object","description":"The JSON-compatible data object available to the template. Arrays, scalars, and null are not accepted as the top-level value. Its canonical recursively key-sorted JSON representation may contain at most 3,145,728 bytes; JSON Schema cannot express that serialized-byte bound."}},"required":["html","data"]}],"unevaluatedProperties":false}. See PdfRequestCommon.
SavedTemplatePdfRequest
Render one exact owned saved-template version. Omit version to resolve the latest version once before validation and admission. Data must be exactly one JSON object.
Schema rules: {"allOf":[{"$ref":"#/components/schemas/PdfRequestCommon"},{"type":"object","properties":{"template_id":{"$ref":"#/components/schemas/TemplateId"},"version":{"type":"integer","minimum":1,"description":"An exact immutable version. Omit to select latest once."},"data":{"type":"object","description":"The JSON-compatible data object validated against the selected version's optional schema before evaluation or admission."}},"required":["template_id","data"]}],"unevaluatedProperties":false}. See PdfRequestCommon, TemplateId.
PdfRequest
Exactly one supported PDF input mode: raw HTML, an inline stateless template, or one saved-template version. Image request shapes are not part of this endpoint.
Schema rules: {"oneOf":[{"$ref":"#/components/schemas/RawPdfRequest"},{"$ref":"#/components/schemas/StatelessTemplatePdfRequest"},{"$ref":"#/components/schemas/SavedTemplatePdfRequest"}]}. See RawPdfRequest, StatelessTemplatePdfRequest, SavedTemplatePdfRequest.
PdfJob
The state of one render job. This is the same shape whether the job just finished (200) or is still in progress (202).
| Field | Required | Description and type |
|---|---|---|
reference | No | Your reference, preserved in the job, history, and webhook. Omitted when not supplied. {"type":"string","minLength":1,"maxLength":200,"pattern":"^[^\\u0000-\\u001f\\u007f-\\u009f]+$"} |
id | Yes | The job's public identifier: "pdf_" followed by 32 lowercase hexadecimal characters. {"type":"string","pattern":"^pdf_[0-9a-f]{32}$"} |
status | Yes | {"type":"string","enum":["queued","running","succeeded","failed","cancelled"]} |
created_at | Yes | {"type":"string","format":"date-time"} |
finished_at | Yes | {"type":["string","null"],"format":"date-time"} |
template | Yes | The exact saved template version this job rendered. Null for raw HTML or an inline stateless template. {"type":["object","null"],"properties":{"id":{"$ref":"#/components/schemas/TemplateId"},"version":{"type":"integer","minimum":1}},"required":["id","version"],"additionalProperties":false}. See TemplateId |
credit | Yes | What happened to the credit this job holds: "reserved" while it is queued or running, "settled" once it succeeded, "released" once it failed or was cancelled, "free" when it is the account's free first file and costs no credit, and "none" when the job was never billed. {"type":"string","enum":["reserved","settled","released","free","none"]} |
artifact | Yes | Present only for a succeeded job. {"type":["object","null"],"properties":{"media_type":{"const":"application/pdf"},"byte_size":{"type":"integer","minimum":0},"sha256":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"64 lowercase hexadecimal characters."},"expires_at":{"type":"string","format":"date-time","description":"When retention ends and the file stops being served."},"removed_reason":{"type":["string","null"],"enum":["expired","deleted",null],"description":"Null while the file is still available. \"expired\" once its retention window or an internal reconciliation has passed; \"deleted\" once the account removed it through DELETE /v1/pdf/{id}."}},"required":["media_type","byte_size","sha256","expires_at","removed_reason"],"additionalProperties":false} |
error | Yes | Present only for a terminal failure. Never carries a message, only a fixed code. {"type":["object","null"],"properties":{"category":{"type":"string","enum":["invalid_input","unsafe_asset","resource_limit","timeout","renderer_failure","internal_failure"]},"code":{"type":"string"}},"required":["category","code"],"additionalProperties":false} |
download | Yes | Present only while a succeeded job's PDF is still available. A fresh signed link every time this job is read. {"type":["object","null"],"properties":{"url":{"type":"string"},"expires_at":{"type":"string","format":"date-time"}},"required":["url","expires_at"],"additionalProperties":false} |
Schema rules: {"type":"object","additionalProperties":false}.
PdfHistoryItem
One job in a bounded account history: the subset of PdfJob a history page lists. Never carries a download link, since a history page can name many jobs at once.
| Field | Required | Description and type |
|---|---|---|
reference | No | Your reference, preserved in the job, history, and webhook. Omitted when not supplied. {"type":"string","minLength":1,"maxLength":200,"pattern":"^[^\\u0000-\\u001f\\u007f-\\u009f]+$"} |
id | Yes | {"type":"string","pattern":"^pdf_[0-9a-f]{32}$"} |
status | Yes | {"type":"string","enum":["queued","running","succeeded","failed","cancelled"]} |
created_at | Yes | {"type":"string","format":"date-time"} |
finished_at | Yes | {"type":["string","null"],"format":"date-time"} |
template | Yes | The exact saved template version this job rendered. Null for raw HTML or an inline stateless template. {"type":["object","null"],"properties":{"id":{"$ref":"#/components/schemas/TemplateId"},"version":{"type":"integer","minimum":1}},"required":["id","version"],"additionalProperties":false}. See TemplateId |
credit | Yes | What happened to the credit this job holds: "reserved" while it is queued or running, "settled" once it succeeded, "released" once it failed or was cancelled, "free" when it is the account's free first file and costs no credit, and "none" when the job was never billed. {"type":"string","enum":["reserved","settled","released","free","none"]} |
artifact | Yes | {"type":["object","null"],"properties":{"media_type":{"const":"application/pdf"},"byte_size":{"type":"integer","minimum":0},"sha256":{"type":"string","pattern":"^[0-9a-f]{64}$"},"expires_at":{"type":"string","format":"date-time","description":"When retention ends and the file stops being served."},"removed_reason":{"type":["string","null"],"enum":["expired","deleted",null]}},"required":["media_type","byte_size","sha256","expires_at","removed_reason"],"additionalProperties":false} |
error | Yes | {"type":["object","null"],"properties":{"category":{"type":"string","enum":["invalid_input","unsafe_asset","resource_limit","timeout","renderer_failure","internal_failure"]},"code":{"type":"string"}},"required":["category","code"],"additionalProperties":false} |
Schema rules: {"type":"object","additionalProperties":false}.
PdfHistoryPage
One bounded page of an account's render history, newest first.
| Field | Required | Description and type |
|---|---|---|
items | Yes | {"type":"array","maxItems":100,"items":{"$ref":"#/components/schemas/PdfHistoryItem"}}. See PdfHistoryItem |
next_cursor | Yes | Pass as the cursor query parameter to read the next page. Null on the last page. {"type":["string","null"],"maxLength":128} |
Schema rules: {"type":"object","additionalProperties":false}.
Shared errors, headers, and authentication schemes are in shared types and security.