Skip to content

Errors, Quotas & Rate Limits

Errors are returned as RFC 9457 application/problem+json:

{
"type": "about:blank",
"title": "Variable validation failed",
"status": 400,
"code": "VALIDATION_ERROR",
"detail": "Required variable 'discount_code' was not supplied.",
"requestId": "3f2b8c1e-7d4a-4b9e-9c2f-5a6d8e1b0c47"
}

The X-Request-Id response header always equals requestId. Send your own X-Request-Id (1–128 characters of A–Z a–z 0–9 . _ : -) to have it used; otherwise one is generated.

Always log requestId — it lets support trace a specific request. Branch your error handling on the numeric status and the machine-readable code.

CodeWhen it happensWhat to do
400Missing required variable, a value failed the template’s validation rules, a body field has the wrong type or value (VALIDATION_ERROR, violations names the field), the body is missing (VALIDATION_ERROR, “Request body is required.”) or is not valid JSON (BAD_REQUEST), a query parameter isn’t a number (VALIDATION_ERROR, violations names the parameter), an authoring request carried an unknown property or hit a plan limit, or both X-Api-Key and Authorization: Bearer were sent on the same requestFix the request; the detail names the variable or property, or see AMBIGUOUS_CREDENTIALS and the authoring codes below
401Missing, invalid, or revoked credential (API key or OAuth bearer token)Check the header and that the credential isn’t revoked or expired
402The output would exceed your plan’s maxOutputPixels (OUTPUT_SIZE_LIMIT_EXCEEDED; details has outputPixels, maxOutputPixels, plan). Nothing is chargedLower scale/outputDpi or upgrade
403Share links aren’t on this plan, the active-link cap is reached, the credential lacks a required scope, or the template or project belongs to another accountUpgrade, revoke a link you no longer need, or see INSUFFICIENT_SCOPE and TEMPLATE_FORBIDDEN below
404Template/project/share link/image/template version not found, or not accessible to this credentialVerify the ID and that the template is shared/owned
422A design failed validation, is too large to preview, or was sent without a valid render receipt; an Idempotency-Key was reused with a different body; an image supplied for an image variable couldn’t be loaded (IMAGE_VARIABLE_UNREACHABLE, see below)Fix the design (the violations say where), render again for a fresh receipt, or use a new key — see Authoring Templates. For IMAGE_VARIABLE_UNREACHABLE, fix the image URL
429Monthly quota or short-term rate limit exceededBack off and retry — see below
502The rendering service returned an error, or a share link couldn’t be publishedRetry with exponential backoff
503The rendering service or share storage is temporarily unavailableRetry with exponential backoff

These appear on POST /api/v1/templates/{templateId}/generate; see Generating images:

codeStatusCause
IMAGE_VARIABLE_UNREACHABLE422An image you supplied for an image variable couldn’t be loaded. Returned by template generate and by POST /api/v1/designs/render (design preview). No image is produced and the render is refunded. details is { "variable": "<name>", "reason": "<reason>" }, with reason one of not_found, forbidden, timeout, not_an_image, blocked_url, too_large

In a batch job, the same failure fails only its row, which is refunded; the job itself doesn’t fail.

