---
name: galley-render
description: Render PDFs, PNGs and JPGs from a template plus a JSON payload, and get back a signed URL. Use when a task needs a real document or image file — an invoice, quote, receipt, certificate, report, shipping label, OG card, social card, ticket, badge or slide deck — rather than text or HTML. Works with no API key for the first 50 renders.
license: Proprietary. See https://galleyrender.com/terms
---

# Galley Render

JSON in, PDF out. You send a template name and a data payload; you get a signed URL to a
finished PDF, PNG or JPG. Renders are deterministic and cached, so the same input always
returns the same file and the second identical call is free.

Use this instead of assembling a PDF yourself when the output has to be a real file a person
will download, attach, print or sign.

## Start here

**MCP (preferred).** Streamable HTTP, no authentication required:

```
https://mcp.galleyrender.com/mcp
```

Connect and call `render`. The first call with no API key mints a **50-render trial** and
returns its token in the response, under `trial.trial_token`. Send that token back as
`X-Galley-Api-Key` on later requests to stay on the same trial.

**HTTP.** The same operations are a plain REST API at `https://api.galleyrender.com`, with the
key in `Authorization: Bearer glr_sk_…`. Every MCP tool below names its curl equivalent.

## The shape of a job

1. `list_templates` — what exists on this account. The starter library is 22 templates:
   invoice, quote, change order, receipt, statement, purchase order, certificate, report cover,
   report table, one-page summary, shipping label, packing slip, OG card, social quote card,
   event ticket, badge, menu, price sheet, letterhead, chart card, slide deck (16:9 PDF) and
   single slide (1920x1080 PNG).
2. `get_template` — read the JSON Schema for the one you picked. **Do this before rendering an
   unfamiliar template.** The schema is the contract for `data`.
3. `validate_data` — optional, free, no render. Dry-run your payload and get field-level errors.
4. `render` — get the file.

If nothing fits, `create_template` with your own HTML, then render it. Changing a template
later is `update_template`, which publishes a new version rather than editing the old one.

## Rendering

```jsonc
// render
{
  "template": "invoice@3",              // pin the version in anything you ship
  "data": { "invoice_number": "INV-1042", "customer": { "name": "Acme" }, "line_items": [] },
  "format": "pdf",                       // pdf | png | jpg
  "options": { "page_size": "Letter", "margin": "18mm" }
}
```

```json
{
  "object": "render",
  "id": "rnd_7hq2m4x8k1bv",
  "status": "succeeded",
  "template": "invoice@3",
  "format": "pdf",
  "cached": false,
  "url": "https://api.galleyrender.com/v1/files/renders/…?expires=…&signature=…",
  "page_count": 2,
  "billable_units": 2
}
```

curl:

```bash
curl -sS https://api.galleyrender.com/v1/render \
  -H "Authorization: Bearer $GALLEY_API_KEY" \
  -H 'content-type: application/json' \
  -d '{"template":"invoice@3","format":"pdf","data":{"invoice_number":"INV-1042"}}'
```

**Small jobs finish inside the call** and come back `status: "succeeded"` with a `url` you can
hand straight to a user. **Anything with a `webhook_url`, `async: true` or a large payload**
comes back `status: "queued"` — poll `get_render` with the id, or wait for the webhook.

Signed URLs expire (an hour by default). The stored object does not: call `get_render` again
for a fresh URL rather than re-rendering.

### Formats and cost

| | Format | Billable |
|---|---|---|
| Documents, anything multi-page | `pdf` | 1 unit per page |
| Cards, images, previews | `png`, `jpg` | 1 unit |

Cache hits are never billed. `webp` is not supported.

### Options

`page_size` (`Letter`, `A4`, `Legal`), `landscape`, `margin` (`"18mm"` or per side), `width` and
`height` in px for raster output, `scale` 1–4, `full_page`, `quality` 1–100 for jpg,
`background`, `print_background`, `css`. Options are part of the cache key.

## Writing a template

One self-contained HTML document with inline CSS and Liquid expressions. No file includes:
everything must be in the string or at a public https URL.

```html
<html><head><style>
  @page { size: Letter; margin: 18mm }
  body { font: 14px/1.5 system-ui }
  .total { font-weight: 700 }
</style></head><body>
  <h1>Invoice {{ invoice_number }}</h1>
  <p>{{ customer.name }} — {{ issued_on | date_medium }}</p>
  <table>
    {% for line in line_items %}
      <tr><td>{{ line.description }}</td><td>{{ line.amount | money }}</td></tr>
    {% endfor %}
  </table>
  <p class="total">{{ total | money }}</p>
</body></html>
```

