API Reference
screenshot.tools.town is a JSON API. Send a URL, receive a signed PNG link. Every request must include an API
key from dashboard.tools.town/keys.
Base URL: https://screenshot.tools.town
Authentication
Pass your API key in the X-API-Key header. The key must have the screenshot:capture scope.
X-API-Key: tt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Get a key at dashboard.tools.town/keys. Keys are shown once at creation; store them in your environment variables.
POST /v1/capture
Render a URL in a headless browser and return a signed PNG link.
Request body (JSON)
| Field | Type | Default | Notes |
|---|---|---|---|
| url | string | required | HTTPS only. Private IPs and internal hosts are blocked. |
| viewport.width | number | 1280 | Clamped to server limits (max 1920). |
| viewport.height | number | 720 | Clamped to server limits (max 1080). |
| fullPage | boolean | false | Capture full page height, not just viewport. |
| waitForSelector | string | — | CSS selector. Browser waits for element to appear. Max 256 chars. |
| navigationTimeoutMs | number | 30000 | Navigation timeout in ms. Clamped 3000–45000. |
Optional headers
| Header | Notes |
|---|---|
| Idempotency-Key | Up to 128 chars. Retries with the same key + URL will not double-charge. |
Example
curl -X POST https://screenshot.tools.town/v1/capture \
-H "X-API-Key: tt_xxxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"viewport": { "width": 1200, "height": 630 },
"fullPage": false,
"waitForSelector": "#main-content",
"navigationTimeoutMs": 20000
}' GET /v1/meta
Returns current limits and defaults. Requires X-API-Key.
{
"sku": "screenshot_render",
"max_viewport": { "width": 1920, "height": 1080 },
"min_viewport": { "width": 200, "height": 100 },
"default_viewport": { "width": 1280, "height": 720 },
"navigation_timeout_ms": { "min": 3000, "max": 45000, "default": 30000 },
"max_wait_for_selector_length": 256,
"rate_limit_per_minute": 60
} Response fields
| Field | Type | Description |
|---|---|---|
| media_id | string (UUID) | Unique ID of the stored image object. |
| image_url | string (URL) | Signed URL to the PNG. Valid until expires_at. |
| expires_at | string (ISO-8601) | When the signed URL expires. |
| content_type | string | Always image/png in v1. |
| byte_size | number | PNG size in bytes. |
| balance_after | number | Remaining screenshot_render credits after this debit. |
Error codes
All errors return {"error": "...", "code": "..."}.
| HTTP | code | Meaning |
|---|---|---|
| 400 | bad_url | URL is invalid, not HTTPS, or blocked (private IP). |
| 400 | too_many_redirects | Redirect chain exceeded the maximum hops. |
| 401 | unauthorized | Missing, invalid, or revoked API key. |
| 403 | forbidden | Key exists but lacks screenshot:capture scope. |
| 402 | insufficient_credits | No screenshot_render credits remaining. |
| 429 | rate_limited | Exceeded 60 captures/minute for this key. |
| 502 | capture_failed | Browser Rendering error. No credit was debited. |
| 502 | storage_failed | Capture succeeded but media upload failed. Credit was debited — contact support. |
Rate limits
Default: 60 captures per minute per API key, enforced
per UTC minute via Cloudflare KV. Exceeding this returns 429 rate_limited — no credit is consumed.
Need a higher limit? Contact us via the dashboard.
Idempotency
Pass an Idempotency-Key header (up to 128 chars). Retrying a request with the same key and URL will not
double-charge — the credit debit is keyed on hash(key_id + idempotency_key).
Idempotency-Key: req_2026-04-12_capture_xyz123