API Reference
ShotPilot gives you a single HTTP endpoint that turns any URL into a screenshot. No SDKs, no configuration — just an API key and a GET request.
Overview
Returns image bytes directly with the appropriate Content-Type header. Save the response body to a file — no JSON parsing needed.
Authentication
Pass your API key as a Bearer token in the Authorization header. Get your key from the dashboard.
Authorization: Bearer sp_live_YOUR_KEY
Keys start with sp_live_. Never expose them client-side.
Endpoint
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/screenshot | Capture a screenshot of any URL |
Parameters
All parameters are query string values.
| Param | Type | Default | Description |
|---|---|---|---|
urlrequired | string | — | Target page to capture. Must be a fully qualified http:// or https:// URL. Localhost, private network, and link-local addresses are blocked for safety. URL-encode any special characters in query strings. |
auto_dismiss | boolean | false | Runs a server-side heuristic that finds and hides fullscreen modal overlays (high-z-index fixed elements with backdrops) and cookie/consent bars detected by text content ("cookie", "consent", "gdpr", etc.) sitting near the viewport edge. Useful for sites with obfuscated class names where the hide param can't target them by selector. May affect intentional layouts that mimic these patterns — combine with hide for surgical control. |
cache_ttl | integer | 86400 | Lifetime in seconds for the cached screenshot in Cloudflare R2. Subsequent requests with identical params (url, width, height, full_page, format, hide, zoom) return the cached PNG in under 50ms. Range: 0 to 604800 (7 days). Pass 0 to skip caching for this request. Use fresh=true to bypass an existing cache entry without changing TTL. |
delay_ms | integer | 0 | Additional milliseconds to wait after networkidle before capturing. Useful for JS-heavy single-page apps that render content via animation or after a setTimeout. Range: 0 to 10000. Typical values: 500ms for hydration, 2000ms for entrance animations, 5000ms for slow-loading SPAs. |
format | string | png | Output format. Accepts png (lossless, largest, default), jpg/jpeg (lossy, smallest, no transparency), webp (modern, good compression), or pdf (rendered via Chromium print pipeline — preserves vector text, A4 sizing when full_page=true). Response Content-Type matches: image/png, image/jpeg, image/webp, or application/pdf. |
fresh | boolean | false | When true, bypasses any cached entry and always launches Chromium for a new capture. The new screenshot is then written back to cache (replacing the old entry) using cache_ttl. Use this when you need to see updates on a page that recently changed. Each fresh=true call uses compute time and counts toward your monthly quota. |
full_page | boolean | false | Captures the full scrollable document height instead of just the viewport. When true, ShotPilot auto-scrolls the page in chunks before capturing to trigger lazy-loaded images and IntersectionObserver-driven reveal animations on modern marketing sites. Resulting image height is capped at 60,000px. Often combined with persist_background for sites that use background-attachment: fixed. |
height | integer | 720 | Viewport height in pixels — the visible window dimensions Chromium emulates before navigating. When full_page=false, this is also the output image height. When full_page=true, it only determines which elements respond as "above the fold" but the output image grows to fit the document. Range: 100 to 2160. |
hide | string | — | Comma-separated CSS selectors to remove from the DOM before capture using display:none. Use this to drop cookie banners, login walls, paywalls, intercoms, or any persistent overlay you know the selector for. Up to 20 selectors per request. Selectors containing { } < > are rejected for safety. Example: hide=.cookie-banner,[role=dialog],#newsletter-modal. |
persist_background | boolean | false | Converts elements with background-attachment: fixed to background-attachment: scroll, and forces backgrounds to tile across the full document height. Without this, full-page captures of sites with static hero backgrounds (parallax effects, fixed dungeon-art wikis) only show the background image at the top of the stitched screenshot. Trade-off: may show seams where tiled backgrounds repeat on sites whose bg is sized to one viewport. |
width | integer | 1280 | Viewport width in pixels — the simulated browser window width. Affects responsive layouts: 375 emulates iPhone-class, 768 iPad-class, 1280 laptop, 1920 desktop. Range: 100 to 3840. Combined with deviceScaleFactor (currently fixed at 1) this is also the output image width. |
zoom | number | 100 | Browser-style page zoom applied via document.documentElement.style.zoom. Accepts either percent (25–500) or decimal (0.25–5.0) — values >5 are interpreted as percent. Layout reflows correctly (unlike CSS transform: scale), so click targets and text wrapping stay accurate. Useful for capturing dashboard-style pages at a tighter zoom to fit more content per request. |
Response
On success, returns raw image or PDF bytes with status 200. Notable response headers:
Content-Typeimage/png, image/jpeg, image/webp, or application/pdfX-CacheHIT (returned from cache) or MISS (freshly captured)X-RateLimit-UsedScreenshots used this monthX-RateLimit-LimitYour monthly quotaX-RateLimit-RemainingRemaining requests per secondCache-Controlpublic, max-age=<cache_ttl>Error codes
Errors return JSON: { "error": "message" }
| Status | Meaning | When |
|---|---|---|
| 400 | Bad request | Missing or invalid url, blocked hostname, unsupported protocol |
| 401 | Unauthorized | Missing Authorization header or invalid API key |
| 422 | Unprocessable | Page failed to load (DNS failure, connection refused, etc.) |
| 429 | Too many requests | Per-second rate limit or monthly quota exceeded |
| 504 | Gateway timeout | Page navigation timed out after 15 seconds |
| 500 | Internal error | Unexpected server error |
Examples
curl -H "Authorization: Bearer sp_live_YOUR_KEY" \ "https://shotpilot.dev/api/v1/screenshot?url=https://example.com" \ -o shot.png
curl -H "Authorization: Bearer sp_live_YOUR_KEY" \ "https://shotpilot.dev/api/v1/screenshot?url=https://example.com&format=jpg&full_page=true" \ -o shot.jpg
curl -H "Authorization: Bearer sp_live_YOUR_KEY" \ "https://shotpilot.dev/api/v1/screenshot?url=https://example.com&width=390&height=844" \ -o mobile.png
curl -H "Authorization: Bearer sp_live_YOUR_KEY" \ "https://shotpilot.dev/api/v1/screenshot?url=https://example.com&fresh=true" \ -o fresh.png
curl -H "Authorization: Bearer sp_live_YOUR_KEY" \ "https://shotpilot.dev/api/v1/screenshot?url=https://example.com&delay_ms=2000" \ -o shot.png
curl -H "Authorization: Bearer sp_live_YOUR_KEY" \ "https://shotpilot.dev/api/v1/screenshot?url=https://example.com&format=pdf&full_page=true" \ -o page.pdf
SDKs & samples
No SDK required — the API is plain HTTP. Drop in one of these snippets from your language of choice. Each writes the response body to disk.
import { writeFile } from "node:fs/promises";
const res = await fetch(
"https://shotpilot.dev/api/v1/screenshot?url=https://example.com&full_page=true",
{ headers: { Authorization: `Bearer ${process.env.SHOTPILOT_KEY}` } },
);
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
await writeFile("shot.png", Buffer.from(await res.arrayBuffer()));import os, requests
r = requests.get(
"https://shotpilot.dev/api/v1/screenshot",
params={"url": "https://example.com", "full_page": "true"},
headers={"Authorization": f"Bearer {os.environ['SHOTPILOT_KEY']}"},
timeout=30,
)
r.raise_for_status()
with open("shot.png", "wb") as f:
f.write(r.content)package main
import (
"io"
"net/http"
"os"
)
func main() {
req, _ := http.NewRequest("GET",
"https://shotpilot.dev/api/v1/screenshot?url=https://example.com&full_page=true", nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("SHOTPILOT_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
f, _ := os.Create("shot.png")
defer f.Close()
io.Copy(f, res.Body)
}<?php
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => "https://shotpilot.dev/api/v1/screenshot?url=https://example.com&full_page=true",
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("SHOTPILOT_KEY")],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
]);
$body = curl_exec($ch);
curl_close($ch);
file_put_contents("shot.png", $body);require "net/http"
require "uri"
uri = URI("https://shotpilot.dev/api/v1/screenshot?url=https://example.com&full_page=true")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{ENV.fetch("SHOTPILOT_KEY")}"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
File.binwrite("shot.png", res.body)Rate limits
| Plan | Monthly quota | Per-second limit |
|---|---|---|
| Free | 100 | 1 req/s |
| Starter ($19/mo) | 5,000 | 5 req/s |
| Pro ($49/mo) | 25,000 | 20 req/s |
| Business ($149/mo) | 150,000 | 50 req/s |
When rate limited, the API returns 429 with a Retry-After header (seconds to wait). Monthly quotas reset on the 1st of each month UTC.