`money` and `date_medium` are the two extra filters. Everything else is standard Liquid.

**Always ship a JSON Schema with the template.** It is what turns a bad payload into a usable
error instead of a blank page:

```json
{
  "type": "object",
  "required": ["invoice_number", "total"],
  "properties": {
    "invoice_number": { "type": "string", "description": "Human-facing invoice number" },
    "total": { "type": "number", "description": "Grand total in dollars" }
  }
}
```

**Engines.** `chromium` (the default) is full HTML and CSS, and is required for PDF. `satori` is
a fast PNG path for simple flexbox card layouts — no page breaks, no floats, no external CSS.
Prefer `satori` for OG images and social cards.

**Versions are immutable.** `update_template` publishes `name@N+1`; `name@N` keeps rendering
exactly as it did, so anything pinned to it is unaffected.

## Errors

Every error names a stable `type`, a `docs_url`, and — for validation — the field path, the
expected type, what was received and a value that would be accepted. Fix the payload from the
error and retry; do not guess.

```json
{
  "error": {
    "type": "validation_error",
    "message": "The data payload does not match the template schema.",
    "docs_url": "https://galleyrender.com/docs/errors/validation_error",
    "errors": [
      {
        "path": "data.line_items[1].quantity",
        "message": "data.line_items[1].quantity must be number",
        "expected": "number",
        "received": "string",
        "example": 2
      }
    ]
  }
}
```

| `type` | HTTP | What to do |
|---|---|---|
| `validation_error` | 422 | Fix the named fields and retry. |
| `invalid_request` | 400 | The request itself is malformed; read `errors`. |
| `not_found` | 404 | `details.available_templates` lists what does exist. |
| `conflict` | 409 | The template name is taken — use `update_template`. |
| `authentication_error` | 401 | The key is missing or revoked. |
| `quota_exceeded` | 402 | Trial or free tier spent. Call `create_account`, or upgrade. |
| `spend_cap_exceeded` | 402 | Raise the monthly cap in the dashboard. |
| `rate_limited` | 429 | Too fast: 60 renders/min free, 600 paid, per key. Wait `Retry-After` seconds, then retry with backoff. |
| `asset_blocked` | 400 | An image or font URL failed the SSRF policy. Use a public https URL. |
| `render_failed` | 500 | The template threw. Check it against a smaller payload. |
| `internal_error` | 500 | Ours. Retry once, then report it. |

## Getting a key

The trial is 50 renders, total, per client. When you need more:

```jsonc
// create_account
{ "email": "dev@example.com" }
```

A verification link goes to that address. Call `create_account` again with the same email once
it has been clicked, and it returns the API key — once. Store it as `GALLEY_API_KEY`.

The trial is upgraded **in place**: the templates and renders made during it are kept. The free
tier is 200 renders a month; pricing beyond that is at <https://galleyrender.com/pricing>.

## Writing code rather than calling tools

If the task is to leave behind a program that renders documents, hand it a client rather than raw
HTTP. Both are typed from the same OpenAPI document and take the same key, including a trial token.

```bash
npm install galley-render        # Node 20+, zero runtime dependencies
pip install galley-render        # Python 3.9+, sync and async
```

```ts
import { Galley, startTrial } from "galley-render";

const trial = await startTrial({ clientId: "my-app" });   // 50 renders, no signup
const galley = new Galley({ apiKey: trial.apiKey });
const render = await galley.render({ template: "invoice@1", format: "pdf", data });
await galley.download(render, { toFile: "invoice.pdf" });
```

```python
from galley_render import Galley, start_trial

trial = start_trial(client_id="my-app")
galley = Galley(api_key=trial.api_key)
render = galley.render("invoice@1", format="pdf", data=data)
galley.download(render, to_file="invoice.pdf")
```

Both retry 429 and 5xx with backoff, re-sign an expired download URL, and raise the API's error
envelope intact — field path, expected type, what arrived, an accepted example — rather than a
bare status code. There is also an n8n community node (`n8n-nodes-galley-render`) and a Zapier
integration for workflow tools.

## Rules of thumb

- Pin the template version in anything you ship.
- Read the schema before rendering a template you have not used.
- Re-send the identical request rather than caching the file yourself; a cache hit is free and
  instant.
- Call `get_render` for a fresh URL instead of re-rendering an expired one.
- Check `usage` before a large batch.
- Do not render a document that impersonates a real company or person. Accounts that do are
  suspended.
