Integration examples
Copy-paste the code in your language. Replace YOUR_API_KEY with your API key (12 characters, free).
PHP
Simple capture (hosted)
<?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
$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
$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
// 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
// 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
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)
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
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)
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)
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
<!-- 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
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
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))
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
# 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)
# 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.
# 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
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
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
# 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.
# 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
$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
$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
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
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
{
"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
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
pip install shotbot
npm install -g shotbot
Simple capture
shotbot capture --url=https://example.com # private + saved to the current directory
With options
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)
npx shotbot capture --url=https://example.com
CI / scripting
export SHOTBOT_API_KEY== htmlspecialchars($k) ?>
shotbot capture --url=https://example.com --output=before.png
# deploy…
shotbot capture --url=https://example.com --output=after.png
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
claude mcp add --scope user shotbot --transport http "https://api.shotbot.net/mcp?key== htmlspecialchars($k) ?>"
Available tools
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)
Take a screenshot of https://www.permalink.fr/ as WebP, viewport 1280, with the browser_chrome frame.
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
$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" => "= htmlspecialchars($k) ?>",
"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
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);
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_chromeTutorials, page shotsbrowser_chrome_darkTutorials on dark pagesmobileMobile responsive previewmobile_lightPhone, light chassistabletTablet responsive previewtablet_lightTablet, light chassislaptopDecks, product heroroundedSoft corners, same sizeshadowBlog posts, documentationpolaroidTestimonials, retro feelgradientSocial cards, OG imagesshotbot_brandShotbot brandingAvailable 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.
E-commerce & catalogs
Batch 500 URLs, AVIF, viewports, callback.
Visual monitoring
Scheduled 6h/24h/7d captures, visual diff.
Legal archiving
Immutable snapshots under a unique token.
AI agents
Native MCP for Claude Code, Claude Desktop, Cursor, Windsurf.
Claude Code
Install the MCP server in one command, then ask for screenshots in plain language.