Generating Images
The generate endpoint is the core of the API. You send variable values; it returns the rendered image bytes — or, with delivery: "link", a public URL for the render instead.
POST /api/v1/templates/{templateId}/generateEach successful call consumes one render from your monthly quota. Identical requests are not deduplicated or cached server-side — every call re-renders and consumes quota, so deduplicate on your side if you may issue the same request twice. (Batch jobs are different: their submit endpoint supports an Idempotency-Key that makes retries safe.)
Request
Section titled “Request”Path parameter: templateId (UUID) — the template to render.
Headers
| Header | Required | Description |
|---|---|---|
X-Api-Key | yes | Your API key |
Content-Type | yes | application/json |
Body
| Field | Type | Required | Description |
|---|---|---|---|
variables | object | yes | Map of variable name → value. Names must match those from GET /templates/{id}. Values are strings. |
format | string | no | png (default), jpeg, webp, or pdf |
delivery | string | no | binary (default) returns image bytes; link publishes the render and returns a JSON share link |
options | object | no | Render options (below) |
options object
| Field | Type | Range | Description |
|---|---|---|---|
scale | integer | 1–4; fractions such as 1.5 are rejected | Resolution multiplier for raster output |
quality | integer | 1–100 | Compression quality for jpeg / webp |
outputDpi | integer | 96, 150, 300 | DPI tier — use 300 for print/PDF |
backgroundColor | string | #RGB, #RRGGBB or #RRGGBBAA | Replaces the template’s background (solid, gradient or image) with this colour; other values are rejected with 400 |
Example
Section titled “Example”curl -X POST https://app.zandovi.com/api/v1/templates/$TEMPLATE_ID/generate \ -H "X-Api-Key: $ZANDOVI_API_KEY" \ -H "Content-Type: application/json" \ -o coupon.png \ -d '{ "variables": { "first_name": "Sarah", "discount_code": "VIP30", "expires_at": "30 Jun 2026" }, "format": "png", "options": { "scale": 2, "quality": 90 } }'For JavaScript and Python examples, see the Quickstart.
Response
Section titled “Response”On success the response is 200 OK. The body depends on delivery.
delivery: "binary" (default)
Section titled “delivery: "binary" (default)”The raw image is the body — no JSON wrapper. The Content-Type matches the requested format:
format | Content-Type |
|---|---|
png | image/png |
jpeg | image/jpeg |
webp | image/webp |
pdf | application/pdf |
A Content-Disposition: attachment; filename=… header is included. Write the body straight to a file or pipe it onward.
delivery: "link"
Section titled “delivery: "link"”The body is application/json describing a public share link:
{ "id": "019a4c31-8e2f-7b10-9c44-1d5e77b0a913", "url": "https://img.zandovi.com/s/8f3c2ad9e1b74c05.png", "mimeType": "image/png", "fileSize": 41982, "createdAt": "2026-08-24T09:00:00Z", "expiresAt": "2026-09-23T09:00:00Z"}The link expires after a plan-dependent number of days, and anyone holding the URL can open it. See Share Links for the full field reference, listing and revocation, and the limits.
Either way the call consumes exactly one render — delivery changes how the result is returned, not what it costs.
Response headers
| Header | Description |
|---|---|
X-Request-Id | ID for this request — include it in support requests. It is the X-Request-Id you sent when that is 1–128 characters of A–Z a–z 0–9 . _ : -, otherwise a generated one, and the requestId of an error body always equals it |
X-Template-Id | Template used for the render |
X-Timestamp | ISO 8601 UTC time the response was produced |
X-Quota-Limit | Monthly render ceiling for the workspace |
X-Quota-Remaining | Renders left this billing period |
X-Quota-Reset | ISO 8601 UTC time the quota resets |
X-Unknown-Variables | Keys in variables that are not variables of the template, so they were ignored. Absent when every key is a variable. See Unknown variable keys. |
X-Unknown-Variables-Total | How many unknown keys there are. Present only when X-Unknown-Variables is cut to its first 20. |
Unknown variable keys
Section titled “Unknown variable keys”A key in variables that is not a variable of the template (a typo, a spare CSV column, the wrong case: names are case-sensitive, so Price is not price) is ignored, never rejected. The render and its charge are exactly what they would be without that key, and the variable you meant to fill keeps its default. Both delivery modes report such keys in the X-Unknown-Variables response header:
- The keys are sorted and comma-separated.
- Each key is percent-encoded as UTF-8, with the RFC 3986 unreserved characters (
A–Z a–z 0–9 - . _ ~) left as they are, so the header stays ASCII and a comma inside a key cannot split it. Decode each entry after splitting on,:my%20key,price%C2%A3ismy keyandprice£. - At most 20 keys are listed, the first 20 in sort order. When there are more,
X-Unknown-Variables-Totalcarries the full count; otherwise it is absent. - A key longer than 64 characters is cut to 64 characters plus
…(encoded%E2%80%A6). - When every key is a variable, neither header is sent.
HTTP/1.1 200 OKContent-Type: image/pngX-Unknown-Variables: Price,discount%20codeBrowser callers can read both headers: they are listed in Access-Control-Expose-Headers next to X-Request-Id.
Variable values & validation
Section titled “Variable values & validation”- Supply every required variable. Missing required variables return
400with anapplication/problem+jsonbody naming the offending variable. - Values are sent as strings, including
number,date,phone, andurltypes — they are validated against the template’s rules server-side. datemust be a real calendar date inYYYY-MM-DD(2026-02-30is rejected).phoneaccepts an optional leading+, 7 to 15 digits, spaces,-,.,/and at most one(…)group of 1 to 5 digits; values with fewer than 7 digits, which older versions accepted, are now rejected. Azandovi://value must bezandovi://images/{uuid}. Any other value returns400VALIDATION_ERRORbefore any render is charged.qrcodeandbarcodevariables take the data to encode. Animagevariable takes one of three forms: anhttps://image URL, azandovi://images/{id}reference to one of your workspace’s images (copy it from the designer’s My Images menu or read it from theurifield ofGET /api/v1/images), or a bundled template image path/images/templates/<file>(a single.jpg,.jpeg,.pngor.webpfile name, as the built-in templates use it). Anything else, such as anhttp://URL, adata:URI or any other path, returns400VALIDATION_ERRORbefore any render is charged.- A
barcodevalue is checked against theformatof every barcode element it fills:EAN13needs exactly 13 digits,UPC12,ITF1414, andCODE39accepts only digits, upper-case letters, space and- . $ / + %. Other formats (such asCODE128) only need a non-empty value without control characters. A value that does not fit returns400VALIDATION_ERRORbefore any render is charged, with a per-variable message indetails, for example"ean": "EAN13 barcode: exactly 13 digits are required". When one name fills barcodes of several formats, the value must fit all of them, and the message lists each format it fails. - Variables you omit fall back to their
defaultValueif the template defines one; otherwise an optional variable simply renders empty. A JSONnullvalue is treated like a missing key, so the default applies.
Errors
Section titled “Errors”| Code | Cause |
|---|---|
400 | Missing required variable or a value failed validation |
401 | Missing or invalid API key |
403 | delivery: "link" requested but share links aren’t on this plan, or the active-link cap is reached |
404 | Template not found or not accessible to this key |
422 | IMAGE_VARIABLE_UNREACHABLE: an image you supplied for an image variable could not be loaded. No image is produced and the render is refunded |
429 | Monthly quota or short-term rate limit exceeded |
502 / 503 | Rendering service errored or is temporarily unavailable — retry with backoff. With delivery: "link", also covers share storage being unavailable or the upload failing |
Unreachable image variables
Section titled “Unreachable image variables”When you supply a value for an image variable and Zandovi cannot load that image, the render fails with 422 IMAGE_VARIABLE_UNREACHABLE instead of drawing a grey placeholder box. Nothing is rendered, and the unit charged for the request is refunded, so your quota is unchanged:
{ "status": 422, "code": "IMAGE_VARIABLE_UNREACHABLE", "detail": "The image supplied for variable 'product_image' could not be loaded (not_found).", "details": { "variable": "product_image", "reason": "not_found" }, "requestId": "3f2b8c1e-7d4a-4b9e-9c2f-5a6d8e1b0c47"}details.variablenames the variable. When several image variables fail, the first one in the template’s element order is reported.details.reasonis one ofnot_found(the host doesn’t resolve, the URL answered 404 or another error, azandovi://images/…id isn’t one your organization can see, or a bundled/images/templates/…file doesn’t exist),forbidden(401 or 403),timeout,not_an_image(the response isn’t a JPEG, PNG, GIF or WebP image),blocked_url(the URL or one of its redirects points at a private address or a non-http(s) scheme), ortoo_large(over 5 MB).- A value you supplied is never replaced by the template’s default image. Fix the URL, or omit the variable to render the default.
- If you don’t supply a value and the template’s own default image can’t be loaded, the render still succeeds with a grey placeholder box. A value you supply that can’t be loaded is a
422even when it equals the template’s default image.
See Errors, quotas & rate limits for response bodies and retry guidance.