Integration examples

Copy-paste the code in your language. Replace YOUR_API_KEY with your API key (12 characters, free).

Create a free account or sign in to see your API key pre-filled in the examples.
Prefer to see the code generated live? The playground generates these snippets in real time as you adjust the URL and options. Try the API →
Languages
Features
Tools

PHP

Simple capture (hosted)

PHP
<?php
$api_key = "YOUR_API_KEY";
$url     = "https://example.com";

// 1. Submit
$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",
    ]),
]);
$res = json_decode(curl_exec($ch), true);
curl_close($ch);

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

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

// 3. Use the image
echo "<img src=\"" . htmlspecialchars($status["image"]) . "\" loading=\"lazy\">";

Social card in 1 line (preset)

The preset parameter auto-fills viewport_width, output_size and crop_height. Available presets: og (1200×630), mobile (390×844), youtube_thumbnail (1280×720), square (1080×1080), reel (1080×1920), pinterest (1000×1500), tablet (768×1024), desktop_hd, twitter_header, linkedin_banner, hero_banner (last 4 require Pro).

PHP
<?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://example.com",
        "preset" => "og",   // 1200×630, OpenGraph card
    ]),
]);
$res = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $res["token"];

Capture with extra options

PHP
<?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://example.com",
        "format"         => "webp",
        "viewport_width" => 1280,
        "output_size"    => 640,
        "ratio"          => "16:9",
        "hidpi"          => true,
        "color_scheme"   => "dark",
        "render_region"  => "ca-montreal", // Pro: fr-paris (default) | ca-montreal | sg-singapore | au-sydney | vn-hanoi
        "fullpage"       => false,
    ]),
]);
$res = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $res["token"]; // token to track status

Callback mode (white-label)

To verify that the incoming POST genuinely comes from Shotbot, supply a callback_secret on submission | Shotbot will echo it back as-is in the reception POST (field callback_secret).

PHP, submission
<?php
// Submit a callback capture
$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://example.com",
        "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);
// $res["token"] available for optional tracking
PHP, reception (shotbot-callback.php)
<?php
// Shotbot sends a multipart POST when the capture is ready.
// Available fields: token, url, status, format, callback_secret
// File: $_FILES["file"] contains the image

$EXPECTED_SECRET = "a-random-secret"; // same as callback_secret sent
if (!hash_equals($EXPECTED_SECRET, $_POST["callback_secret"] ?? "")) {
    http_response_code(403);
    exit("Forbidden");
}

if (($_POST["status"] ?? "") !== "OK" || !isset($_FILES["file"])) {
    http_response_code(400);
    exit;
}

$token = preg_replace("/[^A-Za-z0-9]/", "", (string)($_POST["token"] ?? ""));
$ext   = preg_replace("/[^a-z]/", "", (string)($_POST["format"] ?? "webp"));

if ($token === "") {
    http_response_code(400);
    exit;
}

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

// Save to DB, notify a webhook, etc.
http_response_code(200);
echo "OK";

Python

Simple capture

Python
import requests, time

API_KEY = "YOUR_API_KEY"
url     = "https://example.com"

# 1. Submit
res = requests.post("https://api.shotbot.net/capture", json={
    "key": API_KEY, "url": url, "format": "webp",
    "viewport_width": 1280, "ratio": "16:9",
})
res.raise_for_status()
token = res.json()["token"]

# 2. Poll
while True:
    time.sleep(2)
    status = requests.get(f"https://api.shotbot.net/capture/{token}").json()
    if status["status"] == "done":
        print(status["image"])
        break

Social card in 1 line (preset)

Python
import requests

res = requests.post("https://api.shotbot.net/capture", json={
    "key": "YOUR_API_KEY",
    "url": "https://example.com",
    "preset": "og",   # 1200x630, OpenGraph card
})
token = res.json()["token"]

Download the screenshot

Python
import requests, time

API_KEY = "YOUR_API_KEY"
url     = "https://example.com"

res   = requests.post("https://api.shotbot.net/capture", json={"key": API_KEY, "url": url})
token = res.json()["token"]

while True:
    time.sleep(2)
    s = requests.get(f"https://api.shotbot.net/capture/{token}").json()
    if s["status"] == "done": break

