Skip to content

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.

chromiumsatori
Defaultyesopt in with "engine": "satori"
Output formatspdf, png, jpeg/jpgpng
Default format when you omit itpdfpng
CSSEverything a current Chromium supportsA flexbox subset, inline style= attributes only
LayoutFlexbox, grid, floats, tables, multi-column, absolute positioningFlexbox and absolute positioning
Pagination@page, page breaks, repeating <thead>None — one fixed-size canvas
JavaScript in the templateRuns before captureNot executed
Web fontsAny public https:// font or Google Fonts importBundled Inter 400/600/700 only
ImagesFetched as subresourcesInlined as data URIs before layout
ProcessA headless browser pagePure Node: HTML → SVG → PNG
SpeedHundreds of milliseconds and upTens 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.

There is no engine field on a render request. The engine comes from the template version, and the dispatch rule is exact:

  1. If the version’s engine is satori and the requested format is png, the Satori path runs.
  2. Otherwise — any PDF, any JPEG, and any chromium template — 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.

Same template, both engines
# Satori path: engine=satori, format=png
curl -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 for
curl -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"}}'

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 setContent and waited to load, then document.fonts.ready is awaited so text is measured against the real face.
  • Media is emulated: print for PDF, screen for PNG and JPEG. @media print blocks apply to PDFs and are ignored for raster output.
  • options.css is injected after the template’s own styles.
  • Every subresource is routed through the SSRF-safe asset fetcher. data:, about: and blob: URLs pass straight through; anything that is not http(s)file:// above all — is blocked and reported as asset_blocked.
  • Service workers are blocked and CSP is honoured.
OptionDefaultRange
width12001–8000 px
height6301–8000 px
scale1clamped to 0.1–3, applied as the device pixel ratio
full_pagetrueset false to capture only the viewport
quality85JPEG 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.

OptionDefaultNotes
page_sizeLetterA4, Legal, … Setting it overrides the CSS @page { size: … }. Leave it unset to let the CSS decide.
landscapefalse
margin0.5in each sideString or { top, right, bottom, left }.
print_backgroundtrue
scale1CSS 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 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.

SupportedNot supported
display: flex, display: nonedisplay: block, inline, grid, table
flex-direction, justify-content, align-items, flex-wrap, flex-grow, gapfloats, multi-column
position: absolute / relative, top/right/bottom/leftposition: fixed, sticky
width, height, min-*, max-*, padding, marginpercentage heights that depend on content
background-color, linear and radial gradients, background-image with a URLbackground-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, transformpseudo-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:

FamilyWeights
Inter400, 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.

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.

OptionDefaultNotes
width12001–4000 px. The SVG canvas width.
height6301–4000 px. Fixed — content does not grow the canvas.
scale10.1–3, the same cap as chromium. The PNG is rasterised at width × scale.
backgroundrgba(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.

Here is one 1200×630 card written twice. First the Satori version — this is the real og-card starter, trimmed:

og-card, engine: satori
<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:

og-card-pro, engine: chromium
<!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:

Terminal window
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.

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.