API reference

JSON REST API | an API key is all you need to get started. Two modes: hosted (Shotbot stores the images) or callback (images delivered to your server, white-label). Unfamiliar terms? See the glossary.

Want to play with the API before writing any code? The live playground generates the HTTP request and code in 6 languages as you adjust the parameters. Try the API →
Captures are billed at creation. Displaying an existing screenshot is free and unlimited.

Where to start?

Shotbot generates and hosts the screenshot. You receive a token to track the status and retrieve the direct image URL.

1. Submit a capture

POST https://api.shotbot.net/capture
POST https://api.shotbot.net/capture
Content-Type: application/json

{
  "key":            "your_api_key",
  "url":            "https://test.shotbot.net",
  "format":         "webp",
  "viewport_width": 1280,
  "ratio":          "16:9",
  "wait_time":      15
}

Response

JSON 202
{
  "token":      "4XyZaB…32 alphanumeric characters [A-Za-z0-9]",
  "status":      "queued",
  "eta_seconds": 20
}

If monthly quota and credits are exhausted, the response will be {"status":"waitlisted"} without a token | the capture will be processed at the next monthly reset.

2. Poll for status

GET https://api.shotbot.net/capture/{token}
GET https://api.shotbot.net/capture/a3f1c9…

// Pending
{ "token": "a3f1c9…", "status": "queued", "eta_seconds": 18 }

// Done
{
  "token":           "a3f1c9…",
  "status":           "done",
  "image":            "https://static.shotbot.net/a/a3/a3f1c9….webp",
  "preview":          "https://static.shotbot.net/a/a3/a3f1c9…_p.webp",
  "format":           "webp",
  "url":              "https://test.shotbot.net",
  "meta_http_status": 200,
  "meta_final_url":   "https://test.shotbot.net/",
  "meta_page_title":  "Example Domain",
  "file_hash":        "sha256:e3b0c44298fc1c149afb…",
  "captured_at":      1746268800
}

// Failed (status=3)
{
  "token":     "a3f1c9…",
  "status":     "failed",
  "error_code": "nav_timeout",
  "url":        "https://test.shotbot.net",
  "image":      "https://static.shotbot.net/a/a3/a3f1c9….jpg",
  "partial":    true
}

The token acts as a bearer: anyone who has it can check the status. No API key is required for this GET endpoint.

3. Available parameters

