Skip to main content

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 .raw kept intact and py.typed shipped.
  • 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

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)

Source distribution for galley-render 0.1.0
File Size Uploaded
galley_render-0.1.0.tar.gz 22.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for galley-render 0.1.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page