API Reference

The Screenmint base URL is https://api.screenmint.dev. Every endpoint accepts and returns JSON. Render a page in one call: you get back a hosted image URL, ready to drop into an <img> tag or a meta tag.

curl -X POST https://api.screenmint.dev/v1/screenshot \
  -H "X-API-Key: sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com"}'
Response 200
{
  "url": "https://cdn.example/screenshots/9f2c….jpeg",
  "width": 1280,
  "height": 720,
  "cached": false,
  "render_ms": 1240
}
New accounts include 100 free renders per month — no card required. Create an account and grab your key from the dashboard.

Authentication

Authenticate every request with an X-API-Key header. Keys are created and revoked in the dashboard; the plaintext key is shown exactly once at creation, so store it somewhere safe.

X-API-Key: sk_live_...
Keep keys server-side. Anyone with your key can spend your render quota — never embed it in client-side JavaScript or a public repo.

Rate limits & caching

Limits apply per account, not per API key: every key on the account (and teammates working in it) shares them, so creating more keys doesn’t raise them. There’s a per-minute request rate (fixed one-minute windows), a cap on renders in flight at once, and a monthly render quota. On the Free plan the quota resets on the 1st of each month (UTC); on paid plans it resets on your billing-period anniversary — subscribe on the 18th and it resets on the 18th.

PlanRequests / minRenders in flightRenders / month
Free101100
Starter6032,000
Pro300620,000

Identical requests are cached — screenshots and videos for 1 hour, OG cards for 24 hours. Cache hits return in milliseconds with "cached": true and the same URL, and do not consume monthly renders (they still count toward the per-minute rate, except repeat views of a signed URL). After that, the same request renders afresh and gets a new URL, so you always get the page as it is now.

Don’t render URLs that contain secrets (session tokens, signed query params, API keys). Results are hosted at public URLs, and identical requests are served from the same cached render — so a URL with a secret produces an image that anyone making the same request, or holding the result URL, can retrieve. Render public pages, or ones you’ve authorized another way.

Errors

Errors return a JSON envelope with a stable error code, an optional human-readable message, and, on rate-limit and quota 429s, reset_at.

{
  "error": "rate_limit_exceeded",
  "reset_at": "2026-07-22T18:04:00.000Z"
}
StatusCodeWhen
400invalid_paramsA body field failed validation — the message field says which one and why.
400invalid_urlThe URL is not valid http(s), or it resolves to a private / internal address.
401invalid_api_keyThe X-API-Key header is missing or not recognized.
401missing_signatureA signed URL is missing its token or sig parameter.
401invalid_signatureA signed URL’s sig doesn’t match its parameters, or its key was revoked. Sign exactly the parameters you send, and re-sign after changing any of them.
403plan_upgrade_requiredThe feature isn’t on your plan — e.g. async smooth video on Free.
403domain_blockedThe URL’s host is blocked from being rendered.
403account_blockedThe account is suspended. Contact [email protected].
422unrenderable_urlThe host does not resolve, or the page failed to load in time: 15 seconds, or 45 for smooth (async) videos.
429rate_limit_exceededPer-minute request limit reached (per account, across all its keys). reset_at is when the window resets.
429concurrency_limit_exceededToo many renders in flight at once for your plan (1 on Free, 3 on Starter, 6 on Pro, of which at most 1, 2 or 3 videos), or video capacity is momentarily full. Retry with backoff once a render finishes. This 429 has no reset_at.
429monthly_quota_exceededMonthly render quota used up. reset_at is when it resets: the 1st of next month (UTC) on Free, your billing-period anniversary on paid plans.
429too_many_jobsToo many smooth videos in progress (2 on Starter, 5 on Pro). Wait for one to finish.
429too_many_batchesTwo batches are already running on this account. Wait for one to finish.
503service_unavailableAll renderers are busy. Retry with exponential backoff.
504render_timeoutThe page kept the renderer busy past the time limit — e.g. a script that never finishes. Not charged.

POST/v1/screenshot

Capture a screenshot of any public URL. The page gets a fresh, isolated browser context, waits for the load event (up to 15 seconds), then captures, optimizes, and hosts the image for you.

