Skip to content

SDKs and clients

There is no official SDK yet. The galley-render names on npm and PyPI are reserved but nothing has been published to them. If you find a package by that name today, it is not ours — do not install it.

That is a smaller gap than it sounds. The API is nine JSON endpoints with one bearer header and one error envelope; the clients below are complete, dependency-free, and about a screen each. When packages do ship, they will wrap exactly these calls.

Three ways to talk to Galley in the meantime:

RouteGood for
Paste a small clientApplication code. Start here.
Generate one from the OpenAPI documentTyped clients, unusual languages, large surfaces
MCPAgents, and anything that would rather call tools than endpoints

Node 22 has fetch built in, so this needs nothing installed.

galley.js — Node 22+, no dependencies
const BASE = "https://api.galleyrender.com";
export class GalleyError extends Error {
constructor(status, body) {
super(body?.error?.message ?? `Galley request failed with ${status}`);
this.name = "GalleyError";
this.status = status;
this.type = body?.error?.type ?? "internal_error";
this.docsUrl = body?.error?.docs_url;
/** Field errors: [{ path, message, expected, received, example }] */
this.errors = body?.error?.errors ?? [];
this.details = body?.error?.details;
}
}
export class Galley {
constructor(apiKey = process.env.GALLEY_API_KEY, baseUrl = BASE) {
if (!apiKey) throw new Error("Set GALLEY_API_KEY or pass a key.");
this.apiKey = apiKey;
this.baseUrl = baseUrl;
}
async request(method, path, body) {
const res = await fetch(`${this.baseUrl}${path}`, {
method,
headers: {
authorization: `Bearer ${this.apiKey}`,
...(body === undefined ? {} : { "content-type": "application/json" }),
},
body: body === undefined ? undefined : JSON.stringify(body),
});
const text = await res.text();
const parsed = text ? JSON.parse(text) : {};
if (!res.ok) throw new GalleyError(res.status, parsed);
return parsed;
}
render(body) {
return this.request("POST", "/v1/render", body);
}
renderBatch(renders, webhookUrl) {
return this.request("POST", "/v1/render/batch", { renders, webhook_url: webhookUrl });
}
getRender(id) {
return this.request("GET", `/v1/renders/${encodeURIComponent(id)}`);
}
listRenders(limit = 25) {
return this.request("GET", `/v1/renders?limit=${limit}`);
}
listTemplates() {
return this.request("GET", "/v1/templates");
}
getTemplate(ref) {
return this.request("GET", `/v1/templates/${encodeURIComponent(ref)}`);
}
createTemplate(body) {
return this.request("POST", "/v1/templates", body);
}
publishVersion(name, body) {
return this.request("POST", `/v1/templates/${encodeURIComponent(name)}/versions`, body);
}
validate(ref, data) {
return this.request("POST", `/v1/templates/${encodeURIComponent(ref)}/validate`, { data });
}
usage() {
return this.request("GET", "/v1/usage");
}
/** Polls a queued render until it finishes. Returns the final render object. */
async wait(id, { intervalMs = 1000, timeoutMs = 120_000 } = {}) {
const deadline = Date.now() + timeoutMs;
for (;;) {
const render = await this.getRender(id);
if (render.status === "succeeded" || render.status === "failed") return render;
if (Date.now() > deadline) throw new Error(`Render ${id} did not finish in ${timeoutMs} ms.`);
await new Promise((r) => setTimeout(r, intervalMs));
}
}
}
Using it
import { writeFile } from "node:fs/promises";
import { Galley, GalleyError } from "./galley.js";
const galley = new Galley();
try {
const check = await galley.validate("invoice@1", { invoice_number: "INV-1042" });
if (!check.valid) {
for (const e of check.errors) console.error(`${e.path}: expected ${e.expected}, got ${e.received}`);
}
let render = await galley.render({
template: "invoice@1",
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: "September", quantity: 1, unit_price: 19 }],
},
});
if (render.status !== "succeeded") render = await galley.wait(render.id);
const bytes = await fetch(render.url).then((r) => r.arrayBuffer());
await writeFile("invoice.pdf", Buffer.from(bytes));
console.log(`${render.page_count} page(s), ${render.billable_units} unit(s), cached: ${render.cached}`);
} catch (err) {
if (err instanceof GalleyError) {
console.error(err.type, err.message, err.docsUrl);
for (const e of err.errors) console.error(` ${e.path}: ${e.message} (example: ${JSON.stringify(e.example)})`);
} else {
throw err;
}
}

Standard library only — urllib is enough.

