Quickstart
Galley Render turns a template plus a JSON payload into a PDF, PNG or JPG behind a signed URL.
There are two ways in. Path A needs nothing at all: the MCP endpoint mints a 50-render trial on your first call. Path B is the same render over the REST API once you have a key.
Path A — one curl, no key
Section titled “Path A — one curl, no key”The MCP server is stateless Streamable HTTP, so a single tools/call request works on its own.
There is no initialize handshake to do first, and no session to keep. Two headers matter:
content-type: application/json and an accept that lists both JSON and SSE.
curl -sS https://mcp.galleyrender.com/mcp \ -H 'content-type: application/json' \ -H 'accept: application/json, text/event-stream' \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "render", "arguments": { "template": "invoice", "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 } ] } } } }'The answer is a JSON-RPC envelope. The tool result is one text block whose body is JSON:
{ "jsonrpc": "2.0", "id": 1, "result": { "content": [ { "type": "text", "text": "{\n \"object\": \"render\",\n \"id\": \"rnd_7hq2m4x8k1bv\",\n …\n}" } ] }}Decoded, that text block is:
{ "object": "render", "id": "rnd_7hq2m4x8k1bv", "status": "succeeded", "template": "invoice@1", "format": "pdf", "engine": "chromium", "cached": false, "url": "https://galley-renders.r2.cloudflarestorage.com/renders/acct_9k2p…/2026/09/4f1c…d0.pdf?X-Amz-Expires=3600&X-Amz-Signature=…", "expires_at": "2026-10-16T14:02:11.804Z", "page_count": 1, "billable_units": 1, "byte_size": 48213, "content_type": "application/pdf", "created_at": "2026-09-16T14:02:10.119Z", "completed_at": "2026-09-16T14:02:11.804Z", "error": null, "trial": { "mode": "keyless_trial", "renders_limit": 50, "renders_remaining": 49, "trial_token": "glr_sk_8Qd1nR7xKpV2sYbL4mTf9Uae", "keep_this_token": "Send it back as the `X-Galley-Api-Key` header (or `Authorization: Bearer`) on later MCP requests, and it works as an ordinary API key against https://api.galleyrender.com too.", "to_remove_the_limit": "Call `create_account` with an email. The trial is upgraded in place, so these templates and renders are kept." }}Open url and you have the PDF.
Path B — the same render over REST
Section titled “Path B — the same render over REST”Every MCP tool has a REST equivalent at https://api.galleyrender.com. The trial token works
there unchanged, so you can run this immediately with the token from Path A.
curl -sS https://api.galleyrender.com/v1/render \
-H "Authorization: Bearer $GALLEY_API_KEY" \
-H 'content-type: application/json' \
-d '{
"template": "invoice",
"format": "pdf",
"options": { "page_size": "Letter", "margin": "0.5in" },
"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 }
]
}
}' // Node 22+. No dependencies — `fetch` is built in.
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",
format: "pdf",
options: {
page_size: "Letter",
margin: "0.5in"
},
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
}
]
}
}),
});
const render = await res.json();
if (!res.ok) throw new Error(render.error.message);
console.log(render.url); # Python 3.9+. Standard library only.
import json, os, urllib.request
body = json.dumps({
"template": "invoice",
"format": "pdf",
"options": {
"page_size": "Letter",
"margin": "0.5in"
},
"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
}
]
}
}).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"]) { "object": "render", "id": "rnd_2bk9wx4m7q1h", "status": "succeeded", "template": "invoice@1", "format": "pdf", "engine": "chromium", "cached": false, "url": "https://galley-renders.r2.cloudflarestorage.com/renders/acct_9k2p…/2026/09/9ab3…71.pdf?X-Amz-Expires=3600&X-Amz-Signature=…", "expires_at": "2026-10-16T14:05:44.210Z", "page_count": 1, "billable_units": 1, "byte_size": 48213, "content_type": "application/pdf", "created_at": "2026-09-16T14:05:43.002Z", "completed_at": "2026-09-16T14:05:44.210Z", "error": null}Run it a second time with the identical body and you get "cached": true, instantly, for free.
Two expiries live in that response and they are not the same thing. The signature inside url
lasts an hour; expires_at is when the stored object itself stops being kept. Call
GET /v1/renders/:id for a fresh URL rather than re-rendering.
Get a permanent key
Section titled “Get a permanent key”The trial is capped at 50 renders. To lift it, call the create_account MCP tool with an email
address:
curl -sS https://mcp.galleyrender.com/mcp \ -H 'content-type: application/json' \ -H 'accept: application/json, text/event-stream' \ -H "X-Galley-Api-Key: $GALLEY_TRIAL_TOKEN" \ -d '{ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "create_account", "arguments": { "email": "dev@example.com" } } }'- The first call returns
status: "pending_verification"and mails a verification link. - A human clicks the link.
- Call
create_accountagain with the same email. It returnsapi_key— once. Store it asGALLEY_API_KEY.
The trial is upgraded in place: the templates and renders you made during it are kept, and the lifetime cap is lifted. The free tier is 200 renders a month and 3 templates; see pricing for everything beyond that.
What to read next
Section titled “What to read next”| If you want to | Read |
|---|---|
| Understand the key headers and the 401 body | Authentication |
| See every field of the render request | POST /v1/render |
| Render many documents in one call | POST /v1/render/batch |
| Ship your own HTML template | POST /v1/templates |
| Check a payload before spending a render | POST /v1/templates/:ref/validate |
| Know why a second identical call is free | Caching and signed URLs |
| Handle failures properly | Errors |
| Watch what you are spending | Spend caps, quotas and metering |