Base URL
https://snap.i2dev.comAuthentication
Pass your API key using either header:
Authorization: Bearer YOUR_API_KEYor
x-api-key: YOUR_API_KEYPOST /v1/screenshots
Captures a screenshot of a URL (synchronous). Returns an imageUrl plus metadata.
Example request (copy/paste)
curl -sS -X POST https://snap.i2dev.com/v1/screenshots -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" -d '{
"url": "https://example.com",
"format": "png",
"fullPage": false,
"viewport": { "width": 1280, "height": 720 },
"waitUntil": "load"
}'Example response
{
"jobId": "...",
"request_id": "...",
"imageUrl": "https://snap-i2dev-com.s3.amazonaws.com/...?X-Amz-...",
"contentType": "image/png",
"bytes": 12345,
"durationMs": 987,
"logs": [],
"page_errors": []
}Result URLs and retention
Use imageUrl to retrieve the finished screenshot. Authenticated result URLs are signed and expire. Synchronous results default to seven days; async results default to one hour. Both are deployment-configurable. Download or copy an artifact that must outlive its URL. logs and page_errors contain capture diagnostics and may be empty.
Async mode, when enabled
Set "async": true in the POST /v1/screenshots body to queue the render and return immediately. Then poll GET /v1/jobs/:id for status. If async mode is unavailable on the deployment, the API returns a 400 bad_request; synchronous capture remains available.
Async submit example
curl -sS -X POST https://snap.i2dev.com/v1/screenshots -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" -d '{
"url": "https://example.com",
"format": "png",
"async": true
}'Async submit response
{
"jobId": "...",
"request_id": "...",
"status": "queued",
"statusUrl": "https://snap.i2dev.com/v1/jobs/..."
}Poll job status
curl -sS -H "Authorization: Bearer YOUR_API_KEY" https://snap.i2dev.com/v1/jobs/JOB_IDJob status response (done)
{
"jobId": "...",
"status": "succeeded",
"result": {
"imageUrl": "https://snap-i2dev-com.s3.amazonaws.com/...?X-Amz-...",
"contentType": "image/png",
"bytes": 12345,
"durationMs": 987
}
}Batch: many URLs in one request
Send an items array instead of a single url to capture up to 50 pages in one request. Options at the top level apply to every item, and each item may override any of them, field by field — an item that sets only viewport.width keeps the shared viewport.height. Every item needs its own url. A batch is always asynchronous: it returns a batchId immediately, which you poll at GET /v1/screenshots/:batchId. Because it is asynchronous, it needs the same deployment switch as async mode and returns 400 bad_request where that is unavailable.
Each item costs one render from your monthly allowance, charged up front and all-or-nothing: a batch larger than your remaining allowance is refused whole with 429 quota_exceeded and costs you nothing. A batch containing a URL we will not fetch — a private address, a non-HTTP scheme — is likewise rejected before anything is charged, with the offending index in the error details.
A URL that simply does not resolve is treated differently: it is accepted and fails as its own item, so one dead link in a sitemap does not throw away the rest of the batch. Items render independently, so a batch can finish with a mix of succeeded and failed items — check counts rather than assuming completed means every item worked.
Batch submit example
curl -sS -X POST https://snap.i2dev.com/v1/screenshots -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" -d '{
"format": "jpeg",
"viewport": { "width": 1280, "height": 720 },
"items": [
{ "url": "https://example.com/" },
{ "url": "https://example.com/pricing", "fullPage": true },
{ "url": "https://example.com/docs", "viewport": { "width": 390, "height": 844 } }
]
}'Batch submit response
{
"batchId": "...",
"request_id": "...",
"count": 3,
"status": "queued",
"statusUrl": "https://snap.i2dev.com/v1/screenshots/..."
}Poll batch status
curl -sS -H "Authorization: Bearer YOUR_API_KEY" https://snap.i2dev.com/v1/screenshots/BATCH_IDBatch status response
status is running until every item has finished, then completed — and it stays there. Items come back in the order you submitted them. Results expire (see retention above), so a batch you poll much later can be completed with fewer entries in items than itemCount; fetch what you need before then.
{
"batchId": "...",
"status": "completed",
"itemCount": 3,
"counts": { "queued": 0, "running": 0, "succeeded": 2, "failed": 1 },
"items": [
{
"jobId": "...",
"url": "https://example.com/",
"status": "succeeded",
"imageUrl": "https://snap-i2dev-com.s3.amazonaws.com/...?X-Amz-...",
"contentType": "image/jpeg",
"bytes": 12345,
"durationMs": 987
},
{
"jobId": "...",
"url": "https://example.com/pricing",
"status": "failed",
"error": { "error": "Navigation timeout", "code": "nav_timeout" }
}
]
}Request body
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | The URL to screenshot (public http/https only; SSRF protections enabled). |
format | "png" | "jpeg" | No | Default: png. The anonymous GET endpoint defaults to jpeg instead. |
fullPage | boolean | No | Capture full scrollable page. Default: false |
viewport | { width, height } | No | Default: {"width":1280,"height":720} |
waitUntil | "load" | "domcontentloaded" | "networkidle" | No | Default: load. After this condition fires, the renderer always runs a best-effort settle pass (network quiescence up to 5s, font loading, brief animation delay) before capturing, so SPAs and lazy content typically render fully on the default setting. |
waitForSelector | string | No | Wait for a CSS selector before capturing. |
timeoutMs | number | No | Overall render timeout (bounded by server max). |
navTimeoutMs | number | No | Navigation timeout (bounded by server max). |
timezoneId | string | No | Example: America/Toronto |
locale | string | No | Example: en-US |
extraHTTPHeaders | object | No | Extra request headers (string → string). |
cookies | array | No | Cookies to set before navigation. |
baselineTag | string | No | Optional label included in the result's storage identity. It does not publish a Visual CI baseline. |
compareWithTag | string | No | Optional label included in the result's storage identity. It does not retrieve or diff a previous capture. |
async | boolean | No | Queue render and return immediately (requires async mode enabled). |
items | array | No | Capture many URLs in one request (max 50). Replaces url; every other field becomes a shared default that each item may override. See Batch. |
timezoneId and locale are checked by Chromium when rendering starts, rather than against a server-side allowlist. Values Chromium cannot use return400 invalid_render_option for synchronous requests. Async submissions still return 202, then expose invalid_render_option on the failed job. Unconventional values that Chromium supports remain accepted.
Capturing authenticated pages safely
Supply headers and cookies from CI or repository secrets—never commit authorization values. Cookies must include the target domain. Use the least privilege and shortest lifetime possible, and avoid reusing long-lived personal sessions. Snap forwards these credentials while loading the target page. Header and cookie values are not part of the result's storage identity, so do not use one API key with identical URL/render options for multiple user sessions; a later capture can replace the earlier object.
Rate limits and errors
Errors are returned as JSON and include a request_id. Include that ID when reporting issues.
Authenticated rate-limit responses use HTTP 429 with rate_limited, a Retry-After header, and X-Snap-Rate-Limit-Scope set to ip or api_key. The JSON details object repeats the delay as retry_after_seconds and the scope as scope. too_many_inflight is also a 429, but is a concurrency response and does not include token-refill retry metadata.
{
"error": "Invalid or missing API key",
"code": "unauthorized",
"request_id": "..."
}Common HTTP status codes:
400bad_request / invalid_render_option / unsafe_url401unauthorized413payload_too_large415unsupported_media_type — sendContent-Type: application/json. Worth checking first if a hand-written request fails immediately: some shells drop the header, and curl removes a header given an empty value.429rate_limited / too_many_inflight / quota_exceeded500internal_server_error502automation_refused / upstream_unreachable — the target site failed, not Snap