All errors are JSON with a stable machine-readable error code. Codes never change once shipped. Branch on error, never on the HTTP status alone.
Error shape
json
{
"error": "template_forbidden",
"message": "template_id does not belong to the workspace that owns this API key",
"hint": "List the templates this key can see with GET /v1/templates, and copy the id from there.",
"docs_url": "https://pixldocs.com/docs/api/templates",
"request_id": "9426f621-3a96-4253-8f60-b2d8f4cd6a4a"
}hintanddocs_urlare on every error: the hint is the next action in one sentence, the link is the page that explains it. They are for humans — never branch on them.messageandrequest_idare present on every error the API answers, except the three401s, which carryerror,messageanddocs_urlbut norequest_id— a 401 means we do not know who called, so there is no account for support to look the id up against. A503 service_unavailableis answered by the edge while the API restarts, so it carries amessagebut norequest_id— there was no request to record.- Quote
request_idin any support request — it is how a render is found in our logs.
Full reference
StatusCodeWhen
400
invalid_template_idtemplate_id missing or not a UUID.400
invalid_idA path id (a render or job id) is not a UUID.400
invalid_jsonRequest body is not valid JSON. The message names the position of the syntax error.400
invalid_requestThe body, or one named field in it, has the wrong shape — the body carries field with the name of the offender.400
missing_fieldA required field is absent — html on /v1/render/html, url on /v1/render/url. The body names the field.400
unknown_fieldsdata carries keys the template does not bind. Nothing rendered, no credit used. The body lists unknown_fields and accepted_keys.400
invalid_query_paramA query parameter is present but not a value this endpoint accepts. Nothing is filtered and no list is returned — the message lists the allowed values.400
unknown_query_paramsA query parameter this endpoint does not accept was sent. Refused rather than ignored, so a mistyped filter can never look like a result.400
invalid_formatformat is not one of pdf, png or webp.400
invalid_scalescale is not a number inside the allowed range. Applies to image formats.400
invalid_versionversion is not published or draft.400
invalid_urlurl is not a valid http(s) URL. Applies to /v1/render/url and webhook endpoints.400
invalid_eventsA webhook endpoint was sent an events list that is not a non-empty subset of the supported event names.400
invalid_expect_selectorexpect_selector is not a CSS selector string of at most 200 characters.400
html_too_largehtml plus styles exceeds the inline limit. Nothing rendered, no credit used. Save the markup as a template and render by template_id.400
size_unsupported_for_htmlsize, width or height was sent for an HTML template. HTML templates size themselves through page_setup; those fields apply to canvas templates only.400
unsupported_kindThe template is a canvas template and the endpoint only edits HTML templates.400
invalid_itemsA batch was sent with an empty or non-array items list.400
too_many_itemsA batch carries more than 100 items.400
invalid_idempotency_keyIdempotency-Key longer than 255 characters.400
batch_rejectedAt least one item in a batch is unacceptable — unknown fields, or a template_id that is not a UUID. The whole batch is rejected and no job is queued; the body lists the failing items by index. Note the status is 404, not 400, when the failing item names a template that does not exist.400
liquid_syntax_errorThe template markup does not parse. The body carries line and col. Author error — the caller cannot fix it by retrying.400
liquid_render_errorThe markup parsed but failed while rendering with this data — usually a tag reading a field the data does not carry in the expected shape.400
liquid_filter_errorA filter refused its input — for example barcode or qr given a value it cannot encode. The message names the filter. Nothing rendered, no credit used.400
liquid_partials_disabledThe markup uses include or render. Partials are not available in HTML templates; inline the markup.400
undeclared_fieldsThe markup reads a name that no @field, @derived or @section declares, so no caller could ever send it and every render would answer unknown_fields. The message names each one and hands back a declaration block to paste. Refused on create and on update.401
missing_bearer_tokenNo Authorization header.401
invalid_api_keyBearer token not recognized.401
api_key_revokedKey was revoked from Account Settings.401
api_key_owner_missingThe key is real but the account that created it no longer exists, so nothing it owns can be read or written. Sign in and mint a new key.401
invalid_sessionA browser session token could not be verified. Sign in again and retry. API keys never see this code.402
insufficient_creditsWorkspace owner is out of credits.402
workspace_cap_exceededThe workspace has spent its monthly credit cap. The balance can still be positive — raise the cap in Workspace Settings. Nothing rendered, no credit used.403
template_forbiddentemplate_id belongs to a different workspace.403
feature_disabledThe account does not have the feature the endpoint needs enabled. The body names the feature.400
url_blockedA URL you supplied resolves to a loopback, private, link-local or otherwise non-public address, so this server will not call it. Webhook endpoints are checked when you register them and again on every delivery; a delivery refused this way is terminal and is not retried. Point the endpoint at a publicly reachable URL.403
demo_key_read_onlyThe public demo key cannot create or change templates.403
demo_key_published_onlyThe public demo key renders published gallery templates only — not your own HTML, URLs or drafts.400
demo_key_sync_onlyThe public demo key returns documents directly; async and save_to_renders need your own key.404
template_not_foundtemplate_id does not exist.404
template_config_not_foundThe template exists but has no stored configuration to render — it was never saved.404
render_not_foundAsync render job id does not exist, has expired, or belongs to another workspace.404
not_foundThe addressed row — a webhook endpoint or a delivery — does not exist, is disabled, or belongs to another workspace.409
idempotency_in_progressAn earlier request with the same Idempotency-Key is still running. Retry the same key after Retry-After.413
payload_too_largeThe request body is over the limit for the endpoint (4 MB on most routes, 12 MB on /v1/render-form, 50 MB at the edge). Nothing rendered, no credit used. Upload large images with upload_asset and send the URL instead of inlining the bytes. A body over the 50 MB edge limit is answered by the edge, so it carries no request_id.422
idempotency_key_reuseThe same Idempotency-Key was sent with a different body. Nothing rendered. Use a new key.429
rate_limitedToo many requests in the current one-minute window for this key. The allowance is per key and lower on a test key; the response carries Retry-After, limit_per_min and the RateLimit-* headers, which are the authority on your own limit.500
render_failedThe render itself failed. The credit is refunded. Retry with backoff.500
html_render_failedThe renderer refused inline HTML for a reason it did not name. The credit is refunded. Retry with backoff.500
url_render_failedThe renderer could not turn the given URL into a document. Check the URL is publicly reachable. The credit is refunded.500
render_returned_no_fileThe renderer answered but produced no bytes. Nothing is stored and the credit is refunded. Retry.500
enqueue_failedAn async render could not be put on the queue. No job exists and no credit was used. Retry.500
lookup_failedA render lookup could not read the archive. Nothing was charged. Retry.500
pdf_render_not_configuredThe render service is not configured on this deployment. Nothing rendered, no credit used. This is ours to fix — quote request_id.500
internal_errorThe request failed before it reached a handler. Quote request_id to support.504
render_timeoutThe page did not finish rendering inside the timeout. Nothing was returned and the credit was refunded. Simplify remote assets, then retry.Retry policy
Retry on 429, 500, and 503 with exponential backoff (1s → 2s → 4s → 8s, max 5 attempts). Honour Retry-After when it is present. Do not retry 400/401/402/403/404/422 — they will fail identically. A 409 idempotency_in_progress is the one conflict worth retrying: send the same Idempotency-Key again after Retry-After and you will get the original response, not a second render.