Skip to content

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}/generate

Each 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.)

Path parameter: templateId (UUID) — the template to render.

Headers

HeaderRequiredDescription
X-Api-KeyyesYour API key
Content-Typeyesapplication/json

Body

FieldTypeRequiredDescription
variablesobjectyesMap of variable name → value. Names must match those from GET /templates/{id}. Values are strings.
formatstringnopng (default), jpeg, webp, or pdf
deliverystringnobinary (default) returns image bytes; link publishes the render and returns a JSON share link
optionsobjectnoRender options (below)

options object

FieldTypeRangeDescription
scaleinteger1–4; fractions such as 1.5 are rejectedResolution multiplier for raster output
qualityinteger1–100Compression quality for jpeg / webp
outputDpiinteger96, 150, 300DPI tier — use 300 for print/PDF
backgroundColorstring#RGB, #RRGGBB or #RRGGBBAAReplaces the template’s background (solid, gradient or image) with this colour; other values are rejected with 400
Terminal window
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.

On success the response is 200 OK. The body depends on delivery.

The raw image is the body — no JSON wrapper. The Content-Type matches the requested format:

formatContent-Type
pngimage/png
jpegimage/jpeg
webpimage/webp
pdfapplication/pdf

A Content-Disposition: attachment; filename=… header is included. Write the body straight to a file or pipe it onward.

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

HeaderDescription
X-Request-IdID 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-IdTemplate used for the render
X-TimestampISO 8601 UTC time the response was produced
X-Quota-LimitMonthly render ceiling for the workspace
X-Quota-RemainingRenders left this billing period
X-Quota-ResetISO 8601 UTC time the quota resets
X-Unknown-VariablesKeys 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-TotalHow many unknown keys there are. Present only when X-Unknown-Variables is cut to its first 20.

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%A3 is my key and price£.
  • At most 20 keys are listed, the first 20 in sort order. When there are more, X-Unknown-Variables-Total carries 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 OK
Content-Type: image/png
X-Unknown-Variables: Price,discount%20code

Browser callers can read both headers: they are listed in Access-Control-Expose-Headers next to X-Request-Id.

  • Supply every required variable. Missing required variables return 400 with an application/problem+json body naming the offending variable.
  • Values are sent as strings, including number, date, phone, and url types — they are validated against the template’s rules server-side.
  • date must be a real calendar date in YYYY-MM-DD (2026-02-30 is rejected). phone accepts 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. A zandovi:// value must be zandovi://images/{uuid}. Any other value returns 400 VALIDATION_ERROR before any render is charged.
  • qrcode and barcode variables take the data to encode. An image variable takes one of three forms: an https:// image URL, a zandovi://images/{id} reference to one of your workspace’s images (copy it from the designer’s My Images menu or read it from the uri field of GET /api/v1/images), or a bundled template image path /images/templates/<file> (a single .jpg, .jpeg, .png or .webp file name, as the built-in templates use it). Anything else, such as an http:// URL, a data: URI or any other path, returns 400 VALIDATION_ERROR before any render is charged.
  • A barcode value is checked against the format of every barcode element it fills: EAN13 needs exactly 13 digits, UPC 12, ITF14 14, and CODE39 accepts only digits, upper-case letters, space and - . $ / + %. Other formats (such as CODE128) only need a non-empty value without control characters. A value that does not fit returns 400 VALIDATION_ERROR before any render is charged, with a per-variable message in details, 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 defaultValue if the template defines one; otherwise an optional variable simply renders empty. A JSON null value is treated like a missing key, so the default applies.
CodeCause
400Missing required variable or a value failed validation
401Missing or invalid API key
403delivery: "link" requested but share links aren’t on this plan, or the active-link cap is reached
404Template not found or not accessible to this key
422IMAGE_VARIABLE_UNREACHABLE: an image you supplied for an image variable could not be loaded. No image is produced and the render is refunded
429Monthly quota or short-term rate limit exceeded
502 / 503Rendering service errored or is temporarily unavailable — retry with backoff. With delivery: "link", also covers share storage being unavailable or the upload failing

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.variable names the variable. When several image variables fail, the first one in the template’s element order is reported.
  • details.reason is one of not_found (the host doesn’t resolve, the URL answered 404 or another error, a zandovi://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), or too_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 422 even when it equals the template’s default image.

See Errors, quotas & rate limits for response bodies and retry guidance.