Documentation

Everything you need to generate screenshots and OG images from code. Base URL for all requests: https://api.shots.dev

Quickstart

Get your first image in under two minutes. No SDK required.

1. Get an API key

curl -X POST https://api.shots.dev/v1/keys \ -H "Content-Type: application/json" \ -d '{"email":"you@example.com"}' # → {"key":"sk_live_…","tier":"free","created_at":"2026-10-10T…"} # Save the key — we only show it once.
const res = await fetch("https://api.shots.dev/v1/keys", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: "you@example.com" }), }); const { key } = await res.json(); // save this — shown once
import requests res = requests.post("https://api.shots.dev/v1/keys", json={"email": "you@example.com"}) key = res.json()["key"] # save this — shown once

2. Render your first OG image

curl -X POST https://api.shots.dev/v1/og/blog-classic \ -H "Authorization: Bearer $SHOTS_KEY" \ -H "Content-Type: application/json" \ -d '{"title":"Ship boring software, on purpose","kicker":"SHOTS · BLOG"}' \ --output og.png
const res = await fetch("https://api.shots.dev/v1/og/blog-classic", { method: "POST", headers: { Authorization: `Bearer ${process.env.SHOTS_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ title: "Ship boring software", kicker: "SHOTS · BLOG" }), }); await Bun.write("og.png", res); // or Buffer.from(await res.arrayBuffer())
import os, requests res = requests.post( "https://api.shots.dev/v1/og/blog-classic", headers={"Authorization": f"Bearer {os.environ['SHOTS_KEY']}"}, json={"title": "Ship boring software", "kicker": "SHOTS · BLOG"}, ) open("og.png", "wb").write(res.content)

3. Take a screenshot

curl "https://api.shots.dev/v1/screenshot?url=https://example.com&full_page=true" \ -H "Authorization: Bearer $SHOTS_KEY" \ --output screenshot.png
const url = new URL("https://api.shots.dev/v1/screenshot"); url.searchParams.set("url", "https://example.com"); url.searchParams.set("full_page", "true"); const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.SHOTS_KEY}` }, });
import os, requests res = requests.get( "https://api.shots.dev/v1/screenshot", headers={"Authorization": f"Bearer {os.environ['SHOTS_KEY']}"}, params={"url": "https://example.com", "full_page": "true"}, ) open("screenshot.png", "wb").write(res.content)
💡

Prefer trying before reading? The live demo hits the real API in your browser — no key required.

Authentication

Every request (except POST /v1/keys and GET /v1/templates) needs your API key. Send it as a Bearer token:

Authorization: Bearer sk_live_…

For contexts that can't set headers — <img> tags, og:image meta tags, email HTML — pass the key as a query parameter instead:

<meta property="og:image" content="https://api.shots.dev/v1/og/blog-classic?api_key=sk_live_…&title=Hello">
⚠️

Keep keys server-side. The ?api_key= form is convenient but exposes your key in URLs and logs. For high-traffic pages, render once, cache the PNG on your own CDN, and serve that instead.

Endpoints

Five endpoints. That's the whole API.

POST /v1/keys no auth

Create an API key. One call, one key — no dashboard, no email verification loop.

FieldTypeDescription
emailrequiredstringWhere we send plan and billing updates. Never shared, never marketed to.

Returns 201 with {"key": "sk_live_…", "tier": "free", "created_at": "…"}. The key is shown once — store it in your secrets manager immediately.

GET /v1/templates no auth

List all templates with their fields, defaults, and character limits. Use this to build dynamic forms — it's what powers our live demo.

Returns 200 with {"templates": [{slug, name, description, category, width, height, fields: [{key, label, type, default, maxLength}]}]}.

POST /v1/og/:slug auth required

Render a template to a 1200×630 PNG. Send fields as a JSON object; anything you omit falls back to the template's default. Also available as GET /v1/og/:slug?title=…&kicker=… with fields as query parameters.

ParameterTypeDescription
:slugrequiredpathTemplate slug, e.g. blog-classic. See GET /v1/templates or the gallery.
{field}optionaljson / queryOne key per template field. Values longer than maxLength are rejected with 422.

Returns 200 with Content-Type: image/png — raw image bytes, ready to save, hotlink, or pipe into your build. Unknown slugs return 404.

GET /v1/screenshot auth required

Capture any public URL on our managed Chromium fleet. Ads and cookie banners are blocked automatically; pages render at 2× retina by default.

ParameterTypeDescription
urlrequiredstringFully-qualified URL to capture (https://…). Must be publicly reachable.
widthoptionalintegerViewport width in px. Default 1280, max 1920.
heightoptionalintegerViewport height in px. Default 720. Ignored when full_page=true.
full_pageoptionalbooleanCapture the entire scrollable page instead of just the viewport. Default false.
waitoptionalintegerExtra milliseconds to wait after load (for JS-heavy pages). Default 500, max 10000.

Returns 200 with Content-Type: image/png. Unreachable URLs return 422.

GET /v1/usage auth required

Check your current billing period usage. Poll this to build usage meters or alerting — it's cheap and never rate-limited below your plan's normal limits.

Returns 200 with {"tier": "starter", "period": "2026-10", "images_used": 412, "images_limit": 2500, "resets_at": "2026-11-01T00:00:00Z"}.

Rate limits

Limits are per API key, per minute. Exceeding them returns 429 with a Retry-After header (in seconds). Beta limits are conservative; paid limits increase at launch.

PlanRequests / minuteMonthly images
Free10100
Starter ($9)602,500
Pro ($19)6015,000
💡

Hitting limits in production? Render once and cache the PNG on your CDN — OG images rarely change per deploy. Most customers make 90% fewer calls after adding a cache layer.

Error codes

Errors return JSON with a machine-readable code and a human-readable message:

{ "error": { "code": "invalid_key", "message": "The API key provided is invalid or revoked." } }
StatusCodeWhat it meansWhat to do
400invalid_fieldsMalformed request body or query.Check the parameter types in the reference above.
401invalid_keyMissing, malformed, or revoked API key.Verify the Authorization header; create a new key if needed.
402quota_exceededMonthly image quota used up.Wait for reset or upgrade via GET /v1/usage.
404template_not_foundNo template with that slug.List slugs via GET /v1/templates.
422validation_errorA field exceeds maxLength, or the screenshot URL is unreachable.Trim the field or check the URL.
429rate_limitedToo many requests.Back off for Retry-After seconds; add caching.
500render_failedOur renderer hit an internal error.Retry once; contact support if it persists.

FAQ

How do I rotate a compromised key?
Create a new key with POST /v1/keys, deploy it, then ask support to revoke the old one. Self-serve revocation is coming at launch.
Can I hotlink generated images directly?
Technically yes via ?api_key=, but don't — it leaks your key in referrer headers and logs. Render once, cache on your CDN, serve the cached copy.
What fonts are available in templates?
Templates render with the Inter typeface (400/600/800 weights) for consistent cross-platform output. Custom brand fonts are available on Pro.
Do screenshots work with JS-heavy SPAs?
Yes — the renderer waits for network idle plus your optional wait parameter (up to 10s) before capturing.
Is there an SDK?
The API is plain HTTPS + JSON, so fetch or requests is all you need (see Quickstart). Official Node and Python SDKs ship at launch.
🚀

Ready? Grab a key and render your first image in two minutes — or poke at the live demo first.