FieldTypeDefaultDescription
Essentials
keystring·API key (required)
urlstring·URL to capture, http or https (required)
Image & output
formatstringjpgjpg · png · webp · avif · pdf (see PDF output). Without Shotbot Pro: jpg only.
viewport_widthint1280Viewport: mobile (360 · 375 · 390 · 414 · 430) · tablet (768 · 1024) · desktop (1280 · 1440 · 1920). Without Shotbot Pro, limited to 390, 768, 1280. Shotbot Pro can also pass any custom width 280 to 3840 px (CSS / responsive testing).
output_sizeint= viewportOutput image width: 120 · 160 · 240 · 320 · 390 · 480 · 640 · 768 · 1024 · 1200 · 1280 · 1440 · 1920
ratiostring16:9Aspect ratio of the capture (see table below)
crop_heightint·Custom output height in pixels (overrides ratio). Sets the viewport height and captures that region from the top, so bottom-anchored fixed elements stay at the bottom. Maximum: 30,000 px. Pro only.
fullpageboolfalseCapture full page height (full scroll, max 30,000 px), ignores ratio
hidpiboolfalseHiDPI / Retina mode (resolution ×2)
prefers_reduced_motionboolfalseEmulates the CSS media query prefers-reduced-motion: reduce. Well-built sites disable or shorten their animations (framer-motion, GSAP, AOS, IntersectionObserver): cleaner captures, no mid-animation frames. No effect on sites that don't honour the media query. Available to all accounts.
color_schemestring·Force a color scheme before capture: "dark" · "light" · omit for browser default
selectorstring·CSS selector of the element to capture. Only that element is extracted, returned at its natural size. output_size, ratio and crop_height are not applied (no re-crop or rescale). Ignored when format: "pdf". Fails with nav_error if no element matches. Pro only.
Behaviour
nojsboolfalseDisables JavaScript | bypasses pop-ups, cookie banners and consent overlays
waitint5Seconds to wait before capture, 0-30. Increase for React/Vue/Angular SPAs and lazy-loading. Navigation timeout aligns automatically.
dismiss_cookiesstring""Auto-dismisses CMP banners (OneTrust, Cookiebot, Didomi, TCF…). Values: "" (leave visible) · "reject" (prefer reject) · "accept" (accept to close). Pro only for non-empty values.
scroll_before_captureboolfalseForces full page scroll before capture. Triggers loading="lazy" images and IntersectionObserver pipelines, useful combined with fullpage. Pro only.
scroll_offset_pxint0Scroll down N px (0 to 30,000) before capture, so the frame starts lower. Runs after scroll_before_capture. Mutually exclusive with scroll_to (sending both returns 400 invalid_scroll_combo). Ignored for fullpage, PDF and selector. Pro only.
scroll_tostring·CSS selector scrolled into view (top of frame) before capture. Unlike selector it does not clip output, it only positions the page. Mutually exclusive with scroll_offset_px (400 invalid_scroll_combo). A selector matching no element is a no-op (capture succeeds). Ignored for fullpage, PDF and selector. Pro only.
block_adsboolfalseBlocks requests to the most common ad networks and trackers, faster and cleaner captures. Pro only.
autoplay_videosboolfalseLaunches Chromium with --autoplay-policy=no-user-gesture-required so <video autoplay> elements, including unmuted ones, start playing without a user gesture. Useful for pages with background or hero videos that would otherwise show a static poster frame. No effect on PDF captures. Pro only.
omit_backgroundboolfalseTransparent viewport backdrop (Chromium omitBackground option). The page's own background colour is preserved; only the default white backdrop becomes transparent. Visible on PNG/WebP/AVIF; JPG flattens the alpha to black (silent no-op). No effect on PDF output. Pro only.
emulate_print_mediaboolfalseApplies the page's @media print stylesheet before capture (page.emulateMediaType('print')). Useful for capturing print-only layouts as an image. No additional effect on PDF (already in print mode). Pro only.
render_regionstringfr-parisRender region: fr-paris (default, free) · ca-montreal · sg-singapore · au-sydney · vn-hanoi. The capture is routed through a network egress point in the chosen country, locale and timezone emulated. See geolocated screenshots. Pro only for values other than fr-paris.
Auth / Session
http_authobject·HTTP Basic authentication (see below). Pro only.
cookiesarray·Cookies to inject before navigation (see below). Useful for capturing authenticated pages without HTTP Basic Auth. Max 50 cookies. Pro only.
Privacy
privateboolfalsePrivate capture: the image is not uploaded to the CDN. The GET /capture/{token} response contains "download" (URL pointing to /capture/{token}/file) instead of "image"/"preview". The file is fetchable any number of times until it auto-expires, then purged. Deduplication disabled. Available to all accounts.

These options in pictures: viewports · presets · full page · frames · cookie banners · light/dark theme.

Available ratios

ValueOrientationTypical use
16:9LandscapeHD screen, video (default)
4:3LandscapeClassic standard
16:10LandscapePC monitors
3:2LandscapePhotography
2:1LandscapePanoramic, hero
1:1SquareSocial media
9:16PortraitMobile, stories
3:4PortraitStandard portrait
10:16PortraitTablet portrait
2:3PortraitPhoto portrait
1:2PortraitLong portrait

PDF output (format: "pdf")

With format: "pdf", Shotbot produces a PDF document instead of a raster image. Image-specific options (output_size, ratio, crop_height, fullpage) are ignored: layout is governed by page size, orientation and scale. A 640 px WebP thumbnail is generated in parallel for the dashboard and preview links.

FieldTypeDefaultDescription
pdf_page_sizestringA4A4 · A3 · A5 · Letter · Legal · Tabloid
pdf_margin_mmint10Uniform margin applied to all 4 sides (0-50 mm)
pdf_scalefloat1.00Rendering scale factor (0.10-2.00)
pdf_landscapeboolfalseLandscape orientation if true, portrait otherwise
JSON
{
  "key":           "YOUR_API_KEY",
  "url":           "https://test.shotbot.net/invoice/12345",
  "format":        "pdf",
  "pdf_page_size": "A4",
  "pdf_margin_mm":  12,
  "pdf_scale":      1.00,
  "pdf_landscape":  false
}