img_data = requests.get(s["image"]).content
with open("capture.webp", "wb") as f:
    f.write(img_data)
print("Screenshot saved.")

Node.js

Capture and polling (ESM)

Node.js (ESM)
const API_KEY = "YOUR_API_KEY";
const url     = "https://example.com";

// 1. Submit
const res = await fetch("https://api.shotbot.net/capture", {
  method:  "POST",
  headers: { "Content-Type": "application/json" },
  body:    JSON.stringify({ key: API_KEY, url, format: "webp", viewport_width: 1280 }),
});
const { token: token } = await res.json();

// 2. Poll
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
let image;
while (true) {
  await wait(2000);
  const s = await fetch(`https://api.shotbot.net/capture/${token}`).then((r) => r.json());
  if (s.status === "done") { image = s.image; break; }
}
console.log(image);

Social card in 1 line (preset)

Node.js (ESM)
const res = await fetch("https://api.shotbot.net/capture", {
  method:  "POST",
  headers: { "Content-Type": "application/json" },
  body:    JSON.stringify({
    key:    "YOUR_API_KEY",
    url:    "https://example.com",
    preset: "og",   // 1200×630, OpenGraph card
  }),
});
const { token } = await res.json();

Browser-side polling

HTML + JS
<!-- Your backend submits the capture and stores the token.
     This script watches the status and updates the image when ready. -->
<script>
async function waitForCapture(token, imgEl) {
  while (true) {
    await new Promise((r) => setTimeout(r, 2000));
    const s = await fetch(`https://api.shotbot.net/capture/${token}`).then((r) => r.json());
    if (s.status === "done") { imgEl.src = s.image; return; }
  }
}
const img = document.getElementById("shot");
if (img) waitForCapture(img.dataset.capture, img);
</script>
<img id="shot" data-capture="a3f1c9…" alt="Screenshot" loading="lazy">

Go

Simple capture

Go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"time"
)

const apiKey = "YOUR_API_KEY"

func main() {
	// 1. Submit
	payload, _ := json.Marshal(map[string]any{
		"key":    apiKey,
		"url":    "https://example.com",
		"format": "webp",
	})
	resp, _ := http.Post("https://api.shotbot.net/capture",
		"application/json", bytes.NewReader(payload))
	defer resp.Body.Close()

	var capture struct {
		Token string `json:"token"`
	}
	json.NewDecoder(resp.Body).Decode(&capture)

	// 2. Poll
	for {
		time.Sleep(2 * time.Second)
		r, _ := http.Get("https://api.shotbot.net/capture/" + capture.Token)
		body, _ := io.ReadAll(r.Body)
		r.Body.Close()

		var s map[string]any
		json.Unmarshal(body, &s)
		if s["status"] == "done" {
			fmt.Println(s["image"])
			return
		}
	}
}

Callback mode

Go, submission
payload, _ := json.Marshal(map[string]any{
	"key":             apiKey,
	"url":             "https://example.com",
	"callback_url":    "https://your-site.com/shotbot-callback",
	"callback_secret": "a-random-secret",
	"format":          "webp",
	"ratio":           "16:9",
})
http.Post("https://api.shotbot.net/capture/callback",
	"application/json", bytes.NewReader(payload))
Go, reception
const expectedSecret = "a-random-secret"

http.HandleFunc("/shotbot-callback", func(w http.ResponseWriter, r *http.Request) {
	r.ParseMultipartForm(32 << 20) // 32 MB max

	// Verify shared secret
	if subtle.ConstantTimeCompare(
		[]byte(r.FormValue("callback_secret")),
		[]byte(expectedSecret),
	) != 1 {
		http.Error(w, "forbidden", http.StatusForbidden)
		return
	}

	if r.FormValue("status") != "OK" {
		http.Error(w, "not OK", http.StatusBadRequest)
		return
	}
	token := r.FormValue("token")
	ext   := r.FormValue("format")

	file, _, err := r.FormFile("file")
	if err != nil {
		http.Error(w, "no file", http.StatusBadRequest)
		return
	}
	defer file.Close()

	dst, _ := os.Create("shots/" + token + "." + ext)
	defer dst.Close()
	io.Copy(dst, file)

	fmt.Fprintln(w, "OK")
})

