---
title: "Render a document"
method: POST
path: "/v1/render"
tags: ["Renders"]
---

# Render a document

`POST /v1/render`

## Request

- Base URL: `https://api.galleyrender.com`
- URL: `https://api.galleyrender.com/v1/render`
- Auth: HTTP bearer

Renders `template` with `data` and returns a signed URL. Returns `200` when the render finished inside the request, `202` when it was queued. A cache hit always returns `200`.

## Request body

- RenderRequest
  - `template` string, required: `invoice` for the latest version, or `invoice@3` to pin one. Pin it in anything you ship.
  - `version` union: Version number, if you would rather not put `@3` in `template`.
    - integer
    - string
  - `data` object: The payload, validated against the template's JSON Schema. `lang` and `labels` at the root of `data` are read by the language engine only where the template's schema declares a root property of that name, as a direct property (a `$ref` or `allOf` declaration does not count). The starter library's templates declare them for new accounts and for existing accounts once the library refresh has been run; on a template that does not, they are ordinary unknown fields (an `unknown_field` warning on an open schema, a 422 on a closed one), and the template's own schema decides what they may hold. Fields the template does not use are never an error; they come back in `warnings`.
    - `lang` string: Only where the template's schema declares `lang`. Language of the document's fixed text — titles, labels, headings, default sentences — and the locale dates and money are formatted in, as a BCP 47 tag (the languages with translated text are listed at /docs/templates/languages). The library's schemas accept a tag of up to 35 characters matching the pattern below, and refuse anything else with a 422 at `data.lang`; the engine itself reads up to 100. A tag that passes the pattern but that `Intl` cannot canonicalise (for example `en-a`) is not an error: it is ignored, as if `lang` had not been sent, with no warning. A tag with no translated text formats dates and money for that locale, keeps the English text, and adds an `unsupported_language` warning. Omit it and nothing changes.
    - `labels` object: Your own wording for any of the document's fixed strings, by key. Wins over `lang`. Use it for a house term ("Quotation" instead of "Estimate"), or for a language we do not ship.
  - `format` 'pdf' | 'png' | 'jpg' | 'jpeg': Defaults to `pdf` for chromium templates and `png` for satori ones. `webp` is not in v1.
  - `options` RenderOptions: Merged over the template's own defaults. Options are part of the cache key, so two calls that differ only here are two different renders.
    - `page_size` string: PDF page size: `Letter`, `A4`, `Legal`, or `210mm x 297mm`.
    - `landscape` boolean: PDF only.
    - `margin` union: PDF margins as a CSS length, or per side. Each value is a non-negative number with an optional unit (in, px, cm, mm) — these four and no others, because they are the four Chromium takes. A bare number is pixels: 48, "48", "48px" and "0.5in" are the same margin. A value that is not a length, or a negative one, is refused with 400 invalid_request before the render starts, as are margins larger than the sheet — the one named by page_size, or the largest Chromium knows when there is none, since the page then comes from the template's own CSS. Margins too large for the page a template actually prints on come back as render_failed 422 with cause template.
      - string
      - number
      - object
        - `top` union
          - string
          - number
        - `right` union
          - string
          - number
        - `bottom` union
          - string
          - number
        - `left` union
          - string
          - number
    - `width` integer: Viewport width in px for png/jpg. Default 1200.
    - `height` integer: Viewport height in px for png/jpg. Default 630.
    - `scale` number: Device pixel ratio for png/jpg; a CSS zoom on PDF, where it is capped at 2. Values above the cap are clamped, not rejected, and the cap is 3 on both engines.
    - `full_page` boolean: png/jpg: capture the whole document, not just the viewport. The capture is limited to 16,384 device pixels on either side, measured after `scale`; a larger page is refused with render_failed 422 and cause `template`.
    - `quality` integer: jpg only. Default 90.
    - `background` string: CSS colour painted under the document.
    - `print_background` boolean: PDF: print background colours and images. Default true.
    - `css` string: Extra CSS appended after the template's own styles.
    - `on_blocked_asset` 'fail' | 'skip': What to do when an image, font or other subresource is refused by the egress policy or fails to fetch. `fail` fails the render with `asset_blocked` naming the URL; `skip` drops the asset and renders without it. Both engines behave identically.
  - `webhook_url` string, uri: Absolute https URL to POST on completion. Supplying one forces the queued path. The body is signed with `X-Galley-Signature` (HMAC-SHA256 over the raw body, using the secret from `GET /v1/account`), and delivery is retried up to five times with exponential backoff over about a quarter of an hour.
  - `async` boolean: Force the queued path even for a small job.

## Response `200`

Rendered inside the request, or served from cache.

