---
title: "Render an image synchronously"
method: POST
path: "/render/sync"
---

# Render an image synchronously

`POST /render/sync`

## Request

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

Renders the template with dynamic_data substituted. Waits up to 25s (typical: 1-3s). Returns result_url of the finished PNG. Body flag "preview": true renders at preview price (0.1 credit) and caps the file at 640 px on the long side.

## 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 `200`

Rendered

- SyncRenderResponse
  - `mime_type` string: Effective output MIME type on success, including request overrides.
  - `credits_charged` number: Actual credits debited by this successful render attempt; zero when no charge was made.
  - `job_id` string
  - `status` 'success' | 'error'
  - `result_url` string: Path under /api/v1/files/… relative to the API origin — prefix https://rendry.io to download. Historical image results are public. New document attempt files and their page previews require the authenticated owner; a user asset (user_assets/…) is served only to its owner.
  - `error` string: On a refusal (status error): the machine code — see Refusal.
  - `message` string: On a refusal: the sentence that explains the code.
  - `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
  - `text_fit` TextFit[]: A LIST, one entry per measured layer, each naming its own layer in `id` — not a map keyed by layer id. Actual per-layer text metrics in DSL layout coordinates: requested size, fitted_size, text_w/text_h and box_w/box_h. Finishing and preview display resizing do not change these values; use geometry to map frames onto the displayed image. The clones of one repeat template arrive COLLAPSED into a single entry that carries the template id, the row count, the worst row and the rows that shrank or overflowed — that entry has collapsed_clones: true and is the only shape that differs from TextFit.
    - `id` string: The measured layer's own id. This is why text_fit is a list: the entry names its layer, the list does not
    - `plate` TextPlateFit: Actual unrotated text plate assembly in DSL pixels, before effects and output scaling. Text coordinates identify the logical text box, not tight glyph bounds.
      - `x` integer, required: Plate left relative to the layer origin
      - `y` integer, required: Plate top relative to the layer origin
      - `width` integer, required: Assembled plate width after minimum size and circle sizing
      - `height` integer, required: Assembled plate height after minimum size and circle sizing
      - `text_x` integer, required: Logical text origin inside the plate, including stroke and bearing placement
      - `text_y` integer, required: Logical text origin inside the plate
      - `text_width` integer, required: Resolved advance or constrained wrap width
    - `rotation_origin` TextRotationOrigin: Actual pre-transform assembly center of ordinary raster text relative to the layer origin, in DSL pixels. Half-pixel centers are preserved.
      - `x` number, required
      - `y` number, required
    - `size` integer: The size that was asked for
    - `fitted_size` integer: The size auto_fit settled on; equal to size when nothing shrank
    - `text_w` integer: Width of the rendered text mask
    - `text_h` integer: Height of the rendered text mask
    - `box_w` integer: Declared layer width; 0 means unconstrained
    - `box_h` integer: Declared layer height; 0 means unconstrained
    - `lines` integer: Lines after wrapping, not only explicit newlines
    - `wraps` integer[]: Where the render broke the text for width: the rune offset, in the text as drawn (variables substituted, list markers and soft hyphens in place, rich_text ** pairs removed), at which each line begins because the line before it was full. A line an explicit newline starts is not listed; absent when nothing wrapped. The editor draws its lines at these offsets
    - `mask_x` integer: mask_text placement inside a shape or image
    - `mask_y` integer
    - `mask_w` integer
    - `mask_h` integer
    - `ink_top` integer: Mask offset from the logical text box; a diagnostic, not an alignment source — Pango bounds can include empty rows
    - `ink_left` integer
    - `fit_group` string: The layer's fit_group name, present only for a member of one
    - `size_alone` integer: The size this member would have taken fitted BY ITSELF, before the group levelled. Present only for a member of a fit_group; a layer in no group reports fitted_size and nothing more
    - `fit_group_size` integer: The size the group settled on — the smallest size_alone among its members. A member whose own min_size is above it keeps min_size, so fitted_size can exceed fit_group_size. Present only for a member of a fit_group
    - `origin` TextDiagnosticOrigin
      - `key` string: Server-created prepared structural identity; independent of generated cell IDs.
      - `layer_id` string: Safe authored layer identifier; values and paths are excluded.
      - `table` boolean
      - `master` boolean
      - `page` integer: Final page, 1-based.
      - `row` integer: Original body row, 1-based; header row zero.
      - `column` integer: Column, 1-based.
      - `header` boolean
  - `warnings` string[]: Document profile (pdf/html): which fragments were rasterized and stopped being text.
  - `pages_used` string[]: preset_id of every page actually rendered — the same call twice gives the same list.
  - `chart_fit` object[]: What the engine decided about each chart layer: the chosen scale, how many values were drawn and how many labels fit. Computed from the preset and the data, so it also comes back without a render
    - `id` string
    - `chart_type` string
    - `values` integer: how many numbers the series had after dynamic_data was applied
    - `labels` integer
    - `y_min` number
    - `y_max` number: the scale the ENGINE picked — this is why a bar is that tall
    - `y_step` number
    - `bars` integer: bars or sectors actually drawn; fewer than values means some were dropped
    - `labels_drawn` integer: labels that fit the declared gutters; zero with non-zero labels means no room was reserved
    - `plot_w` number
    - `plot_h` number
  - `page_previews` PagePreview[]: pdf only: raster pictures of the pages of the file this call just produced — page 1 unless preview_pages named others. Free of extra charge: the pages were drawn in order to make the document. Absent for every other format, and for a pdf whose pictures could not be stored.
    - `page` integer: 1-based page number of the document.
    - `url` string: URL of the stored picture (image/jpeg).
    - `width` integer: Pixel width of the picture as stored, not of the pdf page.
    - `height` integer
  - `geometry` RenderGeometry: Geometry of the successful render attempt, bound to job_id and attempt_id. Layout is the DSL coordinate space. Scale layout to pre_finish, map finish.source_rect to finish.destination_rect when present, then scale output to the actual displayed image. Without finish, pre_finish equals output. Display resizing never changes this report. Absent on legacy jobs or document results without raster geometry.
    - `version` 1, required
    - `job_id` string, required
    - `attempt_id` string, required
    - `layout` RenderGeometrySize, required: Positive raster dimensions in pixels.
      - `width` integer, required
      - `height` integer, required
    - `pre_finish` RenderGeometrySize, required: Positive raster dimensions in pixels.
      - `width` integer, required
      - `height` integer, required
    - `output` RenderGeometrySize, required: Positive raster dimensions in pixels.
      - `width` integer, required
      - `height` integer, required
    - `finish` RenderGeometryFinish: source_rect is in pre_finish; destination_rect is in output. These rectangles define the transform. source_bounds is diagnostic alpha in pre_finish; artwork_bounds and finished_bounds are diagnostic alpha in output. Alpha bounds never replace the transform.
      - `profile_id` string, required
      - `source_rect` RenderGeometryRect, required: Half-open integer pixel-edge rectangle. Transform rectangles have positive extents and are contained in their raster space. Diagnostic alpha bounds may be the all-zero rectangle for empty alpha.
        - `x` integer, required
        - `y` integer, required
        - `width` integer, required
        - `height` integer, required
      - `destination_rect` RenderGeometryRect, required: Half-open integer pixel-edge rectangle. Transform rectangles have positive extents and are contained in their raster space. Diagnostic alpha bounds may be the all-zero rectangle for empty alpha.
        - `x` integer, required
        - `y` integer, required
        - `width` integer, required
        - `height` integer, required
      - `source_bounds` RenderGeometryRect, required: Half-open integer pixel-edge rectangle. Transform rectangles have positive extents and are contained in their raster space. Diagnostic alpha bounds may be the all-zero rectangle for empty alpha.
        - `x` integer, required
        - `y` integer, required
        - `width` integer, required
        - `height` integer, required
      - `artwork_bounds` RenderGeometryRect, required: Half-open integer pixel-edge rectangle. Transform rectangles have positive extents and are contained in their raster space. Diagnostic alpha bounds may be the all-zero rectangle for empty alpha.
        - `x` integer, required
        - `y` integer, required
        - `width` integer, required
        - `height` integer, required
      - `finished_bounds` RenderGeometryRect, required: Half-open integer pixel-edge rectangle. Transform rectangles have positive extents and are contained in their raster space. Diagnostic alpha bounds may be the all-zero rectangle for empty alpha.
        - `x` integer, required
        - `y` integer, required
        - `width` integer, required
        - `height` integer, required
  - `expires_at` string, date-time: Server-owned file deadline: authenticated live/test output expires 365 days after completion; guests expire 72 hours after batch creation.

## Other responses

- `400`: Refusal before anything is 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`), or `invalid_input` (`message` names the field).
- `401`: Missing/invalid API key
- `402`: Not enough credits: the body carries error, balance, required and packages_url.
- `404`: `preset_not_found` (or `draft_not_found` for a draft_id) — absent, deleted or another account's, alike.
- `408`: Render timeout
- `409`: `conflict`: the draft has already been rendered, or is rendering now — a draft renders once.
- `422`: Render failed (`message` explains why)
- `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)