ParameterTypeDefaultDescription
url requiredstring—Page to capture. http(s) only; private and internal hosts are rejected.
widthinteger1280Viewport width in pixels. Max depends on plan: 1280 (Free), 1920 (Starter), 2560 (Pro).
heightinteger720Viewport height in pixels. Max depends on plan: 1280 (Free), 1080 (Starter), 1440 (Pro).
scaleinteger1Device pixel ratio. 2 = retina/HiDPI (doubles output resolution). Pro plan only.
full_pagebooleanfalseCapture the full scroll height instead of just the viewport (auto-scrolls to load lazy content). A taller page is captured from the top down to the cap: 20,000px, or 16,383px for WebP and AVIF, and less for very wide or scale: 2 captures.
formatstring"jpeg"Output: "jpeg", "png", "webp", "avif", or "pdf".
qualityinteger85Quality 1–100 for jpeg/webp/avif. Ignored for png/pdf.
delayinteger0Extra wait after page load — useful for animations and late JS. Max depends on plan: 5s (Free), 15s (Starter), 25s (Pro).
dark_modebooleanfalseEmulate prefers-color-scheme: dark.
wait_for_selectorstring—Wait until this CSS selector appears before capturing (up to 15s).
selectorstring—Capture only the first visible element matching this CSS selector, instead of the page. An invalid selector, or one with no visible match, is a 400.
hide_selectorsstring[]—CSS selectors to hide before capture (e.g. modals, chat widgets).
block_cookiesbooleanfalseHide cookie/consent banners (the common consent tools, plus custom banners recognised by their text) and undo the scroll-lock a banner leaves behind.
block_adsbooleanfalseBlock requests to known ad/tracker/analytics hosts, and hide marketing pop-ups and chat widgets (HubSpot, Intercom, Drift, Crisp, LiveChat, OptinMonster, Popup Maker, Elementor). Hide anything else with hide_selectors.
inject_cssstring—Custom CSS injected as soon as the page has loaded (it also applies to elements that appear later).
inject_jsstring—Custom JavaScript run in the page once it has loaded, before wait_for_selector and delay, so those can wait for what it does. A syntax error or an exception it throws is a 400 (not charged).
curl -X POST https://api.screenmint.dev/v1/screenshot \
  -H "X-API-Key: sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com",
    "width": 1280,
    "height": 720,
    "full_page": false,
    "format": "jpeg",
    "quality": 85,
    "delay": 0,
    "dark_mode": false
  }'
Response 200
{
  "url": "https://cdn.example/screenshots/9f2c….jpeg",
  "width": 1280,
  "height": 720,
  "cached": false,
  "render_ms": 1240
}
width and height are the size of the image you get, in pixels. With full_page, selector or scale: 2 they differ from the viewport you asked for.

POST/v1/og

Generate a branded 1200×630 Open Graph card from text and colors — no design tools, no templates to host. Output is an optimized JPEG, cached for 24 hours.

ParameterTypeDefaultDescription
title requiredstring—Card headline, up to 80 characters. Clamped to 2 lines.
descriptionstring—Supporting text, up to 160 characters. Clamped to 2 lines.
site_namestring—Small accent-colored label under the text, up to 40 characters.
logo_urlstring—A direct, publicly-reachable image URL (png/jpg/svg/webp) or a data: URI — shown above the title at 40 px. Not a webpage, and the host must allow hotlinking.
bg_urlstring—Screenshot this website and use it as the card background, with your text overlaid.
bg_imagestring—Use this image URL (or data: URI) as the card background instead of a color.
overlaynumber0.45Darkening scrim over a background image, 0–1, for text legibility.
bg_colorstring"#ffffff"Background color, hex (ignored when a background image is set).
accent_colorstring"#000000"Top bar and site_name color, hex.
text_colorstring"#000000"Title and description color, hex (defaults to white over a background image).
curl -X POST https://api.screenmint.dev/v1/og \
  -H "X-API-Key: sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "title": "How we scaled to 1M renders",
    "description": "Lessons from running headless Chromium in production.",
    "site_name": "Engineering Blog",
    "bg_color": "#0F0F11",
    "accent_color": "#8B5CF6",
    "text_color": "#ffffff"
  }'
Response 200
{
  "url": "https://cdn.example/og/def4….jpeg",
  "cached": false,
  "render_ms": 380
}
Want to see it before you call it? Design your card visually in the free OG playground — it generates the exact API request for you.

POST/v1/video

Record a page as a short video — great for capturing animations or a scroll-through of a long page. Output is mp4 (default), webm, or an animated gif.

ParameterTypeDefaultDescription
url requiredstring—Page to record.
secondsinteger5Recording length. Max: 5s (Free), 30s (Starter), 60s (Pro). Over 30s needs "async": true.
formatstring"mp4"Output: "mp4", "webm", or "gif".
scrollbooleanfalseScroll from top to bottom at an even pace over the duration. The page is scrolled through once before recording, so sections that fade in as you scroll are already showing; pages that scroll inside a panel rather than the window are scrolled too.
widthinteger1280Frame width (plan-capped, same as screenshots).
heightinteger720Frame height (plan-capped).
delayinteger0Wait after load before recording starts.
dark_modebooleanfalseEmulate prefers-color-scheme: dark.
block_adsbooleanfalseBlock ad/tracker hosts and hide marketing pop-ups and chat widgets, as for screenshots.
block_cookiesbooleanfalseHide cookie/consent banners, as for screenshots.
hide_selectorsstring[]—CSS selectors to hide for the whole clip (e.g. an announcement bar or chat widget).
asyncbooleanfalseStarter & Pro. Capture frame by frame in the background for smooth video on heavy pages; returns a job to poll. Required for videos over 30s. See Smooth video.
POST https://api.screenmint.dev/v1/video
{ "url": "https://stripe.com", "seconds": 6, "scroll": true, "format": "mp4" }

