Synchronous capture
curl --request POST \
--url https://screenshotly.app/api/screenshot \
--header 'x-api-key: <api-key>'import requests
url = "https://screenshotly.app/api/screenshot"
headers = {"x-api-key": "<api-key>"}
response = requests.post(url, headers=headers)
print(response.text)const options = {method: 'POST', headers: {'x-api-key': '<api-key>'}};
fetch('https://screenshotly.app/api/screenshot', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://screenshotly.app/api/screenshot",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"x-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://screenshotly.app/api/screenshot"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("x-api-key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://screenshotly.app/api/screenshot")
.header("x-api-key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://screenshotly.app/api/screenshot")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<api-key>'
response = http.request(request)
puts response.read_bodyAPI Reference
Synchronous capture
POST https://screenshotly.app/api/screenshot reference: every request field, plan requirement, response header, and error status for one-request captures.
POST
/
api
/
screenshot
Synchronous capture
curl --request POST \
--url https://screenshotly.app/api/screenshot \
--header 'x-api-key: <api-key>'import requests
url = "https://screenshotly.app/api/screenshot"
headers = {"x-api-key": "<api-key>"}
response = requests.post(url, headers=headers)
print(response.text)const options = {method: 'POST', headers: {'x-api-key': '<api-key>'}};
fetch('https://screenshotly.app/api/screenshot', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://screenshotly.app/api/screenshot",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"x-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://screenshotly.app/api/screenshot"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("x-api-key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://screenshotly.app/api/screenshot")
.header("x-api-key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://screenshotly.app/api/screenshot")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<api-key>'
response = http.request(request)
puts response.read_bodySend a JSON body to
The request body is limited to 64 KiB. The schema is strict: an unknown
top-level field returns
Page navigation times out after 30 seconds.
HTML and WebM output cannot be combined with
The signed
Failed requests are recorded for diagnostics but do not consume your capture
allowance. A response served from the cache is a successful capture and does.
https://screenshotly.app/api/screenshot with an API key in
x-api-key or Authorization: Bearer <key>. The response body is the capture
itself unless you request persisted delivery.
curl --fail-with-body https://screenshotly.app/api/screenshot \
-X POST \
-H "x-api-key: $SCREENSHOTLY_API_KEY" \
-H "content-type: application/json" \
-d '{"url":"https://example.com","format":"webp","quality":80,"fullPage":true}' \
--output example.webp
400, so check spelling before assuming an option is
unsupported. The machine-readable contract is the OpenAPI 3.1 document at
https://screenshotly.app/api/openapi.json.
Core fields
| Field | Type | Default | Bounds and notes |
|---|---|---|---|
url | string | required | HTTP or HTTPS, up to 2,048 characters. Must resolve to a public address. |
format | string | png | png, jpeg, webp, pdf, html, or webm |
device | string | desktop (1920×1080), laptop (1366×768), tablet (768×1024), or mobile (375×812). Overrides width and height. | |
width, height | integer | 1920×1080 | 320–3,840 and 240–3,840. Used only when both are set and device is not. |
deviceScaleFactor | number | 1 | 1–3. Multiplies output pixel dimensions. |
fullPage | boolean | false | Capture the full scrollable height |
quality | number | 100 | 0–100. Applies to jpeg and webp. |
delay | integer | 0 | 0–10,000 ms to wait after the page loads |
waitForSelector | string | Wait up to 10 seconds for a CSS selector. If it never appears, the capture continues. | |
waitForNetworkIdle | boolean | false | Wait until the network is idle instead of DOMContentLoaded |
selector | string | Capture only the first matching element. Returns 400 if it is not found. | |
clip | object | { "x", "y", "width", "height" } region in CSS pixels | |
darkMode | boolean | false | Emulate prefers-color-scheme: dark |
blockAds | boolean | false | Block common ad requests and hide common ad containers |
userAgent | string | Override the browser user agent | |
timezone | string | IANA time zone such as Europe/Berlin | |
videoDuration | integer | 5 | 1–15 seconds. Used only for webm. |
Plan-gated fields
A request that uses a feature outside the workspace owner’s plan returns403
with "error": "Plan upgrade required" and a minimumPlan value.
| Field | Minimum plan | Notes |
|---|---|---|
format: "pdf" | Basic | A4 page size, 1 cm margins, backgrounds printed |
format: "html" | Basic | Returns the rendered DOM as text/html |
headers | Basic | Up to 50 request headers sent to the target page |
cookies | Basic | Up to 50 cookies. Each domain must match the target host or a parent of it. |
stealth | Basic | Reduces common headless-browser signals |
cache, cacheTtl | Basic | Reuse an identical earlier capture for cacheTtl seconds (60–86,400, default 3,600) |
storage | Basic | { "enabled": true }; see Persisted delivery |
webhook | Basic | See Webhooks |
aiRemoval | Growth | See AI element removal |
hideSelectors | Growth | Up to 100 CSS selectors to hide before capture |
javascript | Growth | Up to 5,000 characters run in the page before capture |
scroll | Growth | Scrolls the page first to trigger lazy loading |
geolocation | Growth | { "latitude", "longitude" } |
mockup | Growth | One of six device frames; see Using mockups |
format: "webm" | Scale | A 1–15 second recording at 24 fps |
proxyRegion | Scale | us, eu, or apac; returns 503 if the region is not configured |
selector, clip, or mockup.
Mockups apply only to png, jpeg, and webp output.
Nested objects
{
"url": "https://example.com/account",
"headers": { "Accept-Language": "de-DE" },
"cookies": [
{ "name": "session", "value": "abc123", "domain": "example.com", "secure": true, "httpOnly": true }
],
"aiRemoval": { "enabled": true, "types": ["cookie-banner", "chat-widget"], "confidence": 0.8 },
"hideSelectors": [".promo-bar"],
"scroll": { "enabled": true, "direction": "vertical", "distance": 4000, "speed": 1000 },
"geolocation": { "latitude": 52.52, "longitude": 13.405 },
"webhook": { "url": "https://hooks.example.com/screenshotly", "secret": "whsec_example" }
}
aiRemoval.types: any ofcookie-banner,newsletter,chat-widget,social-overlay,ad. An empty list means all five.aiRemoval.confidence: 0–1, default0.8. Only detections at or above this score are removed.scroll.distance: 100–50,000 px, default 5,000.scroll.speed: 100–5,000 px per second, default 500.webhook.method:POST(default) orPUT.webhook.headersfollows the same rules asheaders.- These header names are rejected in
headersandwebhook.headers:Connection,Content-Length,Forwarded,Host,Proxy-Authorization,TE,Transfer-Encoding,Upgrade,X-Forwarded-For,X-Forwarded-Host, andX-Forwarded-Proto.
Responses
Capture bytes
A200 response contains the file with a matching Content-Type
(image/png, image/jpeg, image/webp, application/pdf, text/html, or
video/webm) and these headers:
| Header | Meaning |
|---|---|
Content-Disposition | attachment; filename="screenshot-<timestamp>.<ext>" |
X-Screenshot-Duration | Processing time in milliseconds |
X-Screenshot-Size | Body size in bytes |
X-RateLimit-Limit | Capture allowance for the current period |
X-RateLimit-Remaining | Successful captures left in the period |
Persisted delivery
With"storage": { "enabled": true } the capture is stored for the workspace’s
retention period (1–30 days) and the response is JSON instead of bytes:
{
"success": true,
"url": "https://screenshotly.app/api/assets/<asset-id>?expires=<unix-seconds>&signature=<signature>",
"format": "png",
"size": 482113,
"duration": 1840
}
url is valid for 24 hours. duration is in milliseconds.
Errors
Error bodies are JSON with anerror string and, usually, a message.
| Status | error | Cause |
|---|---|---|
400 | Invalid JSON | The body is not valid JSON. |
400 | Invalid request data | A field is unknown, has the wrong type, or is out of range. details lists the issues. |
400 | Unsafe URL | url or webhook.url is not a public HTTP(S) address. |
400 | Invalid screenshot options | For example, selector matched nothing or a cookie domain does not match the target. |
401 | Authentication failed | Missing, unknown, revoked, or expired key. |
403 | Authentication failed | The workspace owner’s email address is not verified. |
403 | Plan upgrade required | A requested feature needs a higher plan; see minimumPlan. |
413 | Payload too large | The body exceeds 64 KiB. |
429 | Rate limit exceeded | Allowance or per-minute throughput reached; honor Retry-After. |
500 | Screenshot generation failed | The page could not be captured, for example after a navigation timeout. |
503 | Capture proxy unavailable | The requested proxyRegion is not configured. |
Related
- Authentication and rate limits
- Async and batch jobs for captures that should not hold a request open
- Capturing screenshots for worked examples