Screenshots
The Screenshots API is the core of ScreenshotRun. It lets you capture any website, raw HTML, or Markdown as an image or PDF, then retrieve, list, or delete screenshots you have created. All endpoints require authentication.
This page covers every endpoint. For the full list of capture parameters (viewport, format, device emulation, and more), see Screenshot Options.
How It Works
There are two ways to capture a screenshot:
- Synchronous (Quick Capture) — send a request and get the image back directly. No polling, no waiting. Best for simple integrations where you need the image right away.
- Asynchronous (Create) — send a request and get a screenshot ID back immediately. The screenshot is processed in the background. You can poll for its status or use a webhook to get notified when it is ready. Best for high-volume workloads or when you don't need the image instantly.
How long does a screenshot take?
The API opens a real browser, loads the page, waits for it to render, and captures the image. This means response time depends heavily on the target website. Here is what to expect in practice:
| Page type | Typical time | Examples |
|---|---|---|
| Simple / static HTML | 1–2 s | Landing pages, documentation, text-heavy sites |
| Medium complexity | 2–5 s | News sites, GitHub, Wikipedia |
| JS-heavy / SPA | 5–8 s | Stripe, Vercel, React/Vue apps, Shopify |
| Full-page, long content | Up to 10+ s | Long Wikipedia articles, full documentation pages |
These are real numbers measured from our servers. The processing_time_ms field in every response tells you exactly how long the browser took for that specific capture.
Quick decision guide: sync vs. async
Not sure which mode to use? Pick based on your situation:
| Situation | Recommended mode |
|---|---|
| Building a tool that generates one screenshot on user action | Sync (/capture) |
Embedding a screenshot directly in an <img> tag or PDF | Sync (/capture) |
| Capturing many URLs in a loop or cron job | Async (POST /screenshots) |
| Monitoring dozens or hundreds of pages | Async + webhooks |
| Your backend cannot wait more than a few seconds for a response | Async (POST /screenshots) |
| You are capturing complex, JS-heavy sites | Async (more reliable, no HTTP timeout risk) |
Quick Capture
GET /v1/screenshots/capture
POST /v1/screenshots/capture
The simplest way to take a screenshot. The API waits for the capture to finish and returns the image file directly — no polling needed.
Use GET with query parameters for simple requests. Use POST with a JSON body when sending large payloads like html or markdown (GET query strings have length limits).
Both methods accept the same parameters.
Most HTTP libraries default to a 5–10 second timeout. Because /capture holds the connection open while the browser loads the page, a short timeout in your HTTP client will cause the request to fail with a connection error — even though our server is working fine.
Set your client timeout to 30–60 seconds when using the sync endpoint. If you cannot change the timeout in your environment, use async mode instead.
Setting the HTTP client timeout
Here is how to configure it in the most common languages:
JavaScript (fetch)
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 60_000); // 60 seconds
const res = await fetch("https://api.screenshotrun.com/v1/screenshots/capture?url=https://example.com", {
headers: { "Authorization": "Bearer YOUR_API_KEY" },
signal: controller.signal,
});
clearTimeout(timeout);
JavaScript (axios)
const res = await axios.get("https://api.screenshotrun.com/v1/screenshots/capture", {
params: { url: "https://example.com" },
headers: { Authorization: "Bearer YOUR_API_KEY" },
timeout: 60_000, // 60 seconds
responseType: "arraybuffer", // for binary image response
});
Python (requests)
import requests
response = requests.get(
"https://api.screenshotrun.com/v1/screenshots/capture",
params={"url": "https://example.com"},
headers={"Authorization": "Bearer YOUR_API_KEY"},
timeout=60, # seconds
stream=True,
)
with open("screenshot.png", "wb") as f:
f.write(response.content)
PHP (Guzzle / Laravel HTTP)
// Laravel HTTP client
$response = Http::withToken(env('SCREENSHOT_API_KEY'))
->timeout(60) // seconds
->get('https://api.screenshotrun.com/v1/screenshots/capture', [
'url' => 'https://example.com',
]);
file_put_contents('screenshot.png', $response->body());
// Guzzle directly
$client = new \GuzzleHttp\Client(['timeout' => 60]);
$response = $client->get('https://api.screenshotrun.com/v1/screenshots/capture', [
'query' => ['url' => 'https://example.com'],
'headers' => ['Authorization' => 'Bearer YOUR_API_KEY'],
'sink' => 'screenshot.png',
]);
Ruby (Net::HTTP)
require "net/http"
require "uri"
uri = URI("https://api.screenshotrun.com/v1/screenshots/capture?url=https://example.com")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.read_timeout = 60 # seconds
http.open_timeout = 10 # seconds
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer YOUR_API_KEY"
response = http.request(request)
File.binwrite("screenshot.png", response.body)
Go
client := &http.Client{Timeout: 60 * time.Second}
req, _ := http.NewRequest("GET",
"https://api.screenshotrun.com/v1/screenshots/capture?url=https://example.com", nil)
req.Header.Set("Authorization", "Bearer YOUR_API_KEY")
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
os.WriteFile("screenshot.png", data, 0644)
Example: GET with query parameters
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://api.screenshotrun.com/v1/screenshots/capture?url=https://example.com&width=1440&format=webp" \
-o screenshot.webp
Example: POST with JSON body
curl -X POST https://api.screenshotrun.com/v1/screenshots/capture \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"html": "<h1>Hello World</h1><p>Rendered from HTML</p>",
"width": 1200,
"height": 630
}' \
-o screenshot.png
Using in the browser
Because the GET endpoint returns a binary image, you can use it directly in HTML as a link or an image source.
<a href="https://api.screenshotrun.com/v1/screenshots/capture?url=https://example.com&width=1280&format=png">
Take Screenshot
</a>
Anyone who views your page source can copy an API key embedded in HTML or JavaScript and use it from their own server. To safely use the API in the browser, create a domain-restricted key:
- Go to Dashboard → API Keys → Create New API Key.
- In the Allowed Domains field, enter the domain your site runs on — for example
myapp.com. One domain per line. - Use that key in your client-side code.
A domain-restricted key is rejected by the API if the request comes from any other origin, so even if someone copies the key, they cannot use it from their own site or server.
To get the async JSON response instead (same behavior as POST /v1/screenshots), pass response_type=json.
Create a Screenshot
POST /v1/screenshots
Queues a new screenshot for capture in the background. Returns a 202 Accepted response with the screenshot object in pending status. This is the async method — the screenshot is not ready yet when you get the response.
After creating a screenshot, you can either poll for its status with Get a Screenshot, or set up a webhook to be notified automatically when it completes.
When to use async vs. sync
- Use async (
POST /screenshots) when you are capturing many screenshots, processing them in a queue, or want non-blocking behavior in your application. - Use sync (
GET /capture) when you need the image right away and your use case is simple — a single capture at a time.
The async approach has one key advantage: your application does not hold an open connection while the browser is running. You fire the request, get an ID, and your server is free to do other work. This makes async a better fit for any situation where you are capturing more than a handful of URLs, or where your hosting environment has strict request timeout limits (serverless functions, shared hosting, etc.).
Example Request
cURL
curl -X POST https://api.screenshotrun.com/v1/screenshots \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"width": 1440,
"height": 900,
"format": "webp",
"full_page": true,
"dark_mode": true
}'
PHP
$response = Http::withToken(env('SCREENSHOT_API_KEY'))
->post('https://api.screenshotrun.com/v1/screenshots', [
'url' => 'https://example.com',
'width' => 1440,
'height' => 900,
'format' => 'webp',
'full_page' => true,
'dark_mode' => true,
]);
$screenshot = $response->json('data');
echo "Screenshot ID: " . $screenshot['id'];
Python
import requests
response = requests.post(
"https://api.screenshotrun.com/v1/screenshots",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"url": "https://example.com",
"width": 1440,
"height": 900,
"format": "webp",
"full_page": True,
"dark_mode": True,
}
)
screenshot = response.json()["data"]
print(f"Screenshot ID: {screenshot['id']}")
JavaScript (Node.js)
const response = await fetch("https://api.screenshotrun.com/v1/screenshots", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://example.com",
width: 1440,
height: 900,
format: "webp",
full_page: true,
dark_mode: true,
}),
});
const { data } = await response.json();
console.log(`Screenshot ID: ${data.id}`);
Ruby
require "net/http"
require "json"
require "uri"
uri = URI("https://api.screenshotrun.com/v1/screenshots")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer YOUR_API_KEY"
request["Content-Type"] = "application/json"
request.body = {
url: "https://example.com",
width: 1440,
height: 900,
format: "webp",
full_page: true,
dark_mode: true
}.to_json
response = http.request(request)
screenshot = JSON.parse(response.body)["data"]
puts "Screenshot ID: #{screenshot['id']}"
Go
payload := strings.NewReader(`{
"url": "https://example.com",
"width": 1440,
"height": 900,
"format": "webp",
"full_page": true,
"dark_mode": true
}`)
req, _ := http.NewRequest("POST", "https://api.screenshotrun.com/v1/screenshots", payload)
req.Header.Set("Authorization", "Bearer YOUR_API_KEY")
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
var result struct {
Data struct {
ID string `json:"id"`
} `json:"data"`
}
json.NewDecoder(resp.Body).Decode(&result)
fmt.Printf("Screenshot ID: %s\n", result.Data.ID)
Response (202 Accepted)
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "pending",
"url": "https://example.com",
"options": {
"width": 1440,
"height": 900,
"format": "webp",
"quality": 80,
"full_page": true,
"device": "desktop",
"block_ads": false,
"block_cookies": true,
"dark_mode": true,
"delay": 0,
"timeout": 30,
"retina": false
},
"estimated_time": 5,
"created_at": "2026-03-09T10:30:00.000000Z",
"links": {
"self": "https://api.screenshotrun.com/v1/screenshots/550e8400-e29b-41d4-a716-446655440000"
}
}
}
Screenshot Statuses
After you create a screenshot, it moves through these statuses:
| Status | Meaning |
|---|---|
pending | The screenshot is queued and waiting to be processed. |
processing | A browser is loading the page and taking the screenshot right now. |
completed | The screenshot is ready. You can download the image. |
failed | Something went wrong. Check the error field for details. |
Polling for Completion
If you are not using webhooks, you can poll for the screenshot status. Here is a simple polling example in Python:
import time
import requests
API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.screenshotrun.com/v1"
headers = {"Authorization": f"Bearer {API_KEY}"}
# Create screenshot
response = requests.post(
f"{BASE_URL}/screenshots",
headers=headers,
json={"url": "https://example.com"}
)
screenshot_id = response.json()["data"]["id"]
# Poll until ready
while True:
status_response = requests.get(
f"{BASE_URL}/screenshots/{screenshot_id}",
headers=headers,
)
data = status_response.json()["data"]
if data["status"] == "completed":
# Download the image
image_response = requests.get(
f"{BASE_URL}/screenshots/{screenshot_id}/image",
headers=headers,
)
with open("screenshot.png", "wb") as f:
f.write(image_response.content)
print("Screenshot saved!")
break
elif data["status"] == "failed":
print(f"Failed: {data['error']['message']}")
break
time.sleep(2) # Wait 2 seconds before next poll
For production workloads, use webhooks instead of polling. They are more efficient and give you instant notifications.
Async mode in practice: capturing multiple URLs
A common pattern is to kick off several screenshots at once, then collect the results as they finish. This is much faster than calling /capture one by one, because all the browser sessions run in parallel on our servers.
JavaScript (Node.js)
const API_KEY = "YOUR_API_KEY";
const BASE = "https://api.screenshotrun.com/v1";
const urls = [
"https://example.com",
"https://github.com",
"https://stripe.com",
];
// 1. Kick off all screenshots at once
const screenshots = await Promise.all(
urls.map(url =>
fetch(`${BASE}/screenshots`, {
method: "POST",
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ url, format: "jpeg", width: 1280 }),
})
.then(r => r.json())
.then(r => r.data)
)
);
console.log("Created:", screenshots.map(s => s.id));
// 2. Poll until all are done (simple version — use webhooks in production)
async function waitForAll(ids) {
const results = {};
const pending = new Set(ids);
while (pending.size > 0) {
await new Promise(r => setTimeout(r, 2000)); // wait 2s between checks
for (const id of [...pending]) {
const res = await fetch(`${BASE}/screenshots/${id}`, {
headers: { "Authorization": `Bearer ${API_KEY}` },
});
const data = (await res.json()).data;
if (data.status === "completed") {
results[id] = data.links.image;
pending.delete(id);
console.log(`Done: ${id} → ${data.links.image}`);
} else if (data.status === "failed") {
results[id] = null;
pending.delete(id);
console.error(`Failed: ${id} — ${data.error?.message}`);
}
}
}
return results;
}
const results = await waitForAll(screenshots.map(s => s.id));
console.log("All done:", results);
Python
import time
import requests
API_KEY = "YOUR_API_KEY"
BASE = "https://api.screenshotrun.com/v1"
headers = {"Authorization": f"Bearer {API_KEY}"}
urls = [
"https://example.com",
"https://github.com",
"https://stripe.com",
]
# 1. Kick off all screenshots at once
ids = []
for url in urls:
resp = requests.post(
f"{BASE}/screenshots",
headers=headers,
json={"url": url, "format": "jpeg", "width": 1280},
)
screenshot_id = resp.json()["data"]["id"]
ids.append(screenshot_id)
print(f"Created: {screenshot_id} for {url}")
# 2. Poll until all are done
pending = set(ids)
results = {}
while pending:
time.sleep(2) # wait 2 seconds between checks
for sid in list(pending):
data = requests.get(f"{BASE}/screenshots/{sid}", headers=headers).json()["data"]
if data["status"] == "completed":
results[sid] = data["links"]["image"]
pending.discard(sid)
print(f"Done: {sid} → {data['links']['image']}")
elif data["status"] == "failed":
results[sid] = None
pending.discard(sid)
print(f"Failed: {sid} — {data.get('error', {}).get('message')}")
print("All done:", results)
Most screenshots complete in 2–8 seconds. Polling every 2 seconds is a reasonable starting point. Do not poll faster than once per second — it wastes your rate limit budget without meaningful benefit. For production systems with many screenshots, use webhooks to eliminate polling entirely.
Using the timeout parameter
The timeout parameter controls how long the browser will wait for a page to load before giving up. It defaults to 30 seconds and accepts values from 5 to 60 seconds.
This is different from your HTTP client timeout. The relationship between the two is:
- API
timeout— how long our browser waits for the page to load. If the page does not respond in time, we return aCAPTURE_FAILEDerror. - HTTP client timeout — how long your code waits for our server to respond. This should always be longer than the API
timeout.
A safe rule: set your HTTP client timeout to API timeout + 15 seconds. If you use timeout=30 (the default), set your HTTP client timeout to at least 45 seconds.
{
"url": "https://example.com",
"timeout": 45,
"format": "jpeg"
}
If you are capturing sites that are known to be slow (large e-commerce catalogs, sites with heavy third-party scripts), increase timeout to 45–60 and switch to async mode. Async mode removes the HTTP connection timeout problem entirely, since your request returns in milliseconds with a screenshot ID.
Get a Screenshot
GET /v1/screenshots/{id}
Returns the screenshot object with its current status and details. Use this to check whether an async screenshot has finished processing.
Example Request
curl -H "Authorization: Bearer YOUR_API_KEY" \
https://api.screenshotrun.com/v1/screenshots/550e8400-e29b-41d4-a716-446655440000
Response — Completed
When the screenshot is ready, the response includes full metadata: image dimensions, file size, processing time, and download links.
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "completed",
"url": "https://example.com",
"options": {
"width": 1440,
"height": 900,
"format": "webp",
"quality": 80,
"full_page": true,
"device": "desktop",
"block_ads": false,
"block_cookies": true,
"dark_mode": true,
"delay": 0,
"timeout": 30,
"retina": false
},
"file_size": 245760,
"mime_type": "image/webp",
"width": 1440,
"height": 3200,
"processing_time_ms": 3450,
"completed_at": "2026-03-09T10:30:05.000000Z",
"expires_at": "2026-04-09T10:30:05.000000Z",
"created_at": "2026-03-09T10:30:00.000000Z",
"links": {
"self": "https://api.screenshotrun.com/v1/screenshots/550e8400-e29b-41d4-a716-446655440000",
"image": "https://api.screenshotrun.com/v1/screenshots/550e8400-e29b-41d4-a716-446655440000/image"
}
}
}
Response — Failed
If something went wrong during capture, the error field tells you what happened. Common causes include page timeouts, invalid URLs, and blocked content.
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "failed",
"url": "https://example.com",
"options": { ... },
"error": {
"message": "Page load timed out after 30 seconds.",
"code": "TIMEOUT"
},
"created_at": "2026-03-09T10:30:00.000000Z",
"links": {
"self": "https://api.screenshotrun.com/v1/screenshots/550e8400-e29b-41d4-a716-446655440000"
}
}
}
Get Screenshot Image
GET /v1/screenshots/{id}/image
Downloads the actual image file. This only works for screenshots with completed status. The response includes the correct Content-Type header for the format you chose (e.g. image/png, image/webp, application/pdf).
Example Request
# Download to file
curl -H "Authorization: Bearer YOUR_API_KEY" \
https://api.screenshotrun.com/v1/screenshots/550e8400-.../image \
-o screenshot.webp
Responses
| Status | Description |
|---|---|
200 OK | Image file with appropriate Content-Type header (image/png, image/jpeg, image/webp, image/avif, image/tiff, or application/pdf) |
404 Not Found | Screenshot is not ready yet (status is pending or processing) |
410 Gone | Screenshot image has expired and been deleted (see Image Retention below) |
Image Retention
Screenshots are stored temporarily. After the retention period expires, the image file is automatically deleted. The metadata (status, options, timestamps) stays available through the API, but the image itself returns 410 Gone.
How long your screenshots are kept depends on your plan:
| Plan | Retention |
|---|---|
| Free | 24 hours |
| Starter | 48 hours |
| Pro | 7 days |
| Growth | 30 days |
| Business | 30 days |
Download images as soon as they are ready, or use webhooks to trigger automatic downloads when screenshots complete. You can also create signed URLs to share images without exposing your API key.
List Screenshots
GET /v1/screenshots
Returns a paginated list of your screenshots, newest first. You can filter by status and date range, which is useful for building dashboards or audit logs.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
per_page | integer | 20 | Results per page (max 100) |
status | string | — | Filter by status: pending, processing, completed, failed |
sort_by | string | created_at | Sort field |
sort_dir | string | desc | Sort direction: asc or desc |
date_from | string | — | Filter screenshots created after this date (ISO 8601) |
date_to | string | — | Filter screenshots created before this date (ISO 8601) |
Example Request
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://api.screenshotrun.com/v1/screenshots?status=completed&per_page=10&sort_dir=desc"
Response
{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "completed",
"url": "https://example.com",
"options": { ... },
"file_size": 245760,
"mime_type": "image/webp",
"width": 1440,
"height": 3200,
"processing_time_ms": 3450,
"completed_at": "2026-03-09T10:30:05.000000Z",
"expires_at": "2026-04-09T10:30:05.000000Z",
"created_at": "2026-03-09T10:30:00.000000Z",
"links": {
"self": "...",
"image": "..."
}
}
],
"links": {
"first": "https://api.screenshotrun.com/v1/screenshots?page=1",
"last": "https://api.screenshotrun.com/v1/screenshots?page=5",
"prev": null,
"next": "https://api.screenshotrun.com/v1/screenshots?page=2"
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 5,
"links": [
{"url": null, "label": "« Previous", "page": null, "active": false},
{"url": "...?page=1", "label": "1", "page": 1, "active": true},
{"url": "...?page=2", "label": "2", "page": 2, "active": false},
{"url": "...?page=2", "label": "Next »", "page": 2, "active": false}
],
"path": "https://api.screenshotrun.com/v1/screenshots",
"per_page": 10,
"to": 10,
"total": 48
}
}
Delete a Screenshot
DELETE /v1/screenshots/{id}
Permanently deletes a screenshot and its image file. This action cannot be undone.
Example Request
curl -X DELETE -H "Authorization: Bearer YOUR_API_KEY" \
https://api.screenshotrun.com/v1/screenshots/550e8400-e29b-41d4-a716-446655440000
Response
Returns 204 No Content on success with an empty body.
Feature Availability
Some parameters are only available on certain plans. If you use a feature not included in your plan, the API returns a 422 error telling you which feature requires an upgrade. Check your current plan details anytime via GET /v1/account.
| Feature / Parameter | Free | Starter | Pro | Business |
|---|---|---|---|---|
url, width, height | Yes | Yes | Yes | Yes |
full_page | Yes | Yes | Yes | Yes |
delay | Yes | Yes | Yes | Yes |
block_cookies | Yes | Yes | Yes | Yes |
selector | Yes | Yes | Yes | Yes |
wait_for_selector | Yes | Yes | Yes | Yes |
omit_background | Yes | Yes | Yes | Yes |
reduced_motion | Yes | Yes | Yes | Yes |
device (mobile/tablet) | — | Yes | Yes | Yes |
retina | — | Yes | Yes | Yes |
block_ads | — | Yes | Yes | Yes |
block_chats | — | Yes | Yes | Yes |
format: pdf | — | Yes | Yes | Yes |
webhook_url | — | Yes | Yes | Yes |
html (HTML rendering) | — | Yes | Yes | Yes |
user_agent | — | Yes | Yes | Yes |
headers, cookies | — | Yes | Yes | Yes |
dark_mode | — | — | Yes | Yes |
css, js | — | — | Yes | Yes |
stealth | — | — | Yes | Yes |
timezone | — | — | Yes | Yes |
extract_metadata | — | — | Yes | Yes |
markdown (Markdown rendering) | — | — | Yes | Yes |
| Batch API | — | Yes | Yes | Yes |
| Signed URLs | — | — | Yes | Yes |
| PDF options (landscape, page size, margins) | — | Yes | Yes | Yes |
geolocation | — | — | — | Yes |
proxy | — | — | — | Yes |
scroll_video | — | — | Yes | Yes |
Next Steps
- Screenshot Options — full parameter reference, grouped by category, with examples for every option
- Caching — avoid redundant captures by caching screenshots
- Signed URLs — share screenshots without exposing your API key
- Batch Screenshots — capture up to 100 URLs in a single request
- Webhooks — get notified when screenshots are ready