→ { "url": "https://cdn.example/videos/e46e…-3f9a1c2b.mp4",
    "width": 1280, "height": 720, "seconds": 6, "format": "mp4",
    "cached": false, "render_ms": 7100, "credits": 3 }
Max length is plan-based: 5s (Free), 30s (Starter), 60s (Pro). Pro videos longer than 30s render as smooth videos (add "async": true). Because video is far more compute-intensive, it costs 1 credit per 2 seconds (rounded up) against your monthly quota — e.g. a 10s video = 5 credits. Screenshots and OG cards are 1 credit each. The response includes a credits field.

Smooth video (async)

On the Starter and Pro plans, add "async": true to a /v1/video request and the video is captured frame by frame in the background. Every frame is fully drawn before the next one, so the result stays smooth even on heavy pages — big blur, glass or animated effects — that stutter when recorded in real time, and CSS animations play at their real speed. You get a job_id back immediately (202); poll it, or listen for the render.completed webhook.

POST https://api.screenmint.dev/v1/video
{ "url": "https://stripe.com", "seconds": 15, "scroll": true,
  "block_cookies": true, "async": true }

→ 202  { "job_id": "7c1e…", "status": "queued", "credits": 8,
         "poll": "/v1/video/jobs/7c1e…" }
GET /v1/video/jobs/:id
{
  "job_id": "7c1e…", "status": "completed",
  "url": "https://cdn.example/videos/9b2f….mp4",
  "width": 1280, "height": 720, "seconds": 15, "format": "mp4",
  "credits": 8, "render_ms": 96000
}
status is queued, processing, completed or failed (with error and message). Most videos are ready within a few minutes. Each job gets 10 minutes, or 30 seconds per second of video for longer clips (30 minutes for 60s); one that can’t finish in time fails with render_timeout. When a job ends, your webhooks receive render.completed (with the job_id) or render.failed. Credits are the same as a regular video and are only charged when the video is delivered. Up to 2 smooth videos can be in progress at once on Starter and 5 on Pro; more returns 429 too_many_jobs.

POST/v1/batch

Queue up to 50 renders in one call. Each item is a screenshot (default) or an OG card ("kind": "og") and takes the same params as the single endpoints — including template. You get a job_id back immediately (202); poll it or listen for the batch.completed webhook.

POST https://api.screenmint.dev/v1/batch
{
  "items": [
    { "url": "https://a.com", "full_page": true },
    { "url": "https://b.com", "format": "pdf" },
    { "kind": "og", "template": "e7c1…", "title": "Hello" }
  ]
}

→ 202  { "job_id": "a3d6…", "status": "processing", "total": 3,
         "poll": "/v1/batch/a3d6…" }
GET /v1/batch/:id
{
  "status": "completed", "total": 3, "completed": 3, "failed": 0,
  "items": [
    { "idx": 0, "kind": "screenshot", "status": "done", "result_url": "https://…" },
    { "idx": 1, "kind": "screenshot", "status": "done", "result_url": "https://…" },
    { "idx": 2, "kind": "og",         "status": "done", "result_url": "https://…" }
  ]
}
Each item counts against your monthly quota as it renders. A batch sent with no quota left is refused up front (429 monthly_quota_exceeded); if the quota runs out mid-batch, the remaining items are marked failed with the reason. Items share your plan’s renders-in-flight limit with your other requests, and wait for a free slot rather than failing. Up to 2 batches run at once per account (429 too_many_batches).

Webhooks

Add an HTTPS endpoint under Webhooks in the dashboard and Screenmint POSTs a JSON event to it as renders finish — so you don’t have to poll async jobs. Each endpoint gets its own signing secret (whsec_…), shown once when you add it.

EventSent when
render.completedA screenshot, OG card or video finished — from the API, a signed URL or the dashboard playground, cache hits included. Not sent for repeat views of a signed URL whose image is already rendered, or for batch items (they report through batch.completed). data is the render result (url, cached, render_ms, …) plus endpoint; for an async smooth video it has job_id and credits in place of cached and render_ms.
render.failedAn async smooth video failed (async video only). data has the job_id, error and message.
batch.completedEvery item of a batch has finished. data has the job_id and the total, completed and failed counts; per-item results are at GET /v1/batch/:id.

