---
title: "Render an image asynchronously"
method: POST
path: "/render/async"
---

# Render an image asynchronously

`POST /render/async`

## Request

- Base URL: `https://rendry.io/api/v1`
- URL: `https://rendry.io/api/v1/render/async`
- Auth: one of:
  - HTTP bearer
  - HTTP bearer
  - HTTP bearer
  - HTTP bearer

Enqueues the render and returns 202 immediately. Poll /render/status/{job_id} or pass webhook_url.

## Headers

- `X-Rendry-Account` string

## Request body

- RenderRequest: Exactly one of preset_id or draft_id names the design to render. These two doors sell a FINAL render and always deliver the whole page, so they refuse a region outright, "preview": true included; to read one box of a large page, call POST /render/preview, whose body describes region and whose reply carries region and scale.
  - `preset_id` string: Template ID from GET /presets. Either this or draft_id.
  - `draft_id` string: Draft returned by an inline preview (POST /render/preview with canvas+layers). Renders ONCE, charged as a normal render, no template slot spent, and dynamic_data does not apply to it. Either this or preset_id, not both. Accepted by POST /render/sync only; /render/async refuses it, because a draft renders once and is not queued.
  - `dynamic_data` object: Variable substitutions: text variables take text, dynamic image slots take a public image URL
  - `webhook_url` string: Async only: POST-ed {job_id,status,result_url} on completion; the outcome of that call is reported as webhook on GET /render/status/{id}. Must be an http or https address on the public internet: an unparseable address, another scheme, an internal host name or a literal private, loopback or link-local IP is refused with 400 invalid_webhook_url before anything is queued or charged.
  - `preview` boolean: POST /render/sync only: cap the file at 640 px on the long side and charge the preview price (0.1 credit) — the same price on every channel, api key or signed-in session, because the price follows the capped file and not the transport. Rejected for pdf/html, and refused by POST /render/async, which always renders the full page at the full price.
  - `format` 'png' | 'jpeg' | 'webp' | 'pdf' | 'html': Overrides the template's canvas.output_format for this call only. pdf/html render the DOCUMENT profile.
  - `quality` integer: jpeg/webp only. Accepts a number sent as a string.
  - `max_width` integer: Downscale the finished canvas to this width, proportions kept, never upscales. Rejected for pdf/html.
  - `rows` object: Arrays of rows for repeat groups AND for a table bound to rows_key: {"items": [{"name":"Coffee","price":"3.50"}, ...]}. A group with repeat.source="items" clones its children once per row and values arrive as {row.key} in content and url; a table with rows_key="items" takes each row as a body row, mapped onto its columns by columns[i].key
  - `preview_pages` integer[]: pdf only: pages to also return as raster previews of the finished file, free of extra charge and capped like any preview. Page 1 comes back when the field is absent; at most 3 pages per reply, and a page the render does not have is refused before any charge. POST /render/sync renders ONE page, so only [1] applies there and the pictures arrive in its own page_previews; on POST /render/async and POST /render/document they arrive at GET /render/status/{id} once the job is done.

## Response `202`

Accepted

