Documentation

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

GET https://shotpilot.dev/api/v1/screenshot

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

MethodPathDescription
GET/api/v1/screenshotCapture a screenshot of any URL

Parameters

All parameters are query string values.

ParamTypeDefaultDescription
urlrequiredstringTarget 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_dismissbooleanfalseRuns 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_ttlinteger86400Lifetime 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_msinteger0Additional 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.
formatstringpngOutput 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.
freshbooleanfalseWhen 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_pagebooleanfalseCaptures 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.
heightinteger720Viewport 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.
hidestringComma-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_backgroundbooleanfalseConverts 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.
widthinteger1280Viewport 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.
zoomnumber100Browser-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/pdf
X-CacheHIT (returned from cache) or MISS (freshly captured)
X-RateLimit-UsedScreenshots used this month
X-RateLimit-LimitYour monthly quota
X-RateLimit-RemainingRemaining requests per second
Cache-Controlpublic, max-age=<cache_ttl>

Error codes

Errors return JSON: { "error": "message" }

StatusMeaningWhen
400Bad requestMissing or invalid url, blocked hostname, unsupported protocol
401UnauthorizedMissing Authorization header or invalid API key
422UnprocessablePage failed to load (DNS failure, connection refused, etc.)
429Too many requestsPer-second rate limit or monthly quota exceeded
504Gateway timeoutPage navigation timed out after 15 seconds
500Internal errorUnexpected server error

Examples

Basic PNG
curl -H "Authorization: Bearer sp_live_YOUR_KEY" \
  "https://shotpilot.dev/api/v1/screenshot?url=https://example.com" \
  -o shot.png
Full-page JPEG
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
Mobile viewport (iPhone 14)
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
Bypass cache for fresh shot
curl -H "Authorization: Bearer sp_live_YOUR_KEY" \
  "https://shotpilot.dev/api/v1/screenshot?url=https://example.com&fresh=true" \
  -o fresh.png
Wait 2s before capture (JS-heavy sites)
curl -H "Authorization: Bearer sp_live_YOUR_KEY" \
  "https://shotpilot.dev/api/v1/screenshot?url=https://example.com&delay_ms=2000" \
  -o shot.png
PDF export of full page
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.

Node.js (fetch)
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()));
Python (requests)
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)
Go (net/http)
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 (cURL)
<?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);
Ruby (net/http)
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

PlanMonthly quotaPer-second limit
Free1001 req/s
Starter ($19/mo)5,0005 req/s
Pro ($49/mo)25,00020 req/s
Business ($149/mo)150,00050 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.