Skip to content

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.

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.

Render an invoice with no API key
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:

Response
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "{\n \"object\": \"render\",\n \"id\": \"rnd_7hq2m4x8k1bv\",\n …\n}"
}
]
}
}

Decoded, that text block is:

result.content[0].text
{
"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.

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.

POST /v1/render shell
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 }
      ]
    }
  }'
POST /v1/render javascript
// 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);
POST /v1/render python
# 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"])
200 OK
{
"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.

The trial is capped at 50 renders. To lift it, call the create_account MCP tool with an email address:

create_account
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" }
}
}'
  1. The first call returns status: "pending_verification" and mails a verification link.
  2. A human clicks the link.
  3. Call create_account again with the same email. It returns api_key — once. Store it as GALLEY_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.

If you want toRead
Understand the key headers and the 401 bodyAuthentication
See every field of the render requestPOST /v1/render
Render many documents in one callPOST /v1/render/batch
Ship your own HTML templatePOST /v1/templates
Check a payload before spending a renderPOST /v1/templates/:ref/validate
Know why a second identical call is freeCaching and signed URLs
Handle failures properlyErrors
Watch what you are spendingSpend caps, quotas and metering