galley-render
JSON in, PDF out. The official Python client for Galley Render — a document API for agents and the programs they write. A template plus a JSON payload becomes a PDF, PNG or JPG behind a signed URL, deterministically and cached, so the same input always returns the same file and an identical repeat call is free.
- Sync and async clients with the same surface.
- One dependency:
httpx. - Typed responses from the API's own OpenAPI spec,
with
.rawkept intact andpy.typedshipped. - Retries 429 and 5xx with exponential backoff and full jitter.
- Downloads signed URLs, and re-signs them when they expire.
- Starts a 50-render keyless trial with no signup and no card.
pip install galley-render
Python 3.9 or newer.
Quickstart
import os
from galley_render import Galley
galley = Galley(api_key=os.environ["GALLEY_API_KEY"])
render = galley.render(
"invoice@1",
format="pdf",
data={
"invoice_number": "INV-1042",
"customer": {"name": "Acme Corp"},
"line_items": [{"description": "Consulting", "quantity": 12, "unit_price": 150}],
},
)
print(render.url) # signed, short-lived
galley.download(render, to_file="invoice.pdf")
Async is the same thing with await:
from galley_render import AsyncGalley
async with AsyncGalley() as galley: # reads GALLEY_API_KEY
render = await galley.render("invoice@1", format="pdf", data=payload)
pdf = await galley.download(render)
No key yet
start_trial() mints a real 50-render account through Galley's MCP server and hands back a key that
works everywhere in this package and against the REST API.
from galley_render import Galley, start_trial
trial = start_trial(client_id="my-app") # reuse client_id to keep the same trial
print(trial.renders_remaining) # 50
galley = Galley(api_key=trial.api_key)
render = galley.render("og-card", format="png", data={"title": "Hello"})
async_start_trial() is the awaitable form. To lift the limit, call the create_account tool on
the MCP server with an email, or sign up at
galleyrender.com. The trial upgrades in place — nothing it made is lost.
Configuration
Galley(
api_key=None, # default: $GALLEY_API_KEY
base_url=None, # default: $GALLEY_BASE_URL, then the public API
timeout=60.0, # seconds, per attempt
max_retries=3, # extra attempts on 429/5xx and connection failures
headers=None, # merged into every request
http_client=None, # bring your own httpx.Client
)
Both clients are context managers, and close the httpx client they created:
with Galley() as galley:
...
Rendering
# Small jobs finish inside the call.
render = galley.render(
"certificate@2", # pin the version in anything you ship
format="pdf", # pdf | png | jpg
data={"recipient": "Dana Lee", "course": "Rope Access L1"},
options={"page_size": "Letter", "margin": "18mm", "landscape": True},
)
# A webhook, async_=True or a large payload queues the job instead.
queued = galley.render("report", data=data, async_=True)
done = galley.renders.wait(queued.id) # polls with backoff
# Or do both in one call, whichever path the API takes.
finished = galley.render_and_wait("report", data=data)
# Up to 50 at a time. A bad item fails alone; the rest still run.
batch = galley.renders.batch(
[{"template": "statement@4", "data": c} for c in customers],
webhook_url="https://example.com/hooks/galley",
)
batch.succeeded # the Render objects
batch.failed # the error envelopes, each with its index
render.cached is True when the deterministic cache answered: the same template version, data,
options and format were rendered before, and this call cost nothing.
Downloading
Signed URLs are short-lived; the stored file is not. download() takes a render, a render id or a
URL, and quietly re-signs an expired one.
data = galley.download(render) # bytes
galley.download(render, to_file="out/invoice.pdf")
galley.download("rnd_7hq2m4x8k1bv", to_file="a.png")
Templates
Templates are code: one self-contained HTML document with inline CSS and Liquid expressions, plus a
JSON Schema that is the contract for data. Versions are immutable and content-addressed.
galley.templates.list()
invoice = galley.templates.get("invoice@3")
invoice.schema # JSON Schema for `data`
invoice.example # a payload that renders
galley.templates.create(
"welcome-card",
engine="satori", # fast PNG path for simple flexbox cards
source="<div style='display:flex'>{{ name }}</div>",
schema={"type": "object", "required": ["name"], "properties": {"name": {"type": "string"}}},
example={"name": "Dana"},
)
galley.templates.publish("welcome-card", source="…", message="tighter kerning")
galley.templates.versions("welcome-card")
# Free, renders nothing, and returns the same field errors a render would.
check = galley.templates.validate("welcome-card", {"name": 42})
if not check:
for field in check.errors:
print(field) # name: must be string (expected string, got number)
Usage
usage = galley.usage()
usage.billable_units # 1 per PNG/JPG, 1 per PDF page; cache hits are free
usage.free_renders_remaining
usage.spend_remaining_usd
usage.trial # not None only on a keyless trial
Errors
Every failure is a GalleyError carrying the API's own envelope: a stable type, a docs_url and,
for validation, the field path, the expected type, what arrived and a value that would be accepted.
from galley_render import GalleyError, GalleyConnectionError, GalleyTimeoutError
try:
galley.render("invoice", data={})
except GalleyError as err:
err.type # "validation_error"
err.status # 422
err.retryable # False
err.request_id # quote this in a support mail
for field in err.errors:
print(field.path, field.message, field.expected, field.received, field.example)
| Type | Status | What to do |
|---|---|---|
validation_error |
422 | Fix the named fields. err.errors says exactly which. |
invalid_request |
400 | The request shape is wrong, not the data. |
authentication_error |
401 | Missing or bad key. |
not_found |
404 | No such template, version or render on this account. |
quota_exceeded |
402 | Trial or free tier spent. |
spend_cap_exceeded |
402 | The account's monthly cap. Raise it in the dashboard. |
rate_limited |
429 | Retried for you. |
asset_blocked |
400 | An image or font URL failed the SSRF policy; use a public https URL. |
render_failed |
500 | The template threw. err.body has the detail. |
GalleyConnectionError means no HTTP response at all — DNS, TLS, timeout. GalleyTimeoutError
means wait() gave up while the render was still queued; the render is not lost, so poll again or
take the webhook.
Also
- Node SDK — same surface, zero dependencies.
- MCP server — the same operations as agent tools, no key needed to start.
- Docs · API reference · Template language
MIT licensed. Support: support@galleyrender.com.
Release files for galley-render 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| galley_render-0.1.0.tar.gz | 22.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| galley_render-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 42.8 kB
Release files / galley_render-0.1.0.tar.gz
| Download URL | galley_render-0.1.0.tar.gz |
|---|---|
| Size | 22.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0d2fdfb532132dd482dc81252602a23d009fee5ee2d8eecb2817f8a0de2eb65d
|
|
BLAKE2b-256 checksum How to use checksums |
4216b80bfdeec3bd4f1476e6d4efb387f18912cd48064e1481983be034b8be26
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.
Transparency logRelease files / galley_render-0.1.0-py3-none-any.whl
| Download URL | galley_render-0.1.0-py3-none-any.whl |
|---|---|
| Size | 20.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fe514784101e0a2b3d1ca58767bdfaefaef9ed486a9ce0af369440f68f5aa0e0
|
|
BLAKE2b-256 checksum How to use checksums |
4f633dd935fbc50cc53ac14ff6a7d668e97d0b3d25e0cb912bf6a94545710a1a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.
Transparency log