API REFERENCE · V1
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.
Get a key by signing in with GitHub on the dashboard; the free plan includes 100 renders a month.
Screenshots a public URL. Returns the image body directly.
| param | default | notes |
|---|---|---|
url | required | http(s) only; public hosts only (see security) |
width, height | 1280, 720 | viewport px; 100–3840 / 100–2160 |
scale | 1 | device scale factor, 1–3 |
full_page | false | capture the full scroll height |
format | png | png or jpeg |
quality | 80 | jpeg only, 1–100 |
wait_until | networkidle | load · domcontentloaded · networkidle; a page that never settles is shot as-is at timeout |
delay | 0 | extra ms after load, ≤ 10000 |
dark_mode | false | emulates prefers-color-scheme: dark |
device | — | Playwright preset name, e.g. iPhone 15; overrides width/height/scale |
timeout | 15000 | ms, ≤ 30000 |
fresh | false | bypass the cache for this request |
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.
External assets referenced by the HTML (fonts, images) are fetched through the same SSRF policy as screenshots.
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).
| template | required | optional |
|---|---|---|
article | title | author site tag accent theme |
changelog | version title | date site accent theme |
stats | stat1 label1 | stat2/3 label2/3 title site accent theme |
announce | title | subtitle site accent |
quote | quote | author role site accent theme |
event | title date | time 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.
Usage for the calling key: last 30 days, split into renders (real Chromium work — what quotas meter) and cache_hits (free).
Quotas meter renders — requests that made Chromium do real work. Cache hits never count. Budgets reset on the first of each calendar month.
| plan | renders / month | price |
|---|---|---|
| Darkroom | 100 | $0 |
| Studio | 5,000 | $19 / month |
| Agency | 25,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.
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.
All errors return JSON: { "error": { "code", "message" } }.
| status | code | meaning |
|---|---|---|
| 400 | invalid_request | failed validation; message lists each bad field |
| 401 | unauthorized | missing, unknown, or revoked API key |
| 402 | quota_exceeded | monthly render budget spent — upgrade or wait for the reset |
| 403 | blocked_by_policy | target refused by the SSRF guard |
| 404 | not_found | unknown template or key prefix |
| 422 | navigation_failed | DNS failure, connection refused, bad TLS… |
| 429 | rate_limited | per-minute budget spent; retry next minute |
| 504 | render_timeout | the page couldn't be shot within timeout |
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.