API REFERENCE · V1

Authentication

Every /v1/* endpoint takes an API key in the x-api-key header, or as an access_key query param where headers are awkward (image tags, meta tags). Keys are rate-limited per minute; the server stores only a hash.

curl -H "x-api-key: $KEY" "https://your-host/v1/screenshot?url=https://example.com" -o shot.png # or, where headers are awkward: <img src="https://your-host/v1/screenshot?url=…&access_key=$KEY">

Get a key by signing in with GitHub on the dashboard; the free plan includes 100 renders a month.

GET /v1/screenshot

Screenshots a public URL. Returns the image body directly.

paramdefaultnotes
urlrequiredhttp(s) only; public hosts only (see security)
width, height1280, 720viewport px; 100–3840 / 100–2160
scale1device scale factor, 1–3
full_pagefalsecapture the full scroll height
formatpngpng or jpeg
quality80jpeg only, 1–100
wait_untilnetworkidleload · domcontentloaded · networkidle; a page that never settles is shot as-is at timeout
delay0extra ms after load, ≤ 10000
dark_modefalseemulates prefers-color-scheme: dark
devicePlaywright preset name, e.g. iPhone 15; overrides width/height/scale
timeout15000ms, ≤ 30000
freshfalsebypass the cache for this request

POST /v1/render

Renders HTML you provide. JSON body; same options as above minus url/device, plus html (required, ≤ 2 MB). Defaults are OG-sized: 1200×630 at scale 2.

curl -X POST -H "x-api-key: $KEY" -H "content-type: application/json" \ -d '{"html": "<h1 style=\"font-size:80px\">Hello</h1>"}' \ https://your-host/v1/render -o card.png

External assets referenced by the HTML (fonts, images) are fetched through the same SSRF policy as screenshots.

GET /v1/template/:name

Designed OG cards filled with query params — no HTML required. Always 1200×630 at 2×. Browse the catalog with live previews on the templates page, or fetch it as JSON from GET /v1/templates (no auth).

<meta property="og:image" content="https://your-host/v1/template/article?title=My+post&author=Me&site=blog.dev&access_key=$KEY">
templaterequiredoptional
articletitleauthor site tag accent theme
changelogversion titledate site accent theme
statsstat1 label1stat2/3 label2/3 title site accent theme
announcetitlesubtitle site accent
quotequoteauthor role site accent theme
eventtitle datetime location site accent theme

Shared params: accent (hex, with or without #), theme (dark default · light), plus format and fresh as elsewhere. Values are HTML-escaped before rendering.

GET /v1/usage

Usage for the calling key: last 30 days, split into renders (real Chromium work — what quotas meter) and cache_hits (free).

{ "key": { "name": "production", "prefix": "ogs_3f2a", "source": "self" }, "days": [ { "day": "2026-07-05", "renders": 41, "cache_hits": 380 } ], "totals": { "renders": 41, "cache_hits": 380, "requests": 421 } }

Plans & quotas

Quotas meter renders — requests that made Chromium do real work. Cache hits never count. Budgets reset on the first of each calendar month.

planrenders / monthprice
Darkroom100$0
Studio5,000$19 / month
Agency25,000$49 / month

Over quota, render requests return 402 quota_exceeded (cached images keep serving). Upgrade, downgrade, or cancel any time from the dashboard — billing is Stripe, and plan changes apply within seconds via webhook.

Caching

Responses are cached (default 24h) keyed by every render-affecting option; the x-cache header says HIT or MISS. Hits don't run a browser and won't count toward render quotas. fresh=true forces a re-render and refreshes the cached copy.

Errors

All errors return JSON: { "error": { "code", "message" } }.

statuscodemeaning
400invalid_requestfailed validation; message lists each bad field
401unauthorizedmissing, unknown, or revoked API key
402quota_exceededmonthly render budget spent — upgrade or wait for the reset
403blocked_by_policytarget refused by the SSRF guard
404not_foundunknown template or key prefix
422navigation_failedDNS failure, connection refused, bad TLS…
429rate_limitedper-minute budget spent; retry next minute
504render_timeoutthe page couldn't be shot within timeout

Security model

A screenshot API is "make this server visit a URL," so the fetch path is guarded, not the perimeter: targets must be http(s) and resolve only to public unicast addresses. Loopback, RFC1918, link-local (including cloud metadata at 169.254.169.254), CGNAT, and reserved ranges are refused — and the same check runs inside the browser on every redirect, subresource, iframe, and popup. Service workers are disabled. Known residual risk: a DNS-rebinding window between our resolution and Chromium's; the production plan is a pinned egress proxy.

Account sessions are signed, httpOnly cookies scoped with SameSite=Lax; state-changing dashboard calls also enforce same-origin. GitHub sign-in requests the read:user scope only. API keys are stored as SHA-256 hashes — the plaintext is shown once at creation.