cURL

Simple capture

Shell
# 1. Submit (returns token)
curl -s -X POST https://api.shotbot.net/capture \
  -H "Content-Type: application/json" \
  -d '{"key":"YOUR_API_KEY","url":"https://example.com","format":"webp"}'
# {"token":"a3f1c9...","status":"queued","eta_seconds":20}

# 2. Check status
curl -s "https://api.shotbot.net/capture/a3f1c9..."
# {"status":"done","image":"https://static.shotbot.net/..."}

# 3. Download
curl -o capture.webp "https://static.shotbot.net/a/a3/a3f1c9….webp"

Social card in 1 line (preset)

Shell
# preset=og auto-fills viewport_width / output_size / crop_height (1200×630).
curl -s -X POST https://api.shotbot.net/capture \
  -H "Content-Type: application/json" \
  -d '{"key":"YOUR_API_KEY","url":"https://example.com","preset":"og"}'

Decorative frame around the image (frame)

The frame parameter composites a decorative frame around the image server-side. Variants: brackets, shadow, browser_chrome, mobile (smartphone mockup), tablet, shotbot_brand. Ignored for PDF. Free accounts get a small shotbot.fr mark bottom-right (except shotbot_brand, which includes the brand) | removed with Shotbot Pro.

Shell
# OG card (1200×630) with drop shadow
curl -s -X POST https://api.shotbot.net/capture \
  -H "Content-Type: application/json" \
  -d '{"key":"YOUR_API_KEY","url":"https://example.com","preset":"og","frame":"shadow"}'

# Mobile screenshot inside a phone mockup (great for mobile-app landing pages)
curl -s -X POST https://api.shotbot.net/capture \
  -H "Content-Type: application/json" \
  -d '{"key":"YOUR_API_KEY","url":"https://example.com","preset":"mobile","frame":"mobile"}'

With extra options

Shell
curl -s -X POST https://api.shotbot.net/capture \
  -H "Content-Type: application/json" \
  -d '{"key":"YOUR_API_KEY","url":"https://example.com",
       "format":"avif","viewport_width":390,"ratio":"9:16",
       "hidpi":true,"color_scheme":"dark",
       "block_ads":true,"dismiss_cookies":"reject","scroll_before_capture":true,
       "scroll_offset_px":600,"wait_time":25}'

Callback mode

Shell
curl -s -X POST https://api.shotbot.net/capture/callback \
  -H "Content-Type: application/json" \
  -d '{"key":"YOUR_API_KEY","url":"https://example.com",
       "callback_url":"https://your-site.com/callback",
       "callback_secret":"a-random-secret",
       "format":"webp","ratio":"16:9"}'

PDF output

Shell
# A4 portrait PDF, 10mm margin
curl -s -X POST https://api.shotbot.net/capture \
  -H "Content-Type: application/json" \
  -d '{"key":"YOUR_API_KEY","url":"https://example.com/invoice/12345",
       "format":"pdf","pdf_page_size":"A4","pdf_margin_mm":10,
       "pdf_scale":1.00,"pdf_landscape":false}'

# Retrieve the PDF once ready:
# https://static.shotbot.net/{t1}/{t2}/{token}.pdf

Private capture (never on the CDN)

Pass "private":true when the page being captured is sensitive (intranet, dashboards, customer data). The result is not published on static.shotbot.net; the status response carries a download URL that streams the file. You can fetch it any number of times until it auto-expires, after which it returns 410 Gone.

Shell
# 1. Submit with private:true
TOKEN=$(curl -s -X POST https://api.shotbot.net/capture \
  -H "Content-Type: application/json" \
  -d '{"key":"YOUR_API_KEY","url":"https://intranet.example.com",
       "format":"webp","private":true}' | jq -r .token)

# 2. Poll status | done responses include `download`, not `image`/`preview`
curl -s "https://api.shotbot.net/capture/$TOKEN"
# {"status":"done","private":true,
#  "download":"https://api.shotbot.net/capture/.../file", ...}

# 3. Fetch the bytes (repeatable until the file auto-expires server-side)
curl -o capture.webp "https://api.shotbot.net/capture/$TOKEN/file"

