A galley proof is the first rendered page

Documents
for agents.

JSON in, PDF out. Every render deterministic, cached, and signed.

One call, both halves
you send
POST /v1/render

{
  "template": "invoice@1",
  "format":   "pdf",
  "data":     { … }
}
you get back
200 OK

{
  "id":             "rnd_7hq2m4x8k1bv",
  "status":         "succeeded",
  "url":            "https://…?signature=…",
  "page_count":     1,
  "billable_units": 1,
  "cached":         false
}

Send it again unchanged and cached is true: same file, no render, no charge.

Keyless trial
50 renders
Free tier
200/month
Pay as you go
$0.006/PNG · $0.015/PDF page
Starter library
22 templates

Live · runs against api.galleyrender.com

Press this and a real PDF comes back.

No key, no signup, no card. Your browser calls the production MCP endpoint, which mints a 50-render trial on the spot. Edit the JSON if you like — the template's schema will tell you exactly what it wants.

payload.json invoice@1

Ready. The first render mints your trial.

A rendered invoice: seller and buyer blocks, two line items, tax and total.
Example output, rendered by Galley from the payload on the left.
status
cached
pages
units
round trip
trial left
50
Response, verbatim
Press Render to see the real response.

Three ways in

Point an agent at it, or just call the endpoint.

The MCP server is the wedge: an agent connects, calls render, and gets a 50-render trial without asking anyone for anything. When you are ready to put it in a codebase, it is an ordinary REST API with a bearer token.

Both surfaces hit the same renderer, the same cache and the same template store. A template your agent created in an MCP session is the template your production code renders.

MCP shell + json
# Claude Code
claude mcp add --transport http galley https://mcp.galleyrender.com/mcp

# Cursor — .cursor/mcp.json
{
  "mcpServers": {
    "galley": { "url": "https://mcp.galleyrender.com/mcp" }
  }
}

Streamable HTTP, stateless, no auth to begin. Ten tools — see the MCP reference.

curl shell
curl -sS https://api.galleyrender.com/v1/render \
  -H "Authorization: Bearer $GALLEY_API_KEY" \
  -H 'content-type: application/json' \
  -d '{
    "template": "invoice@1",
    "format": "pdf",
    "data": {
      "invoice_number": "INV-1042",
      "issued_on": "2026-09-16",
      "seller": { "name": "Galley Render" },
      "buyer":  { "name": "Acme Robotics" },
      "line_items": [
        { "description": "Starter plan, September", "quantity": 1, "unit_price": 19 }
      ]
    }
  }'

Small jobs finish inside the request and come back 200 with a URL. See POST /v1/render.

Node javascript
// Node 22+. No dependencies.
const res = await fetch("https://api.galleyrender.com/v1/render", {
  method: "POST",
  headers: {
    authorization: `Bearer ${process.env.GALLEY_API_KEY}`,
    "content-type": "application/json",
  },
  body: JSON.stringify({
    template: "invoice@1",
    format: "pdf",
    data: {
      invoice_number: "INV-1042",
      issued_on: "2026-09-16",
      seller: { name: "Galley Render" },
      buyer: { name: "Acme Robotics" },
      line_items: [{ description: "Starter plan", quantity: 1, unit_price: 19 }],
    },
  }),
});

const render = await res.json();
if (!res.ok) throw new Error(render.error.message);

console.log(render.url);        // signed, expires in an hour
console.log(render.page_count); // 1
console.log(render.cached);     // false the first time, true after

No SDK needed. The published galley-render packages are on the way — status here.

Python python
# Python 3.9+. Standard library only.
import json, os, urllib.request

body = json.dumps({
    "template": "invoice@1",
    "format": "pdf",
    "data": {
        "invoice_number": "INV-1042",
        "issued_on": "2026-09-16",
        "seller": {"name": "Galley Render"},
        "buyer": {"name": "Acme Robotics"},
        "line_items": [{"description": "Starter plan", "quantity": 1, "unit_price": 19}],
    },
}).encode()

req = urllib.request.Request(
    "https://api.galleyrender.com/v1/render",
    data=body,
    headers={
        "Authorization": f"Bearer {os.environ['GALLEY_API_KEY']}",
        "Content-Type": "application/json",
    },
)

render = json.load(urllib.request.urlopen(req))
print(render["url"], render["page_count"], render["cached"])

Swap urllib for requests or httpx if you already have one.

What "deterministic" buys you

The same input returns the same file

The cache key is a SHA-256 over the template version's checksum, the canonical JSON of your data, the canonical JSON of your options, and the format. Key order and whitespace do not change it. A repeat call is a database lookup.

How the cache key is built →

A cache hit is free

Billing is one unit per PNG or JPG and one per PDF page. A cache hit renders nothing, so it costs nothing — to you or to us. Retry loops and idempotent workers stop being expensive.

What a month actually costs →

Versions are immutable

Publishing a change creates invoice@4. invoice@3 keeps rendering byte-for-byte as it did, so a document you generated last quarter can be regenerated this quarter and match.

Versioning →

Starter library

22 templates, each with a schema and an example payload.

See all 22

Every one is a plain HTML document you can read, copy and fork. Loaded into your account the moment it exists — including a keyless trial account.

Billing and money · Operations and fulfilment · Reports and correspondence · Cards and images · Presentations

Next

Go from nothing to a signed PDF in under a minute.

No account, no card, no waiting list. When the trial runs out, one MCP call with an email address turns it into a real account and keeps everything you made.