# Render a PDF

POST /v1/render takes markdown and answers with the finished PDF.

Page: https://legivel.com/docs/api/render

POST/v1/render

Send markdown to `https://api.legivel.com/v1/render` and the response body is the finished PDF.

## Request

| Name | Type | Description |
| --- | --- | --- |
| `Authorization`required | header | `Bearer lgv_your_key_here`, an API key from the account page. See [Authentication](https://legivel.com/docs/authentication). |
| `Content-Type` | header | With `application/json` the body is the object described under [Body fields](#body). With `text/markdown`, no header, or any other type, the body is the raw markdown bytes and every field takes its default. A JSON body without a string `markdown` field answers 400 `invalid_request`. |
| `Idempotency-Key` | header | An optional retry key of 1 to 128 characters, scoped to your account. A later request with the same key and the same document renders again at no charge. A different document under a key already used answers 422. A sealed request with this header is refused; see [Idempotency](#idempotency). |

## Body fields

A JSON body carries these fields and no others. An unknown field, at the top level or inside `features`, answers 400 `invalid_request` naming it, so a typo never costs a document on a render you did not ask for.

| Name | Type | Description |
| --- | --- | --- |
| `markdown`required | string | The Markdown source of the document. An empty string is refused with `empty_document`. |
| `features` | object | What the converter does with the document. Every feature is on unless it is sent as `false`, except `mermaid_appendix` and `embed_source`, which are off unless they are sent as `true`. An unknown or camelCase key is refused with `invalid_request` before the render starts, so a typo never costs a document. |
| `output` | "pdf" \| "json" \| "upload" | How the document comes back: `pdf` streams the file, `json` returns it Base64-encoded beside its metadata, `upload` sends it with one HTTP PUT to `upload_url` and answers with a receipt. `upload` needs `upload_url`. Default `"pdf"`. |
| `upload_url` | string | A pre-signed PUT URL for your own storage, for `output: "upload"` and only valid together with it. It must be an absolute `https` URL on a public host; any other address is refused with `invalid_request` before a document is reserved. The PDF is sent once, as given, with only `Content-Type: application/pdf` and `Content-Length`, so a URL that requires any other signed header fails at the storage. The URL is never stored, logged or repeated in a response. Uri format. |
| `filename` | string | The name the streamed file is offered under. The stem is trimmed to 80 characters and `.pdf` is enforced; the default is `document.pdf`. |
| `sign` | object | The certificate to seal this document with. Exactly one form: a stored certificate named by the account, a PKCS#12 bundle, or a key and certificate sent apart. Only the requesting account's own material is used; there is no service certificate. Sealing makes no claim of qualified status or of a reader trusting the document automatically. |
| `resources` | "strict" \| "lenient" | What a resource that cannot be loaded does: `strict` refuses the render with `resource_fetch_failed`, `lenient` draws a placeholder and reports the failures in the resources-failed headers. Default `"strict"`. |
| `language` | string | The document's language as a BCP 47 tag, such as `en-GB`. It sets hyphenation patterns and text shaping. The default is the front matter's `language`, if the document carries one. At most 63 characters. |

## Features

Every feature is on unless you send `false` for it, except `mermaid_appendix` and `embed_source`, which are off unless you send `true`. The `theme` is `"light"` unless you ask for `"dark"`. A feature name the API does not know, a non-boolean value, or a `theme` other than the two answers 400 `invalid_request` naming the key. The names are the ones below; the editor's own spellings are not accepted here.

| Name | Type | Description |
| --- | --- | --- |
| `mermaid` | boolean | Draw fenced `mermaid` blocks as diagrams. Default `true`. |
| `mermaid_appendix` | boolean | Put each diagram on a page of its own after the document, with a link where the diagram was written. Off unless set to true: diagrams stay where they are written. Default `false`. |
| `highlight` | boolean | Colour fenced code blocks by language. Default `true`. |
| `images` | boolean | Draw the resources the document references. With this off, each one is left out of the page. Default `true`. |
| `smart_typography` | boolean | Set straight quotes, dashes and ellipses as their typographic forms. Default `true`. |
| `tasks` | boolean | Draw task list items as checkboxes. Default `true`. |
| `emoji` | boolean | Replace `:shortcode:` names with emoji. Default `true`. |
| `math` | boolean | Typeset mathematics written in LaTeX notation. Default `true`. |
| `barcode` | boolean | Draw barcode and QR code blocks. Default `true`. |
| `svg` | boolean | Draw vector resources as vectors instead of leaving them out. Default `true`. |
| `html_blocks` | boolean | Render HTML blocks and inline HTML with their styles. CSS applies only inside its own HTML block. Default `true`. |
| `bare_autolinks` | boolean | Turn bare URLs and email addresses into links. With this off they stay plain text, also in raster output. Default `true`. |
| `indented_code` | boolean | Render blocks indented by four spaces as code. Default `true`. |
| `embed_source` | boolean | Attach the Markdown source to the PDF as an embedded file named document.md. Off unless set to true. The free, watermarked delivery leaves it out. Default `false`. |
| `tagged` | boolean | Tag the PDF for screen readers: headings, paragraphs, lists, tables, links, and figures and formulas with their text alternatives. Off leaves the structure out. Default `true`. |
| `theme` | "light" \| "dark" | The page and text colors. A `theme: light` or `theme: dark` key in the document's front matter wins over this. Default `"light"`. |
| `hyphenation` | boolean | Hyphenate justified text. It needs a document `language`: without one the text is set unhyphenated whatever this says. |

```bash
curl -X POST https://api.legivel.com/v1/render \
  -H "Authorization: Bearer $LEGIVEL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"markdown":"# Quarterly report\n\n2026 Q3 closed with revenue up 12%.","features":{"theme":"dark","math":false,"mermaid_appendix":true}}' \
  -o document.pdf
```

## Response

A success is 200. With the default `output: "pdf"` the body is the PDF bytes under `Content-Type: application/pdf`, and `Content-Disposition` names the file. The ASCII form is always present; a name with characters outside ASCII additionally carries the RFC 8187 `filename*=` form.

| output | Media type | Body |
| --- | --- | --- |
| `"pdf"` | application/pdf | The document itself. |
| `"json"` | application/json | The answer to `output: "json"`: the document and the numbers that describe it. |
| `"upload"` | application/json | The answer to `output: "upload"`: the receipt for a document your storage accepted at `upload_url`. It is sent only after the storage answered the PUT with a 2xx status, and it does not repeat the URL. |

output: "pdf"

```text
HTTP/1.1 200 OK
content-type: application/pdf
content-disposition: attachment; filename="Quarterly report.pdf"
cache-control: no-store
server-timing: setup;dur=0.8, render;dur=41.2, serialize;dur=5.6, total;dur=63
x-legivel-pages: 3
x-legivel-delivery: vector
x-legivel-sha256: 9c1185a5c5e9fc54612808977ee8f548b2258d31c1e4e1b4a1ba5f0f1a7f0d2b

%PDF-1.7 ...
```

With `output: "json"` the body is JSON: `pdf` is the same bytes Base64-encoded, `bytes` their decoded length, `sha256` the digest the header also carries, and `signed` says whether a seal was applied. The digest is the same for both deliveries of one document, so the delivery mode never changes the file.

output: "json"

```json
{
  "pdf": "JVBERi0xLjcKJc...",
  "pages": 3,
  "bytes": 48213,
  "sha256": "9c1185a5c5e9fc54612808977ee8f548b2258d31c1e4e1b4a1ba5f0f1a7f0d2b",
  "signed": false
}
```

Apart from content type and disposition, all three deliveries carry the same headers:

| Header | Present | Description |
| --- | --- | --- |
| `Cache-Control` | Always | `no-store`: a rendered document is never cached by an intermediary. |
| `Server-Timing` | Always | The phases of the render and the total, as `total;dur=<milliseconds>` with the converter's own phase marks before it where it reports them. |
| `X-Legivel-Pages` | Always | Pages in the delivered document. |
| `X-Legivel-Delivery` | Always | What was served: `vector` for a full document, `raster` for the watermarked one. This endpoint always answers `vector`. |
| `X-Legivel-SHA256` | Always | The SHA-256 of the exact bytes delivered, in lower-case hexadecimal. The document is hashed after any seal is applied, so the digest is of the file as saved. |
| `X-Legivel-Signature` | When it applies | `signed` when a seal was applied and confirmed; absent otherwise. |
| `X-Legivel-Replay` | When it applies | `true` when the request replayed an earlier render under the same `Idempotency-Key`, so no document was spent. |
| `X-Legivel-Fetch-Id` | When it applies | The public identity the resource loads were made under. It names no account and no internal token. |
| `X-Legivel-Resources-Failed` | When it applies | `<failed>/<asked for>` when `resources: "lenient"` drew placeholders for resources that could not be loaded. |
| `X-Legivel-Resources-Failed-Ids` | When it applies | The ids of those resources, comma-separated, at most sixteen of them with `,+N` for the rest. |

API renders carry no watermark on any plan. Only the web editor's free live preview does.

## Examples

Send a markdown file as `text/markdown` and save the PDF. The key is read from the `LEGIVEL_API_KEY` environment variable, as in every example on this page.

```bash
curl -X POST https://api.legivel.com/v1/render \
  -H "Authorization: Bearer $LEGIVEL_API_KEY" \
  -H "Content-Type: text/markdown" \
  --data-binary @doc.md \
  -o doc.pdf
```

Sign in to run this sample against your own account. [Sign in](https://legivel.com/login)

The same call with a JSON body, for markdown that is already a string in your program.

```bash
curl -X POST https://api.legivel.com/v1/render \
  -H "Authorization: Bearer $LEGIVEL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"markdown": "# Hello"}' \
  -o hello.pdf
```

Name the file and set the document's language.

```bash
curl -X POST https://api.legivel.com/v1/render \
  -H "Authorization: Bearer $LEGIVEL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"markdown":"# Quarterly report\n\n2026 Q3 closed with revenue up 12%.","filename":"Quarterly report.pdf","language":"en-GB"}' \
  -o quarterly-report.pdf
```

Render a document with images linked from the web and accept placeholders for any that fail, reading the resources headers to see what was missing. Linked images are loaded for signed-in accounts with documents left and never stored; see [how we load images](https://legivel.com/fetcher).

```bash
curl -X POST https://api.legivel.com/v1/render \
  -H "Authorization: Bearer $LEGIVEL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"markdown":"# Quarterly report\n\n2026 Q3 closed with revenue up 12%.\n\n![Revenue](https://example.com/revenue.png)","resources":"lenient"}' \
  -o quarterly-report.pdf
```

Take the PDF as a JSON value, for a runtime that cannot read a binary response body.

```bash
curl -X POST https://api.legivel.com/v1/render \
  -H "Authorization: Bearer $LEGIVEL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"markdown":"# Quarterly report\n\n2026 Q3 closed with revenue up 12%.","output":"json"}' \
  -o render.json
```

Seal the result with a certificate stored in your account.

```bash
curl -X POST https://api.legivel.com/v1/render \
  -H "Authorization: Bearer $LEGIVEL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"markdown":"# Quarterly report\n\n2026 Q3 closed with revenue up 12%.","sign":{"certificate":"Acme certificate","reason":"Quarterly close"}}' \
  -o quarterly-report.pdf
```

## Upload to your storage

With `output: "upload"`, legivel sends the finished PDF to your own storage with one HTTP PUT to a pre-signed URL you create, and answers with a receipt instead of the file. Amazon S3, Cloudflare R2, Google Cloud Storage and any store that accepts a pre-signed PUT work. You sign the URL on your side with your own credentials. No access key, secret or bucket policy is sent to legivel; the URL is the only thing you share, and it authorizes one object.

- `output: "upload"` needs `upload_url`, and `upload_url` is refused with any other `output`. Either mistake answers 400 `invalid_request`.
- `upload_url` must be an absolute `https` URL on a public host. A host that resolves to a loopback, private, link-local or other non-public address, a blocked domain, a host with no IPv4 address, or a URL that would not be sent exactly as written is refused with 400 `invalid_request`. The host must resolve within 10 seconds. If the address check itself cannot run, the answer is 503 `quota_unavailable`. Both happen before a document is reserved, so a refused URL costs nothing.
- A deployment that has no outbound address for requests to your storage answers 503 `render_unavailable` to every `output: "upload"`, also before a document is reserved. `pdf` and `json` output are unaffected.
- The URL is used exactly as you sent it, query string included. It is never stored, logged or repeated in a response or an error message.

```bash
curl -X POST https://api.legivel.com/v1/render \
  -H "Authorization: Bearer $LEGIVEL_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"markdown\": \"# Quarterly report\", \"output\": \"upload\", \"upload_url\": \"$UPLOAD_URL\"}"
```

**Headers the URL may be signed for.** The PUT carries exactly two headers: `Content-Type: application/pdf` and `Content-Length` with the exact size of the PDF. Sign the URL for `Content-Type: application/pdf`, and for `Content-Length` only if you know the size in advance. legivel adds no storage-specific header, so a URL that requires a signed `x-amz-*` header, for example `x-amz-server-side-encryption` or a checksum header, fails with 403 at the storage. Use the bucket's default encryption instead.

The PUT is tried once and never follows a redirect. The connection to your storage must be established within 10 seconds, and the whole PUT must finish within 60 seconds. The plan's render time limit covers the render only, so a slow bucket does not turn into `render_timeout`.

**Receipt.** A 200 answer means your storage accepted the PDF with a 2xx status. `status` is that status, so you can tell a 200 from a 201 or a 204. `bytes` and `sha256` describe the exact bytes your storage received: the digest is the same one the `pdf` and `json` deliveries report for the same document, and for a sealed PDF it is taken after the seal. The receipt carries the same headers as the other deliveries, without `Content-Disposition`, and never includes the URL.

output: "upload"

```json
{
  "uploaded": true,
  "pages": 3,
  "bytes": 48213,
  "sha256": "9c1185a5c5e9fc54612808977ee8f548b2258d31c1e4e1b4a1ba5f0f1a7f0d2b",
  "status": 200
}
```

**Failed upload.** When your storage answers with anything other than 2xx, a redirect included, or does not answer in time, or the connection fails, the answer is 424 `upload_failed`. `status` is the storage's HTTP status, such as 403 for an expired signature, or `null` when no answer arrived. A failed upload returns the document to your period: you pay only for PDFs that reached your bucket.

upload_failed

```text
HTTP/1.1 424 Failed Dependency
content-type: application/json

{
  "code": "upload_failed",
  "error": "the storage at upload_url did not accept the PDF",
  "status": 403
}
```

**Retries.** A request repeated under the same `Idempotency-Key` renders and uploads again at no charge, within the plan's replay allowance described under [Idempotency](#idempotency). `upload_url` is not part of the key's fingerprint, so the repeat may carry a fresh pre-signed URL once the first one has expired. A repeat whose upload fails answers 424 and leaves the original charge as it was. A first attempt whose upload failed releases the key, so a retry under the same key is a first attempt again.

**Send an `Idempotency-Key` with every upload.** The render and the PUT together can take longer than 100 seconds on Pro and Scale. A connection that is closed on the way, by a proxy or by your own client, then ends without an answer, although the PDF may already be in your bucket and the document charged. Repeat the request under the same key: it uploads again at no charge and returns the receipt.

### Amazon S3

With the AWS SDK for JavaScript v3, pass `signableHeaders` so the content type is part of the signature; `requestChecksumCalculation: "WHEN_REQUIRED"` keeps the SDK from adding checksum parameters the PUT does not send. The credentials come from your own AWS environment.

```javascript
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

const s3 = new S3Client({
  region: "eu-central-1",
  requestChecksumCalculation: "WHEN_REQUIRED",
});

const uploadUrl = await getSignedUrl(
  s3,
  new PutObjectCommand({
    Bucket: "my-bucket",
    Key: "reports/2026-q3.pdf",
    ContentType: "application/pdf",
  }),
  { expiresIn: 900, signableHeaders: new Set(["content-type"]) },
);
```

### Cloudflare R2

R2 speaks the S3 API: the same code with your account's R2 endpoint, region `auto` and an R2 API token's access key.

```javascript
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

const r2 = new S3Client({
  region: "auto",
  endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
  credentials: {
    accessKeyId: process.env.R2_ACCESS_KEY_ID,
    secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
  },
  requestChecksumCalculation: "WHEN_REQUIRED",
});

const uploadUrl = await getSignedUrl(
  r2,
  new PutObjectCommand({
    Bucket: "my-bucket",
    Key: "reports/2026-q3.pdf",
    ContentType: "application/pdf",
  }),
  { expiresIn: 900, signableHeaders: new Set(["content-type"]) },
);
```

### Google Cloud Storage

A V4 signed URL with the `write` action is a PUT URL. The signing identity needs permission to write the object and to sign, for example a service account key.

```javascript
import { Storage } from "@google-cloud/storage";

const [uploadUrl] = await new Storage()
  .bucket("my-bucket")
  .file("reports/2026-q3.pdf")
  .getSignedUrl({
    version: "v4",
    action: "write",
    expires: Date.now() + 15 * 60 * 1000,
    contentType: "application/pdf",
  });
```

## Customer seals

On an eligible paid plan, ask legivel to seal the finished PDF on your instruction with your certificate. The seal makes later changes detectable and identifies the certificate used. Legivel does not claim qualified status, personal signing intent or equivalence to a handwritten signature. Reader validation depends on the certificate chain, trust settings and validation policy; uploading a certificate does not automatically establish trust.

Customer sealing is available through REST, MCP and the account-render backend. Editor and playground sealing controls are unavailable pending a separate redesign. Legivel does not provide a service certificate.

```json
{
  "markdown": "# Approved invoice",
  "sign": {
    "p12": "<Base64 PKCS#12/PFX bundle>",
    "password": "<bundle password>"
  }
}
```

- Inline `p12` must contain the matching private key and certificate chain, use standard Base64, and decode to at most 65,536 bytes. A public certificate alone cannot seal. `password` is required; an empty string is valid for an unprotected bundle. Inline credentials are used for this request and are never stored in your account or logs. Supply them again on every retry.
- `key` with `cert`, and optionally `chain`, carry the same credential as separate parts. Each is PEM text or standard Base64 of DER, at most 65,536 decoded bytes; a PEM `chain` may hold several certificates, leaf issuer first. RSA, ECDSA and Ed25519 keys are read, with the private key as PKCS#8 (plain or encrypted), PKCS#1 or SEC1. `password` is needed only for an encrypted key. These parts are treated exactly like an inline bundle: used for this request, never stored or logged.
- `certificate` is the name of a certificate uploaded to your account. Upload requires your authorisation to apply it to documents you submit on your behalf, until you delete it. Stored bundles and passwords are encrypted. A missing, deleted or expired certificate fails the request; another certificate is never substituted.
- Optional `reason`, `location` and `contact` inside` sign` each accept up to 200 characters without control characters. These fields describe the seal in the PDF; they do not change whose certificate is used.
- Certificate data counts towards the overall request-body ceiling, with its own decoded bundle cap. It does not count as Markdown text or image/resource data. No certificate chain, timestamp or revocation service is fetched for a customer seal.
- A refused credential answers 400 `invalid_bundle` with a reason (`wrong_password`, `no_private_key`, `cert_chain_incomplete`, `key_cert_mismatch`, `certificate_expired`, `certificate_not_yet_valid`, `unsupported_algorithm` or `iterations_exceeded`) that describes the problem without repeating what you sent. [Seal PDFs](https://legivel.com/docs/sealing#refusals) says what to check for each.
- Omitting `sign` or using `false` requests an unsealed PDF. `true` returns 400 `invalid_request`: name a stored certificate with `{"certificate": "<name>"}` or send the bundle in the request. Other unsupported forms and ambiguous combinations also return 400 `invalid_request`. A Free plan returns 403 `plan_required` for a valid sealing request.

Sealing during rollout

Per-request seal metadata and sealed retries need the resource-rendering backend, which is on unless a deployment opts out. Where it is off, plain customer-certificate seals work without metadata or an Idempotency-Key; requests for the additional guarantees return 503 `signing_unavailable`.

## Idempotency

Set `Idempotency-Key` to a value that identifies the request on your side, for example a document id plus a content hash. Use one key per document and set of output options. Keys are scoped to the account, so its API keys and its web downloads share one namespace. We never store your PDF: we keep a fingerprint of the request, the key and the outcome.

A later request with the same key and the same document is a replay. It renders the document again, answers 200 with `X-Legivel-Replay: true`, and costs no document.

- **Same key, same document:** a free replay, until your plan's replay allowance for that key is used up: 2 on Free, 5 on the paid plans. Past it the answer is 429 `idempotency_replay_limit`, and `Retry-After` gives the seconds until the key expires.
- **Same key, a different document:** 422 `idempotency_mismatch`. Nothing renders and nothing is charged. The document and the feature toggles are both part of what "same" means.
- **Same key, the first request still running:** 409 `idempotency_in_progress` with `Retry-After: 2`. Two requests under one key never both render.
- A key longer than 128 characters, or one that carries control characters, answers 400 `invalid_request`.
- A replay still passes authentication and the per-account limits (requests per minute and concurrent renders), so a burst of replays can answer 429 like any other burst.

A key lives 24 hours. After that the same key is a fresh request and costs a document again. Only a successful render completes a key: if the request fails (queue full, timeout, backend unavailable), the key is released at once and a retry with the same key is a first attempt again, charged like any other render.

**Sealed requests.** A request that asks for a customer seal and also sends an `Idempotency-Key` is refused with 503 `signing_unavailable`. A seal's identity is not yet part of the key's fingerprint, and we will not replay one account's seal for another request. Send a sealed request without the header.

## Errors

Errors are JSON with a stable `code` and a human `error` message. Invalid certificate bundles may include a content-free `reason`, such as` wrong_password`. This endpoint can answer with the codes below; the full list with recovery advice is on [Errors and quota](https://legivel.com/docs/errors), and the plan limits a refusal comes from are on [Limits](https://legivel.com/docs/limits).

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_bundle` | The supplied certificate cannot be used; check its reason and credentials. Compatibility mode may return `wrong_password` or `key_cert_mismatch` directly. |
| 400 | `unknown_signing_identity` | The named certificate is not available for this account. |
| 400 | `certificate_expired` | The stored certificate expired and must be replaced. |
| 403 | `plan_required` | The account plan does not include customer seals. |
| 422 | `signing_failed` | Sealing failed. No unsealed PDF is returned; the reserved document returns to your period. |
| 503 | `signing_unavailable` | The requested sealing capability or certificate service is unavailable. |
| 400 | `invalid_request` | An unknown field or feature key, a wrong field type, an `output` or `theme` outside its set, a malformed seal request, an unsupported seal form or an oversized bundle. The message names the offending key. Also an `upload_url` without `output: "upload"` or the reverse, and an `upload_url` that is not `https` or not on a public host; that message never repeats the URL. |
| 400 | `empty_document` | The markdown is zero bytes long. |
| 413 | `payload_too_large` | The body, the markdown or the resource budget is over the plan's ceiling. Nothing renders and nothing is charged. |
| 422 | `resource_fetch_failed` | A remote resource could not be loaded in `resources: "strict"` mode. Returned to your period. Send `resources: "lenient"` to render with placeholders instead. |
| 422 | `too_many_resources` | The document declares more remote resources than the plan allows. |
| 401 | `unauthorized` | Missing or invalid bearer token. |
| 402 | `quota_exhausted` | The period's documents are used up. |
| 422 | `render_timeout` | The render exceeded its deadline. Returned to your period. |
| 422 | `too_many_pages` | The document exceeds its configured page limit. Returned to your period. |
| 429 | `rate_limited` | Over the plan's requests per minute or concurrent renders. `Retry-After` says how many seconds to wait. |
| 422 / 500 | `render_failed` | The render worker failed. Returned to your period. |
| 503 | `queue_full` | The render queue is full. `Retry-After: 2`. Returned to your period. |
| 503 | `quota_unavailable` | The usage count or the audit storage is unavailable. The PDF is withheld; follow` Retry-After` when present. |
| 503 | `render_unavailable` | The render backend is not available. Returned to your period. No Retry-After header. |
| 424 | `upload_failed` | With `output: "upload"`, the storage did not accept the PDF or did not answer in time. `status` is the storage's HTTP status, or `null` when no answer arrived. Returned to your period. |

Failed renders are refunded

A document is reserved before generating the PDF. Conversion, sealing or upload failure returns that document to your period; successful outputs are recorded before delivery. An interrupted download can be retried using the applicable idempotency policy above.