Every delivery has the same envelope — { event, data, created_at } — with the event name repeated in the X-Screenmint-Event header:

POST /webhooks/screenmint HTTP/1.1
Content-Type: application/json
User-Agent: Screenmint-Webhooks/1
X-Screenmint-Event: render.completed
X-Screenmint-Signature: sha256=3b5f0c9e41d7…

{
  "event": "render.completed",
  "data": {
    "endpoint": "video",
    "job_id": "7c1e…",
    "url": "https://cdn.example/videos/9b2f….mp4",
    "width": 1280, "height": 720, "seconds": 15, "format": "mp4",
    "credits": 8
  },
  "created_at": "2026-09-29T14:03:11.482Z"
}
Verify the signature

X-Screenmint-Signature is sha256=<hex>: the HMAC-SHA256 of the raw request body, keyed with the endpoint’s full signing secret — including the whsec_ prefix. Compute it over the exact bytes you received, before parsing the JSON, and compare in constant time:

import crypto from 'node:crypto';

// rawBody: the request body exactly as received (Buffer or string). Verify it
// before parsing — JSON that has been parsed and re-serialized won't match.
// secret: the endpoint's full signing secret, "whsec_" prefix included.
function verifyScreenmintSignature(rawBody, signatureHeader, secret) {
  const expected =
    'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  const given = Buffer.from(String(signatureHeader || ''));
  const want = Buffer.from(expected);
  return given.length === want.length && crypto.timingSafeEqual(given, want);
}

// Express: take this route's body raw, verify, then parse.
app.post('/webhooks/screenmint', express.raw({ type: 'application/json' }), (req, res) => {
  const ok = verifyScreenmintSignature(
    req.body,
    req.get('X-Screenmint-Signature'),
    process.env.SCREENMINT_WEBHOOK_SECRET, // whsec_...
  );
  if (!ok) return res.sendStatus(401);
  const { event, data } = JSON.parse(req.body);
  // event: 'render.completed' | 'render.failed' | 'batch.completed'
  res.sendStatus(200);
});
Reply with any 2xx within 8 seconds. Each event is sent once — a failed delivery isn’t retried — and recent attempts are listed under Deliveries on the Webhooks page. Redirects aren’t followed, and endpoints must be public HTTPS URLs.

Templates

Save a reusable set of defaults in the dashboard (an OG-card design or a screenshot config), then reference it by id with template. Any fields you also pass in the request override the template — so you design once and vary only what changes per render.

POST https://api.screenmint.dev/v1/og
{
  "template": "e7c1…",         // your saved OG template
  "title": "This week's post"  // overrides the template's title
}
Templates work on /v1/screenshot, /v1/og, and signed URLs. A screenshot template can even omit the URL and take it per-call.

Signed URLs

Render by hitting a URL directly — perfect for an <img src> or an og:image meta tag, with no API key header and no server round-trip. Pass your render options as query params plus a token (your key’s signing token) and a sig (an HMAC over the params). On success it 302-redirects to the hosted image, so browsers and CDNs cache it normally.

Build the signature server-side (the signing secret must never ship to the browser): take every param except sig, sort by key, join as encodeURIComponent(k)=encodeURIComponent(v) with &, and HMAC-SHA256 it with your signing secret (hex).

import crypto from 'node:crypto';

const TOKEN  = '2b9f…';           // key's signing token (public)
const SECRET = 'a1b2c3…';         // signing secret (server-side only)

function signedUrl(params) {
  const q = { token: TOKEN, ...params };
  const canonical = Object.keys(q).sort()
    .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(q[k]))
    .join('&');
  const sig = crypto.createHmac('sha256', SECRET).update(canonical).digest('hex');
  return `https://api.screenmint.dev/v1/screenshot?${canonical}&sig=${sig}`;
}

// Drop straight into HTML:
// <img src={signedUrl({ url: 'https://stripe.com', width: 1200, full_page: true })} />
The same scheme works for /v1/og. Signed renders count against your quota and rate limit exactly like keyed requests. Repeat views of an image that’s already rendered are free and don’t count against your per-minute limit, so a busy page embedding it can’t slow down your API calls. Get your token and secret from API Keys (secret is shown once at creation).

GET/v1/usage

Check your current billing period programmatically — handy for alerting before you hit the quota.

curl https://api.screenmint.dev/v1/usage \
  -H "X-API-Key: sk_live_..."
Response 200
{
  "plan": "starter",
  "renders_used": 487,
  "renders_limit": 2000,
  "period_start": "2026-07-18T09:12:44.000Z",
  "period_end": "2026-08-18T09:12:44.000Z"
}