API documentation
Quick start
The base URL is https://aback-screenshot.pages.dev. Every versioned endpoint lives under /api/v1. The version is part of the path, so a breaking change will ship as a new version rather than as a surprise.
curl -X POST https://aback-screenshot.pages.dev/api/v1/captures \
-H "Authorization: Bearer $ABACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/pricing",
"format": "png",
"width": 1280,
"height": 800,
"fullPage": false,
"waitUntil": "load",
"timeout": 15000
}'The response is JSON. The image itself is fetched from content_url, which carries a signed token that expires with the capture.
{
"id": "cap_7Yk3nQ1sVb0pLxZa2mRc4T",
"status": "succeeded",
"content_url": "https://aback-screenshot.pages.dev/api/v1/captures/cap_7Yk3nQ1sVb0pLxZa2mRc4T/content?token=mf0y9s.gS3k...",
"format": "png",
"width": 1280,
"height": 800,
"full_page": false,
"byte_size": 184320,
"duration_ms": 1187,
"content_type": "image/png",
"created_at": "2026-10-21T09:41:02.114Z",
"expires_at": "2026-10-21T10:41:02.114Z",
"quota": { "limit": 50, "used": 3, "remaining": 47 }
}Retrying safely
Send an Idempotency-Key header and a retry replays the first result instead of creating a second capture and consuming a second unit of quota. The key is scoped to the account or the API key that sent it, and it is remembered for 24 hours. A retry that arrives while the first request is still running is refused rather than queued.
The key needs an API key or a session. An anonymous caller has no identity that two anonymous callers could not share, and a shared scope would let one of them receive the result of the other's request, so an anonymous request that carries a key is refused.
A key that is used for a different request is refused with the idempotency_key_reuse code rather than replaying the old result. A replay whose capture has already expired says so instead of handing out a link that cannot work.
curl -X POST https://aback-screenshot.pages.dev/api/v1/captures \
-H "Authorization: Bearer $ABACK_API_KEY" \
-H "Idempotency-Key: 7f3c1a2e-order-4821" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}'Endpoints
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /api/v1/captures | API key, session or anonymous | Capture a page and return the result with a signed delivery link. |
| GET | /api/v1/captures | API key or session | List the captures of the signed in account. |
| GET | /api/v1/captures/:id | Owner only | Read the metadata of one capture. |
| DELETE | /api/v1/captures/:id | Owner only | Delete a capture and its image before it expires. |
| GET | /api/v1/captures/:id/content | Owner or a signed token | Return the image bytes. |
| GET | /api/v1/take | A signed link, or an API key | Capture a page and return the image itself, so the link can be used as an image source. |
| POST | /api/v1/take | API key | Build a signed take link that renders a page on demand without a key. |
| POST | /api/mcp | API key | The model context protocol, so an agent can capture a page with the same limits and metering. |
| GET | /api/openapi.json | None | The interface description, for generating a client. |
| GET | /api/v1/keys | API key or session | List the API keys of the account. The raw key is never returned. |
| POST | /api/v1/keys | API key or session | Create an API key. The raw key is returned exactly once. |
| DELETE | /api/v1/keys/:id | Owner only | Revoke an API key. It stops working on the next request. |
| GET | /api/v1/usage | API key or session | Usage for the current billing period. |
| GET | /api/v1/plan | API key or session | The plan of the account and the limits it carries. |
| GET | /api/v1/limits | None | The limits the service enforces, as a machine readable contract. |
| POST | /api/v1/checkout | API key or session | Start a checkout for a paid plan. |
| GET | /api/health | None | Whether the service can accept work. Never exposes internal details. |
Authentication
Send your API key as a bearer token. Keys are created in the dashboard, shown exactly once and stored only as a digest, so they can be revoked at any time without affecting other keys. A key starts with sk_live_ followed by 43 random characters.
Authorization: Bearer sk_live_6fJ1r8Nq2XzT5bW9cH0dLpQr4Tn8Vm2XwA request that carries an authorization header is either authenticated or refused. A key that is mistyped, truncated or from another environment is answered with a not authorized response, never treated as an anonymous request, so a broken credential can never produce a capture that belongs to nobody.
Revoke instantly
Revoking a key takes effect on the next request. The key returns a not authorized response straight away.
Rate limits follow the key
Each key has its own request budget, so one noisy integration cannot use up another team member's allowance.
The web interface uses a session cookie instead. Anonymous requests are accepted for manual captures but carry the tightest rate limit.
Capture options
| Option | Type | Default | Range | Notes |
|---|---|---|---|---|
| url | string | required | 2048 characters | Absolute http or https URL. Credentials in the URL are rejected. |
| format | string | png | png, jpeg | jpg is accepted as an alias for jpeg. |
| width | integer | 1280 | 320 to 3840 | Viewport width in CSS pixels. |
| height | integer | 800 | 240 to 2160 | Viewport height in CSS pixels. |
| fullPage | boolean | false | height up to 16000 | Taller pages are clipped at the limit rather than rejected. The limit is the lower of 16000 pixels and the height that fits the render budget, so a wider viewport is clipped lower. |
| waitUntil | string | load | load, domcontentloaded, networkidle | Use network idle for single page applications. |
| timeout | integer | 15000 | 1000 to 30000 | Milliseconds. Always enforced by the server. |
| quality | integer | 80 | 1 to 100 | JPEG only. Sending quality with PNG is an error. |
| deviceScaleFactor | integer | 1 | 1 to 3 | 2 produces a retina quality image. |
Unknown options are rejected rather than ignored, so a typo never silently changes the image.
Errors
Every failure uses the same envelope. The code is stable and safe to branch on, the message says what to change, and the request id can be quoted in a support request.
{
"error": {
"code": "blocked_address",
"message": "This destination resolves to a non-public address and cannot be captured.",
"request_id": "e2f1a0c4-6f2b-4a1e-9f77-2f3f5a1b9c33",
"details": { "reason": "link-local or cloud metadata" }
}
}| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | The request body is missing or malformed. |
| 400 | invalid_url | The url must be a valid absolute http or https URL. |
| 400 | unsupported_scheme | Only http and https URLs can be captured. |
| 400 | credentials_in_url | URLs containing embedded credentials are not accepted. |
| 400 | blocked_host | This host is not allowed. Internal and local destinations are blocked. |
| 400 | blocked_address | This destination resolves to a non-public address and cannot be captured. |
| 502 | dns_resolution_failed | The host name could not be resolved. |
| 400 | port_not_allowed | This port is not allowed. Use port 80 or 443. |
| 414 | url_too_long | The url is longer than the supported limit. |
| 413 | payload_too_large | The request body exceeds the supported size. |
| 400 | unsupported_format | The requested image format is not supported. |
| 400 | invalid_option | One or more capture options are outside the supported range. |
| 401 | unauthorized | Authentication is required. Provide a valid API key or session. |
| 403 | forbidden | You do not have access to this resource. |
| 404 | not_found | The requested resource was not found. |
| 409 | idempotency_key_reuse | This idempotency key was already used for a different request. |
| 429 | rate_limited | Too many requests. Slow down and try again shortly. |
| 503 | capacity_exceeded | The capture service is at capacity. Try again in a moment. |
| 402 | quota_exceeded | The monthly capture quota for this account has been used. |
| 504 | capture_timeout | The capture did not finish before the deadline. |
| 502 | navigation_failed | The target page could not be loaded. |
| 502 | output_too_large | The captured image exceeded the maximum allowed size. |
| 500 | storage_error | The capture could not be stored. |
| 503 | service_misconfigured | The service is not configured to handle this request. |
| 502 | billing_error | The payment provider could not be reached. |
| 400 | invalid_webhook | The webhook signature could not be verified. |
| 500 | internal_error | Something went wrong while processing the request. |
Limits, timeouts and retries
- The request body must stay under 16384 bytes and the URL under 2048 characters.
- Navigation is bounded by the
timeoutoption, and the whole request by a hard deadline that the target page cannot extend. - A page may issue at most 250 requests, and a capture is at most 8 MB.
- At most 5 redirects are followed, and each one is checked against the address policy.
- A capture is retained for 60 minutes. The signed link that points at it expires at the same time.
- The same request can be retried safely. Send an
Idempotency-Keyheader and a retry replays the first result instead of creating a second capture and consuming a second unit of quota.
The machine readable version of this table is available at GET /api/v1/limits.
Usage metering
- A capture counts against the monthly allowance only when it produced a usable image.
- Validation errors, blocked addresses, rate limits, timeouts and target site errors are recorded for reporting but are never charged.
- The allowance is reserved before the capture runs, so several requests that arrive at the same time cannot take more captures than the plan allows. A capture that fails returns the reservation.
- The allowance resets at the start of the billing period. The current window is reported by
GET /api/v1/usage. - Plan changes take effect when the payment provider confirms the payment, not when the browser returns from checkout.
Rate limits
Every capture response carries the limit and the remaining allowance in the x-ratelimit-limit and x-ratelimit-remaining headers, so a client can slow down before it is rejected. A rejection carries a retry-after header with the number of seconds to wait. The values for every plan are published on GET /api/v1/limits.
A caller without an account has no monthly allowance. It is bounded by a per minute limit and by a daily limit instead, and it is metered separately from the accounts.
A link that renders the image
The take endpoint answers with the image itself, so a link can be used as the source of an image tag without any further work. A request can carry an API key in the Authorization header, or it can carry a signature.
curl "https://abackscreenshot.com/api/v1/take?url=https%3A%2F%2Fexample.com&width=1280&height=800" \
-H "Authorization: Bearer $ABACK_API_KEY" \
--output example.pngSigned take links
A signed link needs no key at all. It is signed with the deployment secret and the identifier of the key it is charged to, it carries an expiry, and it stops working the moment that key is revoked. The link renders the page on demand, so it is always current.
curl -X POST https://abackscreenshot.com/api/v1/take \
-H "Authorization: Bearer $ABACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "width": 1280, "height": 800, "ttl": 3600}'The answer is the link and the moment it stops working. Paste it into an email, a document or a page and the image is there. A signed link is a capability: anyone who has it can render that page until it expires, so the expiry should be as short as the use allows.
For agents
The service speaks the model context protocol, so an agent can capture a page through the same pipeline, with the same address policy, the same limits and the same metering as the API. A capture answers with the link to the image and not the image itself, because a picture would fill the context window of the agent that asked for it.
{
"mcpServers": {
"aback-screenshot": {
"type": "http",
"url": "https://abackscreenshot.com/api/mcp",
"headers": { "Authorization": "Bearer YOUR_API_KEY" }
}
}
}Three tools are offered: take_screenshot, get_usage and get_plan.
Generate a client
The interface is published as an OpenAPI description. Any tool that reads it can generate a client, and the description is built from the same limits and the same error codes as the service, so it cannot describe something that does not exist.
curl https://abackscreenshot.com/api/openapi.json --output openapi.jsonAllowance, payment and cancellation
- The allowance is a number of captures per billing period. It is reserved before a capture runs, so several requests that arrive at the same time cannot take more captures than the plan allows.
- When it resets. The period starts on the first of the month, or on the day the subscription started for a paid plan. The current window is reported by
GET /api/v1/usageand by the dashboard. - When you go over. The request is refused with the quota exceeded code. Nothing is charged, no partial capture is stored, and the allowance is not reduced. The response says how many captures were used and when the allowance resets.
- When a payment fails. The plan is kept until the period that was paid for has ended, so a failed card does not cut a working integration off in the middle of a billing period. After that the account falls back to the free plan.
- When you cancel. The plan ends at the end of the paid period, and the account falls back to the free plan. The captures that were already made stay on the account until they pass the retention window.
- The plan is applied when the payment provider confirms the payment, never when the browser returns from the checkout.
Use the API from your language
const response = await fetch(process.env.ABACK_API_URL + "/api/v1/captures", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.ABACK_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://example.com",
format: "jpeg",
width: 1440,
height: 900,
quality: 85,
}),
});
if (!response.ok) {
throw new Error(await response.text());
}
const capture = await response.json();
console.log(capture.content_url);import os
import requests
response = requests.post(
"https://aback-screenshot.pages.dev/api/v1/captures",
headers={
"Authorization": f"Bearer {os.environ['ABACK_API_KEY']}",
"Content-Type": "application/json",
},
json={"url": "https://example.com", "format": "png", "width": 1280, "height": 800},
timeout=45,
)
response.raise_for_status()
print(response.json()["content_url"])