- AsyncRenderResponse
  - `format` string: Document output format.
  - `job_id` string
  - `status` string
  - `status_url` string
  - `message` string
  - `pages_used` string[]: preset_id of every page queued — determinism between calls.
  - `pages` integer: Output page count for document quotes and submissions.
  - `cost_credits` number: Displayed credits for the document. A dry run does not charge.
  - `credits_charged` number: What this call actually took, in credits, at the moment it was queued. Distinct from cost_credits, which a dry run reports for work that is NOT charged.
  - `warnings` string[]: dry_run: the same warnings the finished job would carry.
  - `losses` Loss[]: dry_run, machine-readable: every layer that will be rasterized.
    - `page` integer
    - `layer_id` string
    - `layer_type` string
    - `reasons` string[]: Why it goes raster: rotation, shadow, mask, plate_shape, image_fill and so on.
    - `whole_page` boolean: The whole page was rasterized (blend_mode/backdrop_blur) — single layers are not listed.
    - `loses_text` boolean: This fragment contained text and stopped being text.
  - `text_searchable` boolean: dry_run: true when every page's text comes out of the file as the words it was typed as, false when a line stays text and stops being WORDS (letter_spacing past size/20). ABSENT when the document carries no text at all, so a client promises nothing about a sheet of scans.
  - `error` string: On a refusal: the machine code (see Refusal); `message` is its sentence. On 402 it is the credit failure: see balance, required, packages_url.
  - `balance` number: 402 only: credits the account has.
  - `required` number: 402 only: credits this render needs.
  - `packages_url` string: 402 only: where to top up.
  - `diagnostics` VariableDiagnostic[]: On variables_unresolved: why the template's variables do not resolve for this call.
    - `code` 'invalid_name' | 'invalid_mode' | 'invalid_format' | 'invalid_format_value' | 'reserved_name' | 'unsupported_row_scope' | 'unsupported_reference_scope' | 'template_syntax' | 'formula_syntax' | 'unknown_reference' | 'cycle' | 'nonnumeric_operand' | 'division_by_zero' | 'numeric_limit' | 'source_limit' | 'node_limit' | 'depth_limit' | 'output_limit' | 'circular_layout_dependency', required: The stable codes of variable diagnostics (specs/007-nested-variables/contracts/language.md). A diagnostic names the variable, the code, a reason sub-code, the referenced name, a cycle path and a code-point span in the source — never a source string, a supplied value or a result. A schema problem refuses the save (invalid_name, invalid_mode, reserved_name, unsupported_row_scope, source_limit) and nothing is written; a definition that does not resolve from the saved values is saved and named in the save's warnings; at render every unresolvable definition refuses the request before any charge (400, nothing rendered, nothing charged) with the same codes.
    - `severity` 'error' | 'warning', required
    - `variable` string: The definition or supplied input the problem belongs to; empty for a context-wide limit.
    - `reference` string: The referenced name (unknown_reference, nonnumeric_operand).
    - `path` string[]: A cycle, its first name repeated at the end.
    - `reason` string: A stable sub-code: which syntax rule or which limit.
    - `span` object: Half-open range of Unicode code points in the definition's source.
      - `start` integer
      - `end` integer
  - `row_ranges` DocumentRowRange[]: Exact body-row placement from the prepared document: 1-based inclusive row ranges per final sheet, excluding the repeated header. Available in dry runs and submission replies. Empty tables use 0/0.
    - `authored_page` integer, required
    - `final_page` integer, required
    - `layer_id` string, required
    - `rows_key` string
    - `from_row` integer, required
    - `to_row` integer, required
  - `print` PDFPrint: Selected static CMYK document PDF using the named APTEC coated-offset ICC profile. Omission preserves ordinary RGB output. Opaque canvas required; no HTML, PDF/A/Factur-X or interactive forms. Raster/alpha/effect pages are composited then separated and warn about rasterization/loss of searchable text. Supported square/circle/line/ink/underline/strikeout appearances are server-painted; other annotations refuse before charge.
    - `mode` 'cmyk', required
    - `profile` 'aptec-offset-coated-linearctv-2025-v1'
  - `text_flows` TextFlowReceipt[]
    - `authored_page` integer, required
    - `layer_id` string, required
    - `final_page` integer, required
    - `source_from` integer, required
    - `source_to` integer, required
    - `box` TextFlowBox, required
      - `x` integer, required
      - `y` integer, required
      - `width` integer, required
      - `height` integer, required
    - `text` string, required
    - `wraps` integer[], required

## Other responses

- `400`: The request cannot be queued; nothing was charged: `invalid_request` (the body is not a JSON object), `unknown_field` (a key this door does not read; `fields` names each with a did-you-mean — a near miss is renamed instead and reported in `autofixes`), `variables_unresolved` (with `diagnostics`), `invalid_webhook_url` (a webhook_url the worker would refuse to call, with `reason` beside it), or `invalid_input`.
- `401`: Missing/invalid API key
- `402`: Not enough credits: the body carries error, balance, required and packages_url.
- `404`: `preset_not_found` — absent, deleted or another account's, alike.
- `429`: Too many renders at once — concurrency limit, not a monthly quota. Retry in a moment.

---

[API](https://skmtc.dev/rendry/apis/rendry-api.md) · [All operations](https://skmtc.dev/rendry/apis/rendry-api/llms.txt) · [OpenAPI document](https://skmtc.dev/rendry/apis/rendry-api/revisions/c4fb572c26c9?raw)
