Errors, Quotas & Rate Limits
Error format
Section titled “Error format”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.
Status codes
Section titled “Status codes”| Code | When it happens | What to do |
|---|---|---|
400 | Missing 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 request | Fix the request; the detail names the variable or property, or see AMBIGUOUS_CREDENTIALS and the authoring codes below |
401 | Missing, invalid, or revoked credential (API key or OAuth bearer token) | Check the header and that the credential isn’t revoked or expired |
402 | The output would exceed your plan’s maxOutputPixels (OUTPUT_SIZE_LIMIT_EXCEEDED; details has outputPixels, maxOutputPixels, plan). Nothing is charged | Lower scale/outputDpi or upgrade |
403 | Share 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 account | Upgrade, revoke a link you no longer need, or see INSUFFICIENT_SCOPE and TEMPLATE_FORBIDDEN below |
404 | Template/project/share link/image/template version not found, or not accessible to this credential | Verify the ID and that the template is shared/owned |
422 | A 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 |
429 | Monthly quota or short-term rate limit exceeded | Back off and retry — see below |
502 | The rendering service returned an error, or a share link couldn’t be published | Retry with exponential backoff |
503 | The rendering service or share storage is temporarily unavailable | Retry with exponential backoff |
Render error codes
Section titled “Render error codes”These appear on POST /api/v1/templates/{templateId}/generate; see Generating images:
code | Status | Cause |
|---|---|---|
IMAGE_VARIABLE_UNREACHABLE | 422 | An 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.
Authentication & scope error codes
Section titled “Authentication & scope error codes”These can appear on any /api/v1/** request, whether it’s authenticated with an API key or an OAuth bearer token; see Authentication:
code | Status | Cause |
|---|---|---|
AMBIGUOUS_CREDENTIALS | 400 | Both X-Api-Key and Authorization: Bearer were sent on the same request. Send exactly one. |
INSUFFICIENT_SCOPE | 403 | The credential doesn’t carry the scope the endpoint requires. The detail names the missing scope. |
Authoring error codes
Section titled “Authoring error codes”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:
code | Status | Cause |
|---|---|---|
VALIDATION_ERROR | 400 | A 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_UPDATE | 400 | The PATCH body names no field to change (a receipt on its own is not an update) |
PROJECT_TEMPLATE_LIMIT | 400 | The project already holds the plan’s maximum number of templates |
PROJECT_LIMIT | 400 | The organization already holds the plan’s maximum number of projects |
INVALID_IDEMPOTENCY_KEY | 400 | Idempotency-Key is empty, longer than 255 characters, or not visible ASCII |
FORBIDDEN | 403 | The projectId on a create is not a project owned by your account |
INVALID_CURSOR | 400 | The cursor on a version listing isn’t one that listing issued |
TEMPLATE_FORBIDDEN | 403 | The 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_FOUND | 404 | The template doesn’t exist or isn’t visible to this credential (also on a preview whose templateId names one; nothing is charged) |
VERSION_NOT_FOUND | 404 | The version was pruned (only the last 10 are kept) or never existed; details.oldestRetainedVersion and details.latestVersion give the retained range |
IDEMPOTENCY_IN_PROGRESS | 409 | A request with the same Idempotency-Key is still running — retry shortly to get its result |
DESIGN_INVALID | 422 | The 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_EXCEEDED | 422 | The preview would exceed min(plan cap, 4,000,000) output pixels or scale 2; details has outputPixels, maxOutputPixels, maxScale. Nothing is charged |
RENDER_PROOF_REQUIRED | 422 | A create, or a PATCH with canvasData, was sent without a renderReceipt |
RENDER_PROOF_MISMATCH | 422 | The receipt is for a different design (any changed value or element order) or another organization |
RENDER_PROOF_EXPIRED | 422 | The receipt is older than 60 minutes — render again |
RENDER_PROOF_INVALID | 422 | The receipt is malformed or not signed by Zandovi |
IMAGE_UNREACHABLE | 422 | An external https:// image in the design could not be fetched on save; violations names the elements |
SCHEMA_BREAKING_CHANGE | 422 | A 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_RESTORABLE | 422 | The 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_REUSED | 422 | The Idempotency-Key was already used with a different request body — use a new key |
RENDER_FAILED | 502 | The 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.
Share link error codes
Section titled “Share link error codes”These appear on POST /generate with delivery: "link", and on the share endpoints:
code | Status | Cause |
|---|---|---|
SHARING_NOT_AVAILABLE_ON_PLAN | 403 | The organization’s plan doesn’t include share links |
SHARE_LINK_LIMIT_REACHED | 403 | The organization is at its cap of active links |
SHARE_NOT_FOUND | 404 | No such link, or it belongs to another account |
SHARE_UPLOAD_FAILED | 502 | The render succeeded but couldn’t be published |
SHARE_REVOKE_FAILED | 502 | The published image couldn’t be deleted |
SHARING_UNAVAILABLE | 503 | Share storage is temporarily unavailable |
The two 502s fail in opposite directions, so handle them differently:
SHARE_UPLOAD_FAILEDleaves 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 withdelivery: "binary"instead.SHARE_REVOKE_FAILEDmeans the link may still resolve. Retry until it succeeds; don’t record the link as revoked.
Batch job error codes
Section titled “Batch job error codes”These appear on the batch-jobs endpoints:
code | Status | Cause |
|---|---|---|
VARIABLE_VALIDATION_FAILED | 400 | A row failed the template’s validation — no job created, nothing charged |
BATCH_ROW_LIMIT_EXCEEDED | 400 | More rows than your plan allows per job |
BATCH_SIZE_EXCEEDED | 400 | The job exceeds the structural size ceiling |
TEMPLATE_NOT_FOUND | 400 | Template doesn’t exist or isn’t accessible to this key |
BATCH_INVALID_FILTER | 400 | Unknown status/source value, or invalid paging on the list or items endpoints |
BATCH_INVALID_DELIVERY | 400 | delivery was something other than url or zip |
BATCH_COMPLETION_CALLBACK_URL_INVALID | 400 | callbackUrl isn’t a publicly reachable https address — no job created, nothing charged |
BATCH_NOT_FOUND | 404 | No such job, or it belongs to another account |
BATCH_RESULT_NOT_FOUND | 404 | The completed job’s archive is no longer in storage |
BATCH_RESULT_EXPIRED | 410 | The archive passed its 30-day retention window; the job stays readable |
BATCH_IDEMPOTENCY_KEY_REUSED | 409 | The 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_READY | 409 | The job isn’t COMPLETED yet — keep polling |
BATCH_JOB_ACTIVE | 409 | Delete refused while the job runs — cancel first |
BATCH_RATE_LIMIT_EXCEEDED | 429 | Quota can’t cover the job, or the concurrent-job cap is reached |
BATCH_DISPATCH_FAILED | 500 | Dispatch 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.
Monthly render quota
Section titled “Monthly render quota”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: 10000X-Quota-Remaining: 9831X-Quota-Reset: 2026-07-01T00:00:00ZWhen 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.
Short-term rate limit
Section titled “Short-term rate limit”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: 1X-RateLimit-Remaining: 0Distinguish 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 guidance
Section titled “Retry guidance”- Retry
429(RATE_LIMIT_EXCEEDED),502, and503with exponential backoff (e.g. 1s, 2s, 4s, 8s), honoringRetry-Afterwhen present. - Do not blindly retry
400,401,404, or429(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/503is 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 withIdempotency-Replayed: true. The design preview (POST /designs/render) is not: a retry is a second render, and either receipt is fine. See Authoring Templates.