Batch : up to 500 URLs in one request

The batch endpoint processes up to 500 URLs in a single request (5,000 with Shotbot Pro). Global options apply to all captures; each individual capture can override them. Automatic deduplication avoids counting the same URL twice in the same batch.

Fair scheduling: the batch queue (Q3) uses a fair-share scheduler: each time a worker slot opens, it picks the oldest job from the client with the fewest captures currently in flight. A large batch does not monopolise all workers; other clients keep progressing in parallel. For a reserved queue with no sharing, see Shotbot Dedicated.

Page size limit: pages whose HTML document exceeds 10 MB (20 MB with Shotbot Pro) are automatically rejected with error code response_too_large. The job fails in seconds rather than after a 30 s navigation timeout, sparing worker resources.

PHP : simple batch

PHP
<?php
$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",
        "format"         => "webp",     // default for all captures
        "viewport_width" => 1280,
        "ratio"          => "16:9",
        "jobs"           => [
            ["url" => "https://example.com"],
            ["url" => "https://example.org"],
            ["url" => "https://example.net"],
        ],
    ]),
]);
$res = json_decode(curl_exec($ch), true);
curl_close($ch);

echo "Submitted: " . $res["submitted"] . "\n";
echo "Deduplicated: " . $res["deduplicated"] . "\n";
echo "Waitlisted: " . $res["waitlisted"] . "\n";

foreach ($res["jobs"] as $capture) {
    echo $capture["url"] . " → " . $capture["token"] . "\n";
}

PHP : with per-capture options and callback

PHP
<?php
$urls = [
    ["url" => "https://shop.example.com/product-1"],
    ["url" => "https://shop.example.com/product-2", "viewport_width" => 390, "ratio" => "9:16"],
    ["url" => "https://shop.example.com/product-3", "format" => "avif"],
    // ... up to 500
];

$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",
        "format"          => "webp",        // default, overridable per capture
        "callback_url"    => "https://your-site.com/batch-callback",
        "callback_secret" => "a-random-secret",
        "jobs"            => $urls,
    ]),
]);
$res = json_decode(curl_exec($ch), true);
curl_close($ch);
// Shotbot notifies callback_url for each completed capture.
// callback_secret is echoed back as-is to verify origin.

Python : batch

Python
import requests

res = requests.post("https://api.shotbot.net/capture/batch", json={
    "key":    "YOUR_API_KEY",
    "format": "webp",
    "jobs": [
        {"url": "https://example.com"},
        {"url": "https://example.org", "viewport_width": 390},
        {"url": "https://example.net", "hidpi": True},
    ],
})
data = res.json()
print(f"Submitted: {data['submitted']} | Deduplicated: {data['deduplicated']}")
for capture in data["jobs"]:
    print(capture["url"], "→", capture["token"])

cURL : batch

Shell
curl -s -X POST https://api.shotbot.net/capture/batch \
  -H "Content-Type: application/json" \
  -d '{"key":"YOUR_API_KEY","format":"webp","jobs":[
    {"url":"https://example.com"},
    {"url":"https://example.org"},
    {"url":"https://example.net"}
  ]}'
# {"submitted":3,"waitlisted":0,"deduplicated":0,"errors":[],"jobs":[...]}

Batch response format

JSON
{
  "submitted":    3,
  "waitlisted":   0,
  "deduplicated": 0,
  "errors":       [],
  "jobs": [
    { "index": 0, "url": "https://example.com",  "token": "a3f1c9…", "status": "queued" },
    { "index": 1, "url": "https://example.org",  "token": "b7e2d1…", "status": "queued" },
    { "index": 2, "url": "https://example.net",  "token": "c8f4a0…", "status": "queued" }
  ]
}

Batch captures go to a dedicated queue (Queue 3), isolated from single captures. With a callback_url, each completed capture notifies your server individually. Without callback, poll each token via GET /capture/{token}.

Want each capture to land directly in your bucket? See the Export to S3 / Scaleway / R2 guide.

Ruby

Simple capture

Ruby
require "net/http"
require "json"

API_KEY = "YOUR_API_KEY"
url     = "https://example.com"