galley.py — Python 3.9+, no dependencies
import json
import os
import time
import urllib.error
import urllib.request
BASE = "https://api.galleyrender.com"
class GalleyError(Exception):
def __init__(self, status, body):
err = (body or {}).get("error", {})
super().__init__(err.get("message", f"Galley request failed with {status}"))
self.status = status
self.type = err.get("type", "internal_error")
self.docs_url = err.get("docs_url")
self.errors = err.get("errors", [])
self.details = err.get("details")
class Galley:
def __init__(self, api_key=None, base_url=BASE):
self.api_key = api_key or os.environ["GALLEY_API_KEY"]
self.base_url = base_url
def request(self, method, path, body=None):
data = None if body is None else json.dumps(body).encode()
req = urllib.request.Request(self.base_url + path, data=data, method=method)
req.add_header("authorization", f"Bearer {self.api_key}")
if data is not None:
req.add_header("content-type", "application/json")
try:
with urllib.request.urlopen(req, timeout=60) as res:
return json.loads(res.read() or b"{}")
except urllib.error.HTTPError as e:
raw = e.read()
raise GalleyError(e.code, json.loads(raw) if raw else None) from None
def render(self, **body):
return self.request("POST", "/v1/render", body)
def render_batch(self, renders, webhook_url=None):
return self.request("POST", "/v1/render/batch", {"renders": renders, "webhook_url": webhook_url})
def get_render(self, render_id):
return self.request("GET", f"/v1/renders/{render_id}")
def list_renders(self, limit=25):
return self.request("GET", f"/v1/renders?limit={limit}")
def list_templates(self):
return self.request("GET", "/v1/templates")
def get_template(self, ref):
return self.request("GET", f"/v1/templates/{ref}")
def create_template(self, **body):
return self.request("POST", "/v1/templates", body)
def publish_version(self, name, **body):
return self.request("POST", f"/v1/templates/{name}/versions", body)
def validate(self, ref, data):
return self.request("POST", f"/v1/templates/{ref}/validate", {"data": data})
def usage(self):
return self.request("GET", "/v1/usage")
def wait(self, render_id, interval=1.0, timeout=120.0):
deadline = time.time() + timeout
while True:
render = self.get_render(render_id)
if render["status"] in ("succeeded", "failed"):
return render
if time.time() > deadline:
raise TimeoutError(f"Render {render_id} did not finish in {timeout}s.")
time.sleep(interval)
Using it
import urllib.request
from galley import Galley, GalleyError
galley = Galley()
try:
render = galley.render(
template="invoice@1",
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": "September", "quantity": 1, "unit_price": 19}],
},
)
if render["status"] != "succeeded":
render = galley.wait(render["id"])
urllib.request.urlretrieve(render["url"], "invoice.pdf")
print(render["page_count"], "page(s),", render["billable_units"], "unit(s), cached:", render["cached"])
except GalleyError as err:
print(err.type, err, err.docs_url)
for e in err.errors:
print(" ", e["path"], e["message"], "example:", e.get("example"))

If requests is already in the project, the transport collapses to a few lines:

requests version of the same client
import os
import requests
class Galley:
def __init__(self, api_key=None, base_url="https://api.galleyrender.com"):
self.base_url = base_url
self.session = requests.Session()
self.session.headers["authorization"] = f"Bearer {api_key or os.environ['GALLEY_API_KEY']}"
def request(self, method, path, body=None):
res = self.session.request(method, self.base_url + path, json=body, timeout=60)
payload = res.json() if res.content else {}
if not res.ok:
raise GalleyError(res.status_code, payload)
return payload
def render(self, **body):
return self.request("POST", "/v1/render", body)

The API publishes its own OpenAPI document at https://api.galleyrender.com/openapi.json. For types without a runtime, a full client in another language, or Pydantic models, see OpenAPI.

Types for TypeScript
npx openapi-typescript https://api.galleyrender.com/openapi.json -o galley.d.ts

If the caller is an agent, skip the HTTP client entirely. https://mcp.galleyrender.com/mcp is a stateless Streamable HTTP MCP server exposing ten tools — list_templates, get_template, create_template, update_template, validate_data, render, get_render, list_renders, usage, create_account — with descriptions and input schemas attached. It needs no API key to start: the first render mints a 50-render trial and hands back the token.

Most MCP clients want this
{
"mcpServers": {
"galley-render": {
"type": "http",
"url": "https://mcp.galleyrender.com/mcp",
"headers": { "X-Galley-Api-Key": "glr_sk_…" }
}
}
}

Omit headers entirely to run on the trial. A raw tools/call over curl is in the Quickstart. The MCP endpoint sends CORS headers, so it is also the route for anything running in a browser — the REST API is not callable from page JavaScript.

When the packages ship they will be announced on llms.txt and this page. Until then, if you want a signature-compatible head start, keep your own client behind a thin interface: one request method and one error class, exactly as above.