Skip to content
Reference

API documentation

Everything the capture API accepts, everything it returns and every way it can fail. The examples on this page are the real request and response shapes.

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.

create a capturebash
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.

responsejson
{
  "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.

a retry that is safebash
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

MethodPathAuthPurpose
POST/api/v1/capturesAPI key, session or anonymousCapture a page and return the result with a signed delivery link.
GET/api/v1/capturesAPI key or sessionList the captures of the signed in account.
GET/api/v1/captures/:idOwner onlyRead the metadata of one capture.
DELETE/api/v1/captures/:idOwner onlyDelete a capture and its image before it expires.
GET/api/v1/captures/:id/contentOwner or a signed tokenReturn the image bytes.
GET/api/v1/takeA signed link, or an API keyCapture a page and return the image itself, so the link can be used as an image source.
POST/api/v1/takeAPI keyBuild a signed take link that renders a page on demand without a key.
POST/api/mcpAPI keyThe model context protocol, so an agent can capture a page with the same limits and metering.
GET/api/openapi.jsonNoneThe interface description, for generating a client.
GET/api/v1/keysAPI key or sessionList the API keys of the account. The raw key is never returned.
POST/api/v1/keysAPI key or sessionCreate an API key. The raw key is returned exactly once.
DELETE/api/v1/keys/:idOwner onlyRevoke an API key. It stops working on the next request.
GET/api/v1/usageAPI key or sessionUsage for the current billing period.
GET/api/v1/planAPI key or sessionThe plan of the account and the limits it carries.
GET/api/v1/limitsNoneThe limits the service enforces, as a machine readable contract.
POST/api/v1/checkoutAPI key or sessionStart a checkout for a paid plan.
GET/api/healthNoneWhether 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.

headerhttp
Authorization: Bearer sk_live_6fJ1r8Nq2XzT5bW9cH0dLpQr4Tn8Vm2Xw

A 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

OptionTypeDefaultRangeNotes
urlstringrequired2048 charactersAbsolute http or https URL. Credentials in the URL are rejected.
formatstringpngpng, jpegjpg is accepted as an alias for jpeg.
widthinteger1280320 to 3840Viewport width in CSS pixels.
heightinteger800240 to 2160Viewport height in CSS pixels.
fullPagebooleanfalseheight up to 16000Taller 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.
waitUntilstringloadload, domcontentloaded, networkidleUse network idle for single page applications.
timeoutinteger150001000 to 30000Milliseconds. Always enforced by the server.
qualityinteger801 to 100JPEG only. Sending quality with PNG is an error.
deviceScaleFactorinteger11 to 32 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 responsejson
{
  "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" }
  }
}
StatusCodeMeaning
400invalid_requestThe request body is missing or malformed.
400invalid_urlThe url must be a valid absolute http or https URL.
400unsupported_schemeOnly http and https URLs can be captured.
400credentials_in_urlURLs containing embedded credentials are not accepted.
400blocked_hostThis host is not allowed. Internal and local destinations are blocked.
400blocked_addressThis destination resolves to a non-public address and cannot be captured.
502dns_resolution_failedThe host name could not be resolved.
400port_not_allowedThis port is not allowed. Use port 80 or 443.
414url_too_longThe url is longer than the supported limit.
413payload_too_largeThe request body exceeds the supported size.
400unsupported_formatThe requested image format is not supported.
400invalid_optionOne or more capture options are outside the supported range.
401unauthorizedAuthentication is required. Provide a valid API key or session.
403forbiddenYou do not have access to this resource.
404not_foundThe requested resource was not found.
409idempotency_key_reuseThis idempotency key was already used for a different request.
429rate_limitedToo many requests. Slow down and try again shortly.
503capacity_exceededThe capture service is at capacity. Try again in a moment.
402quota_exceededThe monthly capture quota for this account has been used.
504capture_timeoutThe capture did not finish before the deadline.
502navigation_failedThe target page could not be loaded.
502output_too_largeThe captured image exceeded the maximum allowed size.
500storage_errorThe capture could not be stored.
503service_misconfiguredThe service is not configured to handle this request.
502billing_errorThe payment provider could not be reached.
400invalid_webhookThe webhook signature could not be verified.
500internal_errorSomething 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 timeout option, 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-Key header 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.

the image itselfbash
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.png

Signed 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.

build a signed linkbash
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.

mcp.jsonjson
{
  "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.

the descriptionbash
curl https://abackscreenshot.com/api/openapi.json --output openapi.json

Allowance, 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/usage and 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

node.mjstypescript
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);
capture.pypython
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"])

Frequently asked questions

Yes. The version is the first path segment. A change that would break an existing integration ships as a new version, and the old version stays available for a published period.

The create endpoint always answers with JSON so the response shape never changes. Fetch the content_url to get the bytes, with the correct image content type and a no-store cache policy.

One hour by default, the same as the retention window. A link whose token has expired returns a forbidden response, and the capture itself is removed once it passes the retention window.

The request is rejected with the quota exceeded code and the response says how many captures were used and when the allowance resets. Nothing is charged and no partial capture is stored.

No. The service does not accept credentials, does not store cookies and does not attempt to work around authentication, paywalls or bot protection.