- Render
  - `object` 'render', required
  - `id` string, required
  - `status` 'queued' | 'processing' | 'succeeded' | 'failed', required
  - `template` string, required: The resolved `name@version`.
  - `format` 'pdf' | 'png' | 'jpg', required
  - `engine` 'chromium' | 'satori', required
  - `cached` boolean, required: True when the stored object was reused. A cache hit is never billed.
  - `url` string, uri, nullable, required: Signed URL to the file. Expires in an hour by default; the stored file lasts until `expires_at`. `null` while the render is unfinished, when it failed, and once `expires_at` has passed.
  - `expires_at` string, date-time, nullable: The end of the file's retention, fixed when the render is made from the account's plan. After it the render is no longer a cache hit, `GET /v1/renders/{id}` answers `410 retention_expired`, and the file is deleted by a daily sweep. Not the signed URL's expiry.
  - `page_count` integer, nullable: Pages in the PDF; 1 for raster output.
  - `billable_units` integer, nullable: 1 per PNG/JPG, 1 per PDF page.
  - `byte_size` integer, nullable
  - `content_type` string, nullable
  - `created_at` string, date-time, required
  - `completed_at` string, date-time, nullable
  - `error` unknown
  - `webhook_status` 'pending' | 'delivered' | 'failed' | 'null', nullable: `null` when no `webhook_url` was given. `pending` while delivery is still being attempted, `delivered` once your endpoint answered 2xx, `failed` once all attempts were used.
  - `webhook_attempts` integer: Delivery attempts made so far.
  - `batch_id` string: Present only for batch items.
  - `warnings` Warning[]: What the request carried that the template did not use, possibly empty. Present on the `POST /v1/render` response (200 and 202) and on every batch item that is not an error. It describes the request, not the render: it is not stored, so `GET /v1/renders/{id}`, `GET /v1/renders` and webhook bodies do not carry it.
    - union: Something about the request that did not stop it. Paths and schema key names only, never a value; at most 20 per response. Discriminated by `type`.
      - object
        - `type` 'unknown_field', required
        - `path` string, required: Where the field is, e.g. `data.deposit`.
        - `message` string, required
        - `did_you_mean` string: The path of the field the template does have, when one is close or a known alias.
      - object
        - `type` 'unsupported_language', required
        - `path` string, required
        - `message` string, required
        - `supported` string[], required: The languages this template has translated text for.
      - object: Language support is switched off on this service, so `data.lang` and `data.labels` were not applied: the fixed text is English, dates and money are formatted for en-US and the paper size is the template's own. Send the same request again later for the document in your language.
        - `type` 'i18n_disabled', required
        - `path` string, required: `data.lang`, or `data.labels` when only labels were sent.
        - `message` string, required
      - object: A PNG made with the Satori engine whose data holds characters outside the Latin fonts the template embeds. Satori has no fallback face, so they render as empty boxes; render with `format: "pdf"` to have them drawn with system fonts.
        - `type` 'font_coverage', required
        - `path` string, required: The first field that holds such text. The text itself is never quoted.
        - `message` string, required
  - `file_deleted_at` string, date-time, nullable: When the file was deleted because its retention had passed. `null` until then. The render record itself is kept; the `data` and `options` it was made from are deleted with the file.

## Other responses

- `202`: Queued. Poll `GET /v1/renders/{id}` or wait for the webhook.
- `400`: `invalid_request` — including a value anywhere in `template`, `data`, `options` or `webhook_url` that cannot be stored: a NUL character (U+0000), an unpaired UTF-16 surrogate (U+D800–U+DFFF not part of a pair), or JSON nested more than 128 levels deep; `errors[0].path` names it. Or `asset_blocked`.
- `401`: `authentication_error` — the key is missing, malformed or revoked.
- `402`: `quota_exceeded` or `spend_cap_exceeded` — checked before the render starts, against the template's declared `expected_pages` for a PDF and 1 unit for raster output. `details.estimated_units` is what was estimated.
- `403`: `permission_error` — the account is suspended. `plan_required` — the request asked for something this account's plan does not carry: a `webhook_url`, or a cloud delivery option. `details.required_plan` names the cheapest plan that would work.
- `404`: `not_found` — no such template or version on this account.
- `422`: `validation_error` — the payload does not match the template schema. `render_failed` with `details.cause: "template"` — the template or its data made the render fail (a Liquid error, markup Satori rejects, a page that never finishes loading, a page size Chromium cannot print). Fix the template or the data; retrying fails the same way.
- `429`: `rate_limited` — more renders started than the plan allows per minute (60 free, 600 paid), counted per API key. `Retry-After` says how many seconds to wait; `X-RateLimit-Remaining` is on successful responses too.
- `500`: `render_failed` with `details.cause: "server"` or `"storage"` — the render failed on our side; one retry is reasonable. `details.shutting_down: true` is one shape of `cause: "server"`: the instance that would have handled this render is being replaced and refused rather than starting work it might not finish — the retry is close to certain to work, on the next instance. `internal_error` — ours.

---

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