API reference

Everything the API does, documented against what is actually implemented. Base URL is wherever this server runs; the examples use https://backshot.dev. All responses are the rendered bytes by default; errors are JSON.

Authentication

Every /v1/* endpoint requires an API key, accepted two ways:

# header (recommended for server-side code)
curl "https://backshot.dev/v1/screenshot?url=https://example.com" \
  -H "Authorization: Bearer $BACKSHOT_KEY"

# query param (what makes <img src> embeds possible)
<img src="https://backshot.dev/v1/screenshot?url=https://example.com&key=YOUR_KEY">

Query-param keys are visible to anyone who can read the URL: treat a key you embed publicly as spendable by the public, and use a low-credit key for embeds. Keys are created with the server-side CLI (node bin/backshot.js keys:create); there is no self-serve signup yet.

The landing-page playground uses a shared demo key: 50 credits, refilling to 50 every hour, 10 requests/minute. It is deliberately too small to build on; get a real key for real use.

Screenshot

GETPOST/v1/screenshot

Renders a URL and returns the image bytes with the matching Content-Type. POST accepts the same parameters as a JSON body (body values win over query values).

paramtypedefaultwhat it does
urlstringrequiredPage to render. http/https only; see URL security.
widthint 16–40961280Viewport width in CSS pixels.
heightint 16–4096800Viewport height. Ignored visually when full_page=1, but still part of the cache key.
full_pageboolfalseCapture the entire scrollable page instead of the viewport.
formatpng | jpeg | webppngOutput encoding. jpg is accepted as an alias for jpeg.
qualityint 1–10080Compression quality for jpeg/webp. Silently ignored for png (png is lossless).
delay_msint 0–100000Extra wait after page load before the capture, for animations and late JS.
wait_untilload | domcontentloaded | networkidleloadNavigation event that counts as "loaded". networkidle is the patient option.
selectorstringCSS selector; capture just that element. 422 if it doesn't appear within 5s.
dark_modeboolfalseEmulates prefers-color-scheme: dark.
block_cookie_bannersboolfalseInjects CSS hiding common consent managers (OneTrust, Cookiebot, Didomi, Usercentrics, Osano and friends). Cosmetic: nothing is "accepted".
scalefloat 0.5–31Device scale factor. 2 = retina. Hard-capped at 3.
response"json"Return metadata instead of bytes: {url, cached, credits_remaining, bytes, render_ms}. url points at the stored file.

Booleans accept 1/true/yes/on (anything else is false). Out-of-range numbers are clamped, not rejected.

PDF

POST/v1/pdf

Renders a URL or raw HTML to PDF. Send exactly one of url / html (both is a 400). Body limit is 2 MB.

paramtypedefaultwhat it does
urlstringPage to render. Same security rules as screenshots.
htmlstringRaw HTML to render instead of a URL. Subresources it references are still SSRF-screened.
formatA4 | Letter | LegalA4Paper size (case-insensitive).
landscapeboolfalseLandscape orientation.
marginstringUniform margin on all four sides, e.g. 1cm, 0.5in, 18px, 10mm. Malformed values are ignored.
print_backgroundbooltrueInclude background colors and images.
scalefloat 0.1–21Print scale.
wait_until, delay_ms, responseSame behavior as the screenshot endpoint.
curl -X POST "https://backshot.dev/v1/pdf" \
  -H "Authorization: Bearer $BACKSHOT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"html": "<h1>Invoice #42</h1><p>Total: $90</p>", "format": "A4", "margin": "1cm"}' \
  -o invoice.pdf

Account

GET/v1/account

Returns the calling key's plan, live credit balance, rate limit, lifetime render count, and its 20 most recent requests:

{
  "plan": "free",
  "label": "my-project",
  "credits": 1961,
  "rate_limit_per_min": 60,
  "total_renders": 39,
  "created_at": 1787810000,
  "recent_usage": [ { "endpoint": "screenshot", "cached": false, "ms": 1412, … } ]
}

Stored files

GET/files/{hash}

Every render is stored under a content hash of its full parameter set; response=json returns this path. Files are served without auth: the hash is only derivable from the exact parameters, and only public http/https content is ever stored. Files expire with the cache (24h) and then 404.

Caching & billing

One credit buys one fresh render. The cache key is a SHA-256 of the normalized parameter set, TTL 24 hours. A cache hit returns in about a millisecond, costs zero credits, and is marked in the response:

headermeaning
X-Backshot-CacheHIT or MISS.
X-Backshot-Credits-RemainingBalance after this request.
X-Backshot-Render-MsServer-side time spent on this request.
X-RateLimit-Limit / X-RateLimit-RemainingYour per-minute budget and what's left of it this window.

Failed renders (navigation errors, blocked URLs, timeouts) are never billed. Any parameter change, even width by one pixel, is a different cache entry and a fresh render. There is no cache-bypass parameter yet; to force a fresh render within the TTL, vary a parameter.

Rate limits

Each key has a fixed-window per-minute limit (set at key creation; 10/min on the demo key). Exceeding it returns 429 with a Retry-After header in seconds. The render queue itself is also bounded: when the box is saturated far beyond its concurrency, requests return 503 immediately rather than piling up.

URL security

backshot refuses to be a proxy into private networks. For every URL (the one you submit, every redirect hop it takes, and every subresource the page loads):

checkwhat gets rejected
schemeAnything but http: and https:; no file:, data:, javascript:, chrome:.
hostnamelocalhost, *.local, *.internal, metadata.google.internal.
resolved IPs127.0.0.0/8, 10/8, 172.16/12, 192.168/16, 169.254/16 (cloud metadata), 100.64/10, 0/8, multicast, ::1, fc00::/7, fe80::/10, including IPv4-mapped IPv6 spellings. Every DNS record is checked, not just the first.
redirectsThe full redirect chain is re-validated after navigation; a hop into private space fails the render.

Blocked URLs return 400 url_blocked with a reason, and are never billed.

Errors

Errors are JSON: {"error": {"code", "message"}}.

statuscodewhen
400missing_url bad_format missing_source ambiguous_source url_blockedBad request parameters, or a URL the security screen refused.
401missing_key invalid_keyNo key, unknown key, or revoked key.
402no_creditsBalance is zero. Top up, or wait for the demo refill.
413body_too_largeRequest body over 2 MB.
422render_failedThe page couldn't be rendered: navigation failure, HTTP error from the target, selector never appeared, or output over the 25 MB cap.
429rate_limitedPer-minute limit exceeded. Honor Retry-After.
503queue_fullRender queue saturated. Retry with backoff.
504render_timeoutThe render exceeded the 30s hard timeout.

Health

GET/healthz

No auth. Live process state; the landing page's status pill reads from it:

{ "status": "ok", "browser": "up", "queue_depth": 0, "active_renders": 0, "uptime_s": 4210, "version": "0.1.0" }

What's deliberately not here yet: self-serve signup and checkout, webhooks, storage/S3 export, signed URLs, multi-region rendering. The docs describe the build that exists, not the one that's planned.