The PDF is served at https://static.shotbot.net/{t1}/{t2}/{token}.pdf with Content-Type: application/pdf. The WebP thumbnail remains accessible at https://cache.shotbot.net/t/{token}/preview.

HTTP Basic authentication (http_auth)

To capture pages protected by a password (HTTP 401):

JSON
{
  "key": "your_key",
  "url": "https://test.shotbot.net/private-page",
  "http_auth": {
    "user": "login",
    "pass": "password"
  }
}

Credentials are erased from the database as soon as the capture completes. If a capture remains pending for more than 2 hours, they are deleted automatically. They are never returned in API responses.

Cookie injection (cookies) Pro

Inject cookies before navigation to capture authenticated pages (sessions, JWT tokens, preferences…) without HTTP Basic Auth.

JSON
{
  "key": "your_key",
  "url": "https://test.shotbot.net/dashboard",
  "cookies": [
    { "name": "session_id", "value": "abc123xyz" },
    { "name": "lang",       "value": "en", "domain": "test.shotbot.net", "path": "/" }
  ]
}
FieldTypeDescription
namestringCookie name (required, max 255 chars)
valuestringCookie value (required, max 4096 chars)
domainstringCookie domain (optional, defaults to target URL domain, max 255 chars)
pathstringCookie path (optional, defaults to "/", max 255 chars)

PHP example: APIv2 hosted

PHP
<?php
$api_key = "YOUR_API_KEY";
$url     = "https://test.shotbot.net";

// 1. Submit the capture
$ch = curl_init("https://api.shotbot.net/capture");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => ["Content-Type: application/json"],
    CURLOPT_POSTFIELDS     => json_encode([
        "key"            => $api_key,
        "url"            => $url,
        "format"         => "webp",
        "viewport_width" => 1280,
        "ratio"          => "16:9",
    ]),
]);
$res    = json_decode(curl_exec($ch), true);
curl_close($ch);

$token = $res["token"] ?? null;
if (!$token) { echo "Error: " . ($res["error"] ?? "unknown"); exit; }

// 2. Wait for result (polling)
do {
    sleep(3);
    $status = json_decode(file_get_contents(
        "https://api.shotbot.net/capture/" . $token
    ), true);
} while (($status["status"] ?? "") !== "done");

echo "<img src=\"" . htmlspecialchars($status["image"]) . "\" loading=\"lazy\">";
Example with extra options (ratio, HiDPI, ad blocking, lazy-load, HTTP auth)…
<?php
$ch = curl_init("https://api.shotbot.net/capture");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => ["Content-Type: application/json"],
    CURLOPT_POSTFIELDS     => json_encode([
        "key"                   => "YOUR_API_KEY",
        "url"                   => "https://test.shotbot.net",
        "format"                => "avif",
        "viewport_width"        => 390,       // iPhone 14 / 15
        "output_size"           => 390,
        "ratio"                 => "9:16",    // mobile portrait
        "hidpi"                 => true,
        "color_scheme"          => "dark",    // force prefers-color-scheme: dark
        "block_ads"             => true,      // block ad networks + trackers
        "dismiss_cookies"       => "reject",  // dismiss CMP banners
        "scroll_before_capture" => true,      // trigger lazy-load
        "wait_time"             => 20,
        "http_auth"             => ["user" => "admin", "pass" => "s3cr3t"],
    ]),
]);
$res = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $res["token"];

4. Account status

GET https://api.shotbot.net/account
GET https://api.shotbot.net/account?key={YOUR_KEY}

// Or with Authorization header
GET https://api.shotbot.net/account
Authorization: Bearer {YOUR_KEY}

Response