# 1. Submit
uri = URI("https://api.shotbot.net/capture")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
req.body = { key: API_KEY, url: url, format: "webp", viewport_width: 1280 }.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
token = JSON.parse(res.body)["token"]

# 2. Poll
loop do
  sleep 2
  s = JSON.parse(Net::HTTP.get(URI("https://api.shotbot.net/capture/#{token}")))
  break puts s["image"] if s["status"] == "done"
end

CLI

Install

Python
pip install shotbot
Node.js
npm install -g shotbot

Simple capture

CLI
shotbot capture --url=https://example.com   # private + saved to the current directory

With options

CLI
shotbot capture --url=https://example.com --format=webp --viewport=1440 --full-page
shotbot capture --url=https://example.com --output=shot.png   # choose the filename
shotbot capture --url=https://example.com --cdn              # public CDN URL, no local file
shotbot capture --url=https://example.com --color-scheme=dark --wait=5

No install (npx)

Node.js
npx shotbot capture --url=https://example.com

CI / scripting

Shell
export SHOTBOT_API_KEY=

shotbot capture --url=https://example.com --output=before.png
# deploy…
shotbot capture --url=https://example.com --output=after.png

Full CLI documentation →

MCP Server

Shotbot exposes an MCP server (Model Context Protocol) natively supported by Claude Code, Claude Desktop, Cursor, and Windsurf. Ask for screenshots in plain language, no code required.

Install

Shell
claude mcp add --scope user shotbot --transport http "https://api.shotbot.net/mcp?key="

Available tools

MCP
capture        — capture a URL (format, viewport, frame, region…)
batch          — submit up to 5,000 URLs in one request
get_status     — poll a capture by token
account_status — remaining quota, Pro status, batch limit

Example (Claude Code)

Prompt
Take a screenshot of https://www.permalink.fr/ as WebP, viewport 1280, with the browser_chrome frame.

Full MCP server documentation →

Export to S3

Push each screenshot directly into your bucket via callback. Shotbot posts the file to your endpoint as soon as the render is ready. Works with AWS S3, Scaleway, OVH, Cloudflare R2, Backblaze B2.

Submit with callback_url

PHP, submission
<?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"             => "",
        "url"             => "https://example.com",
        "format"          => "webp",
        "callback_url"    => "https://yourapp.com/callback-s3.php",
        "callback_secret" => "your_secret_here",
    ]),
]);
$res = json_decode(curl_exec($ch), true);

Callback handler (PHP)

PHP, reception
<?php
require 'vendor/autoload.php'; // composer require aws/aws-sdk-php
use Aws\S3\S3Client;

if (!hash_equals('your_secret_here', $_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'] ?? '');
$fmt   = $_POST['format'] ?? 'jpg';

$s3 = new S3Client(['version'=>'latest','region'=>'us-east-1',
      'endpoint'=>null, // swap for Scaleway/OVH/R2/B2
      'credentials'=>['key'=>getenv('S3_KEY'),'secret'=>getenv('S3_SECRET')]]);

$s3->putObject(['Bucket'=>'my-bucket',
    'Key'         => 'screenshots/' . date('Y/m/d/') . $token . '.' . $fmt,
    'Body'        => fopen($_FILES['file']['tmp_name'], 'rb'),
    'ContentType' => 'image/' . $fmt]);

http_response_code(200);

Full guide + Python + all providers →

Frames

The frame parameter wraps the screenshot in a decorative frame or device mockup (image formats only, ignored for PDF). Available values, grouped by use:

Browser
browser_chromeTutorials, page shots
browser_chrome_darkTutorials on dark pages
Devices
mobileMobile responsive preview
mobile_lightPhone, light chassis
tabletTablet responsive preview
tablet_lightTablet, light chassis
laptopDecks, product hero
Decorative
roundedSoft corners, same size
shadowBlog posts, documentation
polaroidTestimonials, retro feel
gradientSocial cards, OG images
Branding
shotbot_brandShotbot branding

Available on all plans. Clean frames (no watermark) with Shotbot Pro.

JSON{
  "url": "https://example.com",
  "format": "png",
  "frame": "browser_chrome"
}

Gallery of real renders: frames on real captures →

Using the legacy API (add.shotbot.net)? v1 examples are in the Legacy API tab of the documentation.