Every UI change makes some screenshot in your docs, help center, onboarding emails or marketing site wrong. Nobody retakes forty screenshots by hand after a redesign, so they quietly drift out of date. Describe each screenshot once as a request, run them all in CI, and your docs update themselves.
One screenshot, described as data
Log in as a demo account with a session cookie, capture one panel, and hide what changes between runs:
curl -X POST "https://shotkit.net/api/take" \
-H "X-Access-Key: YOUR_ACCESS_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://app.example.com/settings/billing","cookies":["session=DEMO_SESSION_TOKEN; Domain=app.example.com; Secure"],"selector":"main","hide_selectors":[".avatar",".last-login","#intercom-container"],"styles":"* { caret-color: transparent !important }","device_scale_factor":2,"reduce_motion":true,"format":"png","wait_for_selector":"[data-loaded]"}' \
--fail-with-body -o shot.pngdevice_scale_factor: 2 produces retina images that stay sharp in docs. wait_for_selector waits until your data has loaded instead of guessing with a delay.
A light and a dark version
If your app follows the system theme, render each screen twice and let the docs pick with a <picture> element:
curl -X POST "https://shotkit.net/api/take" \
-H "X-Access-Key: YOUR_ACCESS_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://app.example.com/dashboard","cookies":["session=DEMO_SESSION_TOKEN; Domain=app.example.com; Secure"],"dark_mode":true,"device_scale_factor":2,"format":"png"}' \
--fail-with-body -o shot.pngRun them all in CI
Keep a list of screenshots next to your docs and render it on every release:
import { writeFile } from "node:fs/promises";
import shots from "./docs/screenshots.json" with { type: "json" }; // [{ "file": "billing.png", "params": { ... } }]
for (const { file, params } of shots) {
const res = await fetch("https://shotkit.net/api/take", {
method: "POST",
headers: { "X-Access-Key": process.env.SHOTKIT_ACCESS_KEY, "Content-Type": "application/json" },
body: JSON.stringify(params),
});
if (!res.ok) throw new Error(`${file}: ${(await res.json()).error_message}`);
await writeFile(`docs/images/${file}`, Buffer.from(await res.arrayBuffer()));
}Commit the images or upload them to your docs host. A failed request fails the job, so a renamed selector is caught on the day it changes, not months later.
Tips
- Demo data: point at a seeded demo account, or use
scriptsto replace names and numbers before the capture. - Stable output:
reduce_motion, a fixedtime_zoneand hidden timestamps make each run produce the same image when nothing changed, so your docs repo only sees real updates. - Callouts: add highlights and arrows with
styles, for example an outline around the button the article is about. - Animated steps: record a short GIF of a flow with
format: "gif"and aclick.
Frequently asked questions
Can it screenshot my app on localhost?
No, the page must be reachable from the internet. Point it at a preview deployment or staging environment, or expose localhost with a tunnel such as Cloudflare Tunnel or ngrok.
How do I log in?
Pass a session cookie with cookies or a token with authorization. Use a dedicated demo account with read-only data, not a real user's session.
Will the screenshots change when nothing in the UI changed?
Not if you hide what varies (timestamps, avatars, random data), set reduce_motion and a fixed time_zone. Then identical UI renders identical images.