JSON
{
  "plan":            "free",     // "free" | "pro"
  "pro_until":       null,       // Unix timestamp (Pro only)
  "credit":          50,         // Extra credits beyond the monthly quota
  "quota_used":      12,         // Captures consumed today
  "quota_total":     200,        // Monthly quota ceiling
  "quota_remaining": 188,        // Captures still available
  "inflight":        1,          // Captures currently queued or processing
  "inflight_cap":    3           // Concurrency cap for this account tier
}

Use shotbot status in the CLI for a human-readable version of this endpoint.

Shotbot generates the screenshot and pushes it to your server in a single multipart request. No hosting on our side, images land directly with you, with no Shotbot branding. Included in the free account.

Submit a callback capture

POST https://api.shotbot.net/capture/callback
POST https://api.shotbot.net/capture/callback
Content-Type: application/json

{
  "key":             "your_api_key",
  "url":             "https://test.shotbot.net",
  "callback_url":    "https://your-site.com/shotbot-callback.php",
  "callback_secret": "a-random-secret",
  "format":          "webp",
  "viewport_width":  1280,
  "ratio":           "16:9"
}

Immediate response

JSON 202
{ "token": "a3f1c9…", "status": "queued", "eta_seconds": 20 }

Reception on your server

Once the capture is complete, Shotbot sends a POST multipart/form-data to your callback_url:

Fields received
$_POST["token"]          → 32-char alphanumeric token (unique capture identifier)
$_POST["url"]             → original captured URL
$_POST["status"]          → "OK" | "ERR"
$_POST["format"]          → image format (jpg, webp, etc.)
$_POST["callback_secret"] → secret echoed back as-is (only if provided on creation)
$_FILES["file"]           → binary image (requested size and format)

// No preview in callback mode | one image in the requested format only.

Constraints on callback_url

  • HTTPS required.
  • The URL must not resolve to a private IP address (SSRF protection).
  • Requests originate from IP 163.172.105.112, whitelist it in your firewall if needed. Up-to-date JSON list.

Callback-specific parameters

Same as the hosted APIv2, plus:

FieldTypeDescription
callback_urlstringHTTPS reception URL (required for this mode)
callback_secretstringOptional shared secret (≤ 255 chars) echoed back as-is in the reception POST (field callback_secret); allows verifying that the request originates from Shotbot

PHP example: callback mode

PHP, submission
<?php
// submit-capture.php, client side
$ch = curl_init("https://api.shotbot.net/capture/callback");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => ["Content-Type: application/json"],
    CURLOPT_POSTFIELDS     => json_encode([
        "key"             => "YOUR_API_KEY",
        "url"             => "https://test.shotbot.net",
        "callback_url"    => "https://your-site.com/shotbot-callback.php",
        "callback_secret" => "a-random-secret",
        "format"          => "webp",
        "viewport_width"  => 1280,
        "ratio"           => "16:9",
    ]),
]);
$res = json_decode(curl_exec($ch), true);
curl_close($ch);
echo "Capture: " . $res["token"];
PHP, reception (your server)
<?php
// shotbot-callback.php | receives the capture from Shotbot
$EXPECTED_SECRET = "a-random-secret"; // same as callback_secret sent

if (!hash_equals($EXPECTED_SECRET, $_POST["callback_secret"] ?? "")) {
    http_response_code(403);
    exit;
}

if (($_POST["status"] ?? "") !== "OK") {
    http_response_code(200);
    exit;
}

$token = preg_replace("/[^A-Za-z0-9]/", "", $_POST["token"] ?? "");
$format = in_array($_POST["format"] ?? "", ["jpg","png","webp","avif"])
        ? $_POST["format"] : "jpg";

if (!$token || !isset($_FILES["file"])) {
    http_response_code(400);
    exit;
}

$dest = __DIR__ . "/shots/" . $token . "." . $format;
move_uploaded_file($_FILES["file"]["tmp_name"], $dest);

http_response_code(200);
echo "OK";
White-label: images are hosted on your server. No Shotbot URLs are exposed to your users. Included free in all accounts.

Submit up to 500 URLs in a single request. Same options as the standard APIv2, applicable globally or per URL. Used for e-commerce (product catalogs), legal archiving (immutable timestamped snapshots, each capture remains accessible indefinitely and is never overwritten), and visual monitoring pipelines.

