Engines
Every template version is stored with an engine. It decides what CSS you can write, what output
formats you can ask for, and how long a render takes.
chromium | satori | |
|---|---|---|
| Default | yes | opt in with "engine": "satori" |
| Output formats | pdf, png, jpeg/jpg | png |
Default format when you omit it | pdf | png |
| CSS | Everything a current Chromium supports | A flexbox subset, inline style= attributes only |
| Layout | Flexbox, grid, floats, tables, multi-column, absolute positioning | Flexbox and absolute positioning |
| Pagination | @page, page breaks, repeating <thead> | None — one fixed-size canvas |
| JavaScript in the template | Runs before capture | Not executed |
| Web fonts | Any public https:// font or Google Fonts import | Bundled Inter 400/600/700 only |
| Images | Fetched as subresources | Inlined as data URIs before layout |
| Process | A headless browser page | Pure Node: HTML → SVG → PNG |
| Speed | Hundreds of milliseconds and up | Tens of milliseconds |
| Price to you | $0.006 per PNG/JPG, $0.015 per PDF page | $0.006 per PNG |
Price per unit is the same on both. The difference is latency and cost to run: Satori needs no browser process, which is why the card-shaped starters use it and why you should too.
How the engine is picked
Section titled “How the engine is picked”There is no engine field on a render request. The engine comes from the template version, and
the dispatch rule is exact:
- If the version’s engine is
satoriand the requested format ispng, the Satori path runs. - Otherwise — any PDF, any JPEG, and any
chromiumtemplate — Chromium runs.
The second rule is a deliberate fallback. Ask a Satori template for a PDF and you get one: the same Liquid-rendered markup is handed to Chromium, which is perfectly happy with inline styles. You do not have to rewrite a card to print it.
# Satori path: engine=satori, format=pngcurl -sS https://api.galleyrender.com/v1/render \ -H "Authorization: Bearer $GALLEY_API_KEY" -H 'content-type: application/json' \ -d '{"template":"og-card","format":"png","data":{"title":"Documents for agents"}}'
# Chromium path: same template, PDF asked forcurl -sS https://api.galleyrender.com/v1/render \ -H "Authorization: Bearer $GALLEY_API_KEY" -H 'content-type: application/json' \ -d '{"template":"og-card","format":"pdf","data":{"title":"Documents for agents"}}'Chromium
Section titled “Chromium”A single headless Chromium with a bounded page pool. Each render gets a fresh incognito context, so cookies, storage and service workers never survive from one render to the next.
- The document is loaded with
setContentand waited toload, thendocument.fonts.readyis awaited so text is measured against the real face. - Media is emulated:
printfor PDF,screenfor PNG and JPEG.@media printblocks apply to PDFs and are ignored for raster output. options.cssis injected after the template’s own styles.- Every subresource is routed through the SSRF-safe asset fetcher.
data:,about:andblob:URLs pass straight through; anything that is nothttp(s)—file://above all — is blocked and reported asasset_blocked. - Service workers are blocked and CSP is honoured.
Raster output
Section titled “Raster output”| Option | Default | Range |
|---|---|---|
width | 1200 | 1–8000 px |
height | 630 | 1–8000 px |
scale | 1 | clamped to 0.1–3, applied as the device pixel ratio |
full_page | true | set false to capture only the viewport |
quality | 85 | JPEG only, 1–100 |
scale is a real device pixel ratio, not an upscale: width: 1200, scale: 2 produces a genuine
2400 px bitmap. It is capped at 3 for raster so that one render cannot allocate an unbounded
image.
Note that full_page defaults to true, so a document taller than height produces a taller
image. Set "full_page": false when you need exactly width × height.
PDF output
Section titled “PDF output”| Option | Default | Notes |
|---|---|---|
page_size | Letter | A4, Legal, … Setting it overrides the CSS @page { size: … }. Leave it unset to let the CSS decide. |
landscape | false | |
margin | 0.5in each side | String or { top, right, bottom, left }. |
print_background | true | |
scale | 1 | CSS zoom, clamped 0.1–2 for PDF. |
Page count is read back out of the produced PDF, and each page is one billable unit. A three-page invoice costs three units.
Satori
Section titled “Satori”Satori turns a flexbox subset into an SVG, and resvg rasterises that SVG to PNG. There is no browser, no layout engine beyond flexbox, and no script execution.
What Satori accepts
Section titled “What Satori accepts”| Supported | Not supported |
|---|---|
display: flex, display: none | display: block, inline, grid, table |
flex-direction, justify-content, align-items, flex-wrap, flex-grow, gap | floats, multi-column |
position: absolute / relative, top/right/bottom/left | position: fixed, sticky |
width, height, min-*, max-*, padding, margin | percentage heights that depend on content |
background-color, linear and radial gradients, background-image with a URL | background-attachment, blend modes |
color, font-size, font-weight, font-family, line-height, letter-spacing, text-align, text-transform, text-overflow | @font-face, @import, external stylesheets |
border, border-radius, box-shadow, opacity, transform | pseudo-elements (::before, ::after), pseudo-classes |
<img>, <svg> | <canvas>, <video>, <iframe>, <script> |
| — | @page, page breaks, @media queries — a Satori render is one fixed canvas |
Two rules cause almost every Satori failure:
Three weights of Inter ship with the renderer and are the only faces available:
| Family | Weights |
|---|---|
Inter | 400, 600, 700 (all normal style) |
Fonts are embedded in the SVG, so glyphs are outlines in the output — no system font lookup, and
no italic synthesis. Ask for font-weight: 500 and Satori snaps to the nearest bundled weight.
If you need another typeface, use the chromium engine.
Images and options
Section titled “Images and options”Remote <img src="https://…"> tags are fetched through the asset fetcher and inlined as data
URIs before layout, because Satori itself cannot make network requests. The same SSRF policy
applies: public https:// only, and a refused image fails the render with
asset_blocked — exactly as it does on chromium. Set
options.on_blocked_asset: "skip" to draw a transparent
pixel in its place instead.
| Option | Default | Notes |
|---|---|---|
width | 1200 | 1–4000 px. The SVG canvas width. |
height | 630 | 1–4000 px. Fixed — content does not grow the canvas. |
scale | 1 | 0.1–3, the same cap as chromium. The PNG is rasterised at width × scale. |
background | rgba(0,0,0,0) | Painted under the SVG. Transparent by default, so set it if you want an opaque PNG. |
page_size, margin, landscape, full_page, quality and print_background have no effect
on the Satori path.
The same card on both engines
Section titled “The same card on both engines”Here is one 1200×630 card written twice. First the Satori version — this is the real og-card starter, trimmed:
<div style="display: flex; flex-direction: column; justify-content: space-between; width: 1200px; height: 630px; padding: 72px; background-color: {{ background | default: '#0b0f14' }}; font-family: Inter;"> <div style="display: flex; align-items: center;"> <div style="display: flex; width: 16px; height: 16px; border-radius: 8px; background-color: {{ accent | default: '#2dd4bf' }};"></div> <div style="display: flex; margin-left: 16px; font-size: 24px; color: {{ accent | default: '#2dd4bf' }}; letter-spacing: 2px; text-transform: uppercase;">{{ eyebrow | default: 'Galley Render' }}</div> </div> <div style="display: flex; flex-direction: column;"> <div style="display: flex; font-size: 76px; font-weight: 700; color: #ffffff; line-height: 1.1;">{{ title }}</div> {% if subtitle %}<div style="display: flex; margin-top: 24px; font-size: 32px; color: #9aa4b2; line-height: 1.35;">{{ subtitle }}</div>{% endif %} </div> <div style="display: flex; font-size: 22px; color: #6b7280;">galleyrender.com</div></div>Note what it does not have: no <html>, no <head>, no <style>, no classes, no
@font-face, and display: flex on literally every element.
The Chromium equivalent is an ordinary web page, and can use the things Satori will not:
<!doctype html><html> <head> <style> @import url("https://fonts.googleapis.com/css2?family=Newsreader:wght@400;600&display=swap"); * { box-sizing: border-box; } body { margin: 0; width: 1200px; height: 630px; padding: 72px; display: grid; grid-template-rows: auto 1fr auto; background: {{ background | default: "#0b0f14" }}; background-image: radial-gradient(60% 80% at 80% 0%, {{ accent | default: "#2dd4bf" }}22, transparent); font-family: Newsreader, Georgia, serif; color: #fff; } .eyebrow { color: {{ accent | default: "#2dd4bf" }}; letter-spacing: .12em; text-transform: uppercase; font-size: 24px; } .eyebrow::before { content: ""; display: inline-block; width: 16px; height: 16px; border-radius: 50%; background: currentColor; margin-right: 16px; vertical-align: -2px; } h1 { font-size: 76px; line-height: 1.1; margin: 0; font-weight: 600; } p { font-size: 32px; line-height: 1.35; color: #9aa4b2; margin: 24px 0 0; } footer { font-size: 22px; color: #6b7280; } </style> </head> <body> <div class="eyebrow">{{ eyebrow | default: "Galley Render" }}</div> <div> <h1>{{ title }}</h1> {% if subtitle %}<p>{{ subtitle }}</p>{% endif %} </div> <footer>galleyrender.com</footer> </body></html>Render either one the same way:
curl -sS https://api.galleyrender.com/v1/render \ -H "Authorization: Bearer $GALLEY_API_KEY" \ -H 'content-type: application/json' \ -d '{ "template": "og-card@1", "format": "png", "data": { "eyebrow": "Galley Render", "title": "Documents for agents", "subtitle": "JSON in, PDF out. Every render deterministic, cached, and signed.", "accent": "#2dd4bf", "background": "#0b0f14" }, "options": { "width": 1200, "height": 630, "scale": 2 } }'The Chromium version needs "full_page": false if the content could ever exceed 630 px; the
Satori version is fixed at the canvas size no matter what the data contains.
Choosing
Section titled “Choosing”Use satori when all of these hold:
- The output is a PNG.
- The layout is a fixed-size card: OG image, social card, badge, ticket, avatar, chart label.
- Inter is an acceptable typeface.
- Flexbox alone is enough.
Use chromium otherwise, and always for:
- Any PDF, and anything that paginates.
- Tables, grid, floats, multi-column.
- A specific typeface.
- Templates that need
::before,::after, or any pseudo-class. - Anything you are porting from an existing web page.