These can appear on any /api/v1/** request, whether it’s authenticated with an API key or an OAuth bearer token; see Authentication:

codeStatusCause
AMBIGUOUS_CREDENTIALS400Both X-Api-Key and Authorization: Bearer were sent on the same request. Send exactly one.
INSUFFICIENT_SCOPE403The credential doesn’t carry the scope the endpoint requires. The detail names the missing scope.

These appear on the authoring endpoints: POST /designs/validate, POST /designs/render, POST /templates, PATCH and DELETE /templates/{id}, POST /projects, and on the version history endpoints:

codeStatusCause
VALIDATION_ERROR400A required field is missing, a constraint failed, or the body carries a property the endpoint does not declare (visibility, thumbnail, a viewport size); the detail names it
NOTHING_TO_UPDATE400The PATCH body names no field to change (a receipt on its own is not an update)
PROJECT_TEMPLATE_LIMIT400The project already holds the plan’s maximum number of templates
PROJECT_LIMIT400The organization already holds the plan’s maximum number of projects
INVALID_IDEMPOTENCY_KEY400Idempotency-Key is empty, longer than 255 characters, or not visible ASCII
FORBIDDEN403The projectId on a create is not a project owned by your account
INVALID_CURSOR400The cursor on a version listing isn’t one that listing issued
TEMPLATE_FORBIDDEN403The template exists and is visible to you but was created by another member of your organization; only its creator (in the designer or through the API) can patch or delete it, read or restore its versions, or name it as a preview’s templateId
TEMPLATE_NOT_FOUND404The template doesn’t exist or isn’t visible to this credential (also on a preview whose templateId names one; nothing is charged)
VERSION_NOT_FOUND404The version was pruned (only the last 10 are kept) or never existed; details.oldestRetainedVersion and details.latestVersion give the retained range
IDEMPOTENCY_IN_PROGRESS409A request with the same Idempotency-Key is still running — retry shortly to get its result
DESIGN_INVALID422The design has validation errors; violations lists them with path, code, message, hint — see the validation codes. Nothing is rendered, written or charged
PREVIEW_SIZE_LIMIT_EXCEEDED422The preview would exceed min(plan cap, 4,000,000) output pixels or scale 2; details has outputPixels, maxOutputPixels, maxScale. Nothing is charged
RENDER_PROOF_REQUIRED422A create, or a PATCH with canvasData, was sent without a renderReceipt
RENDER_PROOF_MISMATCH422The receipt is for a different design (any changed value or element order) or another organization
RENDER_PROOF_EXPIRED422The receipt is older than 60 minutes — render again
RENDER_PROOF_INVALID422The receipt is malformed or not signed by Zandovi
IMAGE_UNREACHABLE422An external https:// image in the design could not be fetched on save; violations names the elements
SCHEMA_BREAKING_CHANGE422A PATCH replacement design, a version being restored, or a preview sent with that template’s templateId removes or retypes a variable existing callers rely on; details.violations names them. On a preview nothing is charged
VERSION_NOT_RESTORABLE422The version’s design no longer passes validation; violations lists why, as on DESIGN_INVALID. Read it with ?include=canvas, fix it and save it as a new design
IDEMPOTENCY_KEY_REUSED422The Idempotency-Key was already used with a different request body — use a new key
RENDER_FAILED502The preview could not be rendered; the render unit is refunded, retry

Validation itself never fails a request: POST /designs/validate answers 200 with valid: false and the violations. Successful replays of an idempotent write carry Idempotency-Replayed: true and the original Location.

These appear on POST /generate with delivery: "link", and on the share endpoints:

codeStatusCause
SHARING_NOT_AVAILABLE_ON_PLAN403The organization’s plan doesn’t include share links
SHARE_LINK_LIMIT_REACHED403The organization is at its cap of active links
SHARE_NOT_FOUND404No such link, or it belongs to another account
SHARE_UPLOAD_FAILED502The render succeeded but couldn’t be published
SHARE_REVOKE_FAILED502The published image couldn’t be deleted
SHARING_UNAVAILABLE503Share storage is temporarily unavailable

The two 502s fail in opposite directions, so handle them differently:

  • SHARE_UPLOAD_FAILED leaves nothing behind — no link was created, and nothing is publicly reachable. Retrying is safe, but it re-renders and consumes another render. If you only need the image, retry with delivery: "binary" instead.
  • SHARE_REVOKE_FAILED means the link may still resolve. Retry until it succeeds; don’t record the link as revoked.

These appear on the batch-jobs endpoints:

codeStatusCause
VARIABLE_VALIDATION_FAILED400A row failed the template’s validation — no job created, nothing charged
BATCH_ROW_LIMIT_EXCEEDED400More rows than your plan allows per job
BATCH_SIZE_EXCEEDED400The job exceeds the structural size ceiling
TEMPLATE_NOT_FOUND400Template doesn’t exist or isn’t accessible to this key
BATCH_INVALID_FILTER400Unknown status/source value, or invalid paging on the list or items endpoints
BATCH_INVALID_DELIVERY400delivery was something other than url or zip
BATCH_COMPLETION_CALLBACK_URL_INVALID400callbackUrl isn’t a publicly reachable https address — no job created, nothing charged
BATCH_NOT_FOUND404No such job, or it belongs to another account
BATCH_RESULT_NOT_FOUND404The completed job’s archive is no longer in storage
BATCH_RESULT_EXPIRED410The archive passed its 30-day retention window; the job stays readable
BATCH_IDEMPOTENCY_KEY_REUSED409The Idempotency-Key was already used for a different submission: any difference in template, format, options, filename pattern, callbackUrl, or the rows’ values or order. Not billed
BATCH_RESULT_NOT_READY409The job isn’t COMPLETED yet — keep polling
BATCH_JOB_ACTIVE409Delete refused while the job runs — cancel first
BATCH_RATE_LIMIT_EXCEEDED429Quota can’t cover the job, or the concurrent-job cap is reached
BATCH_DISPATCH_FAILED500Dispatch to the renderer failed — the charge was refunded; retry

Note the deliberate split on results: 409 BATCH_RESULT_NOT_READY means wait longer, 410 BATCH_RESULT_EXPIRED means this job’s archive aged out after 30 days, and 404 BATCH_RESULT_NOT_FOUND means gone. Cancelling a job refunds every render that wasn’t completed — see Batch Rendering.

A completion callback that can’t be delivered doesn’t produce an error response — there’s no request of yours to fail. Its outcome shows up on GET /api/v1/batch-jobs/{jobId} under completionCallback, with the reason in lastError.

Every workspace has a monthly render quota tied to its plan. Only image generation consumes quota, including design previews (POST /designs/render, one unit each, charged only after the design passed validation and the size cap). The read endpoints (list/get templates, version history, images, design reference), design validation, image uploads, version restores and template or project writes are free, and a generate call that is rejected before rendering (missing variables, plan limits, or a 404) does not consume quota either; see Generating images.

Each generate response includes:

X-Quota-Limit: 10000
X-Quota-Remaining: 9831
X-Quota-Reset: 2026-07-01T00:00:00Z

When the quota is exhausted, generation returns 429 with:

Retry-After: 1209600 # seconds until the quota resets (start of next month)
X-RateLimit-Remaining: 0
{
"title": "Monthly quota exceeded",
"status": 429,
"code": "QUOTA_EXCEEDED",
"detail": "Monthly render quota exceeded. Quota resets at the start of next month."
}

A quota 429 will not succeed on retry until the reset time — upgrade your plan or wait. Track X-Quota-Remaining to avoid hitting the wall mid-campaign. See Usage & Quotas.

Independent of the monthly quota, requests are throttled per second to protect the service. Exceeding the rate returns 429 with code: RATE_LIMIT_EXCEEDED and a short Retry-After (seconds):

Retry-After: 1
X-RateLimit-Remaining: 0

Distinguish the two 429 cases by the code field: RATE_LIMIT_EXCEEDED is transient (retry after a short pause), while QUOTA_EXCEEDED is not (wait for the monthly reset).

  • Retry 429 (RATE_LIMIT_EXCEEDED), 502, and 503 with exponential backoff (e.g. 1s, 2s, 4s, 8s), honoring Retry-After when present.
  • Do not blindly retry 400, 401, 404, or 429 (QUOTA_EXCEEDED) — fix the cause instead.
  • The single-render endpoint has no server-side request deduplication — every generate call re-renders and consumes quota. Quota charged for a render that fails with 502 / 503 is refunded automatically, so retrying those is safe. But a retry after a client-side timeout on a call that actually succeeded will consume a second render, so track your own request IDs and deduplicate before retrying. See Generating images.
  • Batch submissions are deduplicated when you send an Idempotency-Key — a retry with the same key returns the original job without billing or rendering twice. Always send one from automation. See Batch Rendering.
  • Template and project writes are deduplicated the same way (POST /templates, PATCH/DELETE /templates/{id}, POST /templates/{id}/versions/{version}/restore, POST /projects, image upload and delete): a repeat with the same key and body replays the first answer with Idempotency-Replayed: true. The design preview (POST /designs/render) is not: a retry is a second render, and either receipt is fine. See Authoring Templates.