Limit: 500 URLs/request (Shotbot Pro: 5,000).

Planning an unusually large run (e.g. 500,000 captures at once)? Let us know before you launch - we'll prepare capacity on your end.

Submit a batch

POST https://api.shotbot.net/capture/batch
POST https://api.shotbot.net/capture/batch
Content-Type: application/json

{
  "key":  "your_api_key",

  "jobs": [
    { "url": "https://test.shotbot.net" },
    { "url": "https://other.com", "format": "png", "viewport_width": 390 }
  ],

  // Global options, applied to all captures, overridable per job
  "format":         "webp",
  "viewport_width": 1280,
  "ratio":          "16:9"
}

Response (HTTP 202)

JSON 202
{
  "submitted":    2,
  "waitlisted":   0,
  "deduplicated": 0,
  "errors":       0,
  "jobs": [
    { "index": 0, "url": "https://test.shotbot.net", "token": "a3f1c9…", "status": "queued" },
    { "index": 1, "url": "https://other.com",   "token": "b7e2d4…", "status": "queued" }
  ]
}

Possible statuses per capture

StatusMeaning
queuedCapture created, poll GET /capture/{token} for the result
queued + "deduplicated":trueURL already submitted recently (60 s) or twice in the same batch | existing token reused, no additional quota consumed
waitlistedMonthly quota and credits exhausted, processed at next reset
errorInvalid URL or incorrect parameter | other captures in the batch are not affected

Parameters

FieldLevelDescription
keyRootAPI key (required)
jobsRootArray of capture objects, max 500 (required)
jobs[n].urlPer captureURL to capture, http or https (required)
format, viewport_width, output_size, ratio, crop_height, fullpage, hidpi, color_scheme, prefers_reduced_motion, nojs, wait, dismiss_cookies, scroll_before_capture, scroll_offset_px, scroll_to, block_ads, autoplay_videos, omit_background, emulate_print_media, http_auth, cookies, selector, private, pdf_page_size, pdf_margin_mm, pdf_scale, pdf_landscapeRoot or per captureCapture options (see Hosted tab for allowed values) | per-capture value takes priority over the global default. private disables deduplication: two identical URLs in the same batch produce two distinct tokens.
callback_urlRootIf present (HTTPS, non-private): all captures switch to callback mode, images are pushed to this URL when each capture finishes
callback_secretRootOptional shared secret echoed back as-is in the reception POST, useful to verify request origin (only if callback_url is set)
idempotency_keyRootOptional client-supplied retry-safety key (16-128 chars [A-Za-z0-9_.-]). Replaying the same batch with the same key within 24h from the same account returns the original response (header X-Idempotent-Replay: true) without creating new jobs or deducting quota. Recommended: generate a UUID before the first call, reuse it on transient retries, drop it on success.

Global errors

HTTP codeErrorCause
400missing_jobsjobs field missing
400jobs_not_arrayjobs is not an array
400too_many_jobs + "max":500More than 500 URLs
400invalid_callback_urlInvalid callback URL, non-HTTPS or private address
400invalid_idempotency_keyIdempotency key out of format (16-128 chars [A-Za-z0-9_.-])
401invalid_keyIncorrect API key

PHP example: batch mode

PHP
<?php
$api_key = "YOUR_API_KEY";

$urls = [
    "https://test.shotbot.net",
    "https://github.com",
    "https://wikipedia.org",
    // ... up to 500
];

// Submit the batch
$ch = curl_init("https://api.shotbot.net/capture/batch");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => ["Content-Type: application/json"],
    CURLOPT_POSTFIELDS     => json_encode([
        "key"            => $api_key,
        "format"         => "webp",
        "viewport_width" => 1280,
        "ratio"          => "16:9",
        "jobs"           => array_map(fn($u) => ["url" => $u], $urls),
    ]),
]);
$res = json_decode(curl_exec($ch), true);
curl_close($ch);

echo "Submitted: {$res['submitted']} · Waitlisted: {$res['waitlisted']}\n";

// Collect tokens for created captures
$tokens = [];
foreach ($res["jobs"] as $capture) {
    if (isset($capture["token"])) {
        $tokens[$capture["url"]] = $capture["token"];
    }
}

// Wait and display results
foreach ($tokens as $url => $token) {
    do {
        sleep(3);
        $status = json_decode(file_get_contents(
            "https://api.shotbot.net/capture/" . $token
        ), true);
    } while (($status["status"] ?? "") !== "done");

    echo $url . " → " . $status["image"] . "\n";
}
Batch callback, images delivered to your server…
<?php
// All captures in the batch are pushed to your callback_url
$ch = curl_init("https://api.shotbot.net/capture/batch");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => ["Content-Type: application/json"],
    CURLOPT_POSTFIELDS     => json_encode([
        "key"          => "YOUR_API_KEY",
        "callback_url" => "https://your-site.com/shotbot-callback.php",
        "format"       => "webp",
        "jobs"         => [
            ["url" => "https://test.shotbot.net"],
            ["url" => "https://other.com", "ratio" => "1:1"],
        ],
    ]),
]);
$res = json_decode(curl_exec($ch), true);
curl_close($ch);
// No polling needed | each image will be POSTed to your callback_url
echo "Captures submitted: " . $res["submitted"];
Automatic deduplication. If the same URL appears twice in the batch, or was already submitted in the last 60 seconds, the existing token is reused at no additional quota cost.
One URL, that's it. The legacy API works with a simple HTTP GET: no library, no JSON, no polling. Integrate in 30 seconds from any language or shell script. It is maintained and will continue to work indefinitely. New integrations may also use the APIv2, Hosted tab for additional options.

Hosted mode (legacy)

Request

HTTP GET
https://add.shotbot.net/k={KEY}/url={ENCODED_URL}

// With options
https://add.shotbot.net/k={KEY}/format/webp/w/1280/fullpage/1/url={URL}

Display the screenshot

PHP
<!-- Format: https://static.shotbot.net/{md5[0]}/{md5[0:2]}/{md5}/{size}.jpg -->
$md5 = md5($url);
$img = "https://static.shotbot.net/{$md5[0]}/{$md5[0]..$md5[1]}/{$md5}/120.jpg";

Output sizes (fixed 4:3 ratio)

SizeDimensions
320320×240
240240×180
160160×120
120120×90
9292×69
8080×60

Options

ParameterValues
/nojs/1/Disable JavaScript
/format/webp/jpg · png · webp · avif
/w/1280/768 · 1024 · 1280 · 1600
/fullpage/1/Full page
/hidpi/1/Retina ×2
/darkmode/1/Dark mode

Return codes

CodeMeaning
OKCapture created or queued
OK waitlistQuota exceeded, added to waitlist
ERR keyInvalid key
ERR argsIncorrect parameters

Refresh a capture (update)

HTTP GET
https://update.shotbot.net/k={KEY}/[options/]{URL}
// Same parameters as add | re-captures without deduplication

Callback mode (legacy)

Captures are generated then delivered to your server via HTTP POST.

Request (your server → Shotbot)

HTTP POST
POST https://add.shotbot.net/callback.php

k={KEY}&uri={ENCODED_URL}&s=120&t=10&uri_callback={CALLBACK_URL}&nojs=0

Response: OK {id}. On error: standard return codes.

Reception (your server ← Shotbot)

POST received
$_POST["id"]      → job identifier
$_POST["uri_md5"] → MD5 of the captured URL
$_POST["uri"]     → captured URL
$_POST["width"]   → requested size (e.g. 120)
$_POST["status"]  → OK | ERR
$_FILES["file_120"] → JPEG image (name matches requested size)

Available sizes: 80 · 92 · 120 · 160 · 240 · 320. Your callback script must respond within 30 seconds.

Check quota (legacy)

GET
https://status.shotbot.net/k={KEY}
// → "OK 500:327:5000"  (quota:used:reserve)
Independent since 2008, hosted in France. Your data is stored in France: GDPR compliant, French jurisdiction, default rendering in France.

A question before integrating?

Quotas, formats, ratios, callbacks, batch, scheduling: the FAQ covers the most common questions asked by developers before their first request.

Frequently asked questions