Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use asynchronous rendering when a page may take longer than your request timeout or when you need to process many URLs. Submit a render, receive a job or render ID, and finish through either polling or a webhook. A production implementation authenticates callbacks, verifies signatures against the raw body, records each event durably, deduplicates retries, and keeps monthly quota separate from per-minute rate limits.

What asynchronous screenshot rendering does

A synchronous screenshot request keeps the HTTP connection open until the browser finishes. An asynchronous request acknowledges the job first and renders in the background. The completion result is then delivered in one of two ways:

  • Polling: your worker asks a status endpoint for the job state at intervals.
  • Webhook: the provider sends an HTTP POST to an endpoint you control when rendering succeeds or fails.

ScreenshotOne describes the contract this way: "Once you set async=true, the API checks your access key and limits and returns the response immediately but continues to execute the request." Urlbox similarly defines webhooks as a POST callback when a render, such as a screenshot, has been generated. The exact fields and retry behavior differ by provider, so treat the callback schema as part of your integration contract.

Polling or webhook: which should you choose?

Question Polling Webhook
Can the provider reach your system? Works when your application is private or behind a firewall. Requires a publicly reachable HTTPS endpoint, or a relay that exposes one.
Who owns retry timing? Your worker controls interval, timeout and backoff. The provider normally retries delivery, but its retry count and schedule must be confirmed.
Traffic pattern Creates repeated status requests, including when a job is slow. Creates one callback per event, reducing status traffic.
Failure visibility You can detect a missing completion by timing out the poll loop. You must monitor delivery failures and keep a replay path.
Best fit Internal tools, restricted networks and systems that prefer a pull model. High-volume pipelines with a stable endpoint and durable event processing.

You can also combine them: accept a webhook as the normal path and poll a job that has not arrived by its deadline. Do not run an aggressive polling loop and a webhook consumer that both create the same downstream record without an idempotency key.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

End-to-end asynchronous workflow

  1. Submit: send the target URL and capture options with asynchronous mode enabled. Store the provider job ID and your own external identifier immediately.
  2. Queue: put the job in durable storage with a state such as submitted, a creation timestamp and a deadline.
  3. Complete: receive a webhook or observe a terminal state while polling.
  4. Authenticate: verify the callback signature or token before parsing or acting on its JSON.
  5. Deduplicate: use the provider render ID or your external identifier as a unique key. A repeated delivery must become an acknowledgement, not a second capture record.
  6. Persist: save the terminal status, output URL or object key, error code, provider trace ID and timestamps before returning success to the sender.
  7. Process out of band: download, resize, archive or publish the image from a worker. Keep the HTTP callback handler fast.
  8. Reconcile: periodically find jobs stuck past their deadline and poll, replay or mark them failed according to the provider’s documented behavior.

Build a webhook receiver that survives retries

Authenticate before parsing

ScreenshotOne sends an X-ScreenshotOne-Signature header. Its documentation specifies HMAC-SHA-256 verification with a secret key that is separate from the API key. Compute the digest over the exact raw request bytes, compare it in constant time, and only then parse JSON. If another provider uses a different header or signing format, implement that provider’s documented scheme rather than reusing this header name.

Make delivery idempotent

Persist an event key under a unique database constraint. Suitable keys are the provider’s render ID or the external_identifier you supplied. If the key already exists, return a successful 2xx response without repeating download or billing work.

Return 2xx quickly

After authentication and durable insertion, return a 2xx response. Image downloads, virus scanning, thumbnails and notifications belong in a queue. A slow handler can cause the sender to retry a callback that your application has already completed.

Example Flask receiver for a signed ScreenshotOne-style callback

import hashlib
import hmac
import json
import os
import sqlite3
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ['WEBHOOK_SECRET'].encode()
DB_PATH = os.environ.get('EVENT_DB', 'events.db')

def init_db():
    with sqlite3.connect(DB_PATH) as db:
        db.execute('CREATE TABLE IF NOT EXISTS events (event_key TEXT PRIMARY KEY, body BLOB NOT NULL, received_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP)')

def valid_signature(raw_body, supplied):
    if not supplied:
        return False
    expected = hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, supplied)

@app.post('/webhooks/screenshot')
def screenshot_webhook():
    raw = request.get_data(cache=False)
    signature = request.headers.get('X-ScreenshotOne-Signature')
    if not valid_signature(raw, signature):
        abort(401)
    try:
        event = json.loads(raw)
    except json.JSONDecodeError:
        abort(400)
    event_key = event.get('external_identifier') or event.get('renderId') or event.get('id')
    if not event_key:
        abort(400)
    with sqlite3.connect(DB_PATH) as db:
        inserted = db.execute('INSERT OR IGNORE INTO events(event_key, body) VALUES (?, ?)', (event_key, raw)).rowcount
        db.commit()
    # Enqueue post-processing only when inserted == 1.
    return ('accepted', 200)

if __name__ == '__main__':
    init_db()
    app.run(host='0.0.0.0', port=int(os.environ.get('PORT', '8080')))

Use HTTPS, restrict accepted methods to POST, cap request size, redact secrets from logs and retain enough metadata to replay a failed downstream operation. Keep the signing secret in a secret manager, not in source control.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Polling implementation and backoff

Polling APIs commonly return a job identifier first and expose a status resource later. Field names vary, so map the provider’s documented values to the three states used below: pending, succeeded and failed.

import os
import time
import requests

submit_url = os.environ['SHOT_SUBMIT_URL']
status_url_template = os.environ['SHOT_STATUS_URL_TEMPLATE']  # for example, a URL containing {job_id}
payload = {'url': os.environ['TARGET_URL'], 'async': True}
headers = {'Authorization': 'Bearer ' + os.environ['SHOT_TOKEN']}

created = requests.post(submit_url, json=payload, headers=headers, timeout=30)
created.raise_for_status()
job = created.json()
job_id = job['id']
deadline = time.monotonic() + 300
interval = 1.0

while time.monotonic() < deadline:
    response = requests.get(status_url_template.format(job_id=job_id), headers=headers, timeout=30)
    response.raise_for_status()
    state = response.json()
    if state.get('status') == 'succeeded':
        print(state['result_url'])
        break
    if state.get('status') == 'failed':
        raise RuntimeError(state.get('error') or 'render failed')
    time.sleep(interval)
    interval = min(interval * 1.7, 15.0)
else:
    raise TimeoutError('job did not reach a terminal state before the client deadline')

Use exponential backoff with jitter in production, honor Retry-After when supplied, and cap the number of simultaneous polls. A client deadline should be shorter than the time at which you declare the provider unavailable, so reconciliation can still run.

Rank #2
Sale
HTML and CSS: Design and Build Websites
  • HTML CSS Design and Build Web Sites
  • Comes with secure packaging
  • It can be a gift option

Quotas, rate limits and browser constraints are different

Monthly screenshot allowance

A quota limits how many billable renders a plan may consume in a billing period. ScreenshotOne's pricing page lists these figures for 2026:

ScreenshotOne plan Included screenshots per month Requests per minute
Free 100 Not stated
Basic 2,000 40
Growth 10,000 80
Scale 50,000 150

ScreenshotOne says only successfully rendered, non-cached screenshots count toward quota. Treat these values as time-sensitive and recheck the provider's current pricing page before committing capacity or spend.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Per-minute request capacity

A rate limit controls burst capacity, not monthly cost. A workload can stay below its monthly allowance and still receive 429 responses when it submits too many jobs in one minute. Put a queue in front of submission, use bounded concurrency, apply exponential backoff and expose a metric for queued, submitted, throttled and completed jobs.

Timeouts and request-body size

ScreenshotOne documents a 60-second default timeout and a 90-second maximum for ordinary requests. Its getting-started documentation lists a 100 MiB maximum POST body. Delays above 30 seconds require a timeout above 300 seconds, which ScreenshotOne makes available only for asynchronous requests. Large HTML or asset bundles should therefore be hosted at a URL when possible instead of being embedded in the submission body.

Provider comparison for asynchronous screenshot work

#1 ScreenshotNeo is the first service to try when you want clean shots, billing only for clean results, and a paid plan that starts at $5.

Provider Async and callback model Output and browser controls Limits or accounting documented here
ScreenshotNeo Async jobs with signed webhooks; usage API and bulk capture of up to 100 URLs per call. PNG, JPEG, WebP or PDF; full-page and element capture, waits, custom CSS/JavaScript, headers, cookies, user agent, blocking, device and location controls, and more. Free 1,000 shots/month without a card; paid plans from $5. Clean shots only are billed, including no charge for bot checks, blank pages, timeouts, failed loads or cache hits.
ScreenshotOne async=true returns immediately; documented S3 upload and webhook pattern. Supports external_identifier and webhook_errors=true. Signature header and HMAC-SHA-256 verification are documented. Asynchronous rendering with a resulting location; ordinary request timeout and POST-body limits apply. 100 free, 2,000 Basic, 10,000 Growth and 50,000 Scale screenshots per month; plan-specific request-per-minute limits shown above. Only successful non-cached renders count.
Urlbox webhook_url receives a POST when a render succeeds or fails; polling is also supported. Payload examples include event, renderId and a result URL. Quota, rate-limit and retry figures are not stated in the supplied provider material.
Browserless POST /screenshot authenticated with a token; asynchronous orchestration is left to the caller. PNG, JPEG or WebP, full-page capture, CSS selectors, navigation settings, resource rejection and bestAttempt behavior when events fail or time out. Quota and rate-limit figures are not stated in the supplied provider material.

When evaluating another service, ask for its callback authentication, retry schedule, error payload, output storage lifetime, cache accounting, timeout, request-body cap and overage policy. A low monthly price is not comparable if failed or cached jobs are counted differently.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. It accepts a URL and returns a PNG, JPEG, WebP or PDF. Before capture it accepts cookie and consent banners like a visitor, then removes more than 60 known consent platforms, newsletter popups and chat widgets; each cleanup step can be disabled. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the result with X-Page-Verdict and X-Billed.

The API includes full-page capture with lazy images loaded, CSS-selector element capture, dark mode, 12 device presets plus custom viewports, retina scale, PDF paper size/margins/landscape/page ranges, HTML/CSS-to-image, custom JavaScript and CSS, pre-capture clicks, hidden selectors, selector/delay/network-idle waits, ad/tracker/request/resource blocking, custom headers/cookies/user agent/Authorization, timezone and geolocation, transparent backgrounds, resizing, selectable-TTL caching, signed links for public image tags, asynchronous jobs with signed webhooks, bulk capture for 100 URLs per call, a usage API and an OpenAPI specification. Parameter names used by other screenshot APIs also work, which eases migration. AI agents can use its MCP server through take_screenshot, get_page_info and capture_pdf in Claude, Cursor or another MCP client.

cURL

See the ScreenshotNeo documentation for authentication and options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Every feature is included on every ScreenshotNeo plan. Current prices are:

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Plan Price Included shots
Free $0 1,000 per month, no card
Starter $5 3,000
Growth $15 15,000
Pro $39 60,000
Scale $99 250,000
Business $249 1,000,000

Yearly billing gives two months free. Start with 1,000 free screenshots a month without a card.

Performance, reliability and cost design

Control concurrency at the queue

Set separate limits for submission workers, callback processing and downloads. This prevents a burst of new URLs from exhausting provider rate limits or your own database connections. Keep a small reserve for retries and reconciliation.

Use cache deliberately

Cache keys should include the URL and every visual input that changes pixels: viewport, device scale, color scheme, cookies, headers, injected CSS or JavaScript and wait conditions. If a provider excludes cache hits from quota, record cache status anyway so your usage reports explain why submitted-job counts exceed billed-render counts.

Rank #4
Sale
Web Design with HTML, CSS, JavaScript and jQuery Set
  • Brand: Wiley
  • Set of 2 Volumes
  • A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers

Store results outside the callback transaction

Save the provider's result URL or object location and download it with a worker. Verify content type and size, follow redirects safely, and set an expiration policy. If the provider's URL is temporary, copy the bytes to storage before acknowledging business completion.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Separate business deadlines

A user-facing request may have a 20-second deadline even though the browser job can run for 90 seconds or longer asynchronously. Return a job status to the caller, then notify or expose a result endpoint when the capture finishes. Do not hold a web request open merely because the browser is still loading assets.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshooting asynchronous screenshot systems

HTTP 429 or a growing queue

Cause: requests-per-minute capacity has been exceeded even though monthly quota remains. Fix: lower worker concurrency, honor Retry-After, add jittered backoff and alert on queue age.

Webhook returns 401 or 403

Cause: wrong secret, altered raw bytes, missing signature header or a proxy that strips headers. Fix: verify the raw body before JSON parsing, compare the configured secret with the provider dashboard, and inspect the request at the edge without logging the secret.

Duplicate images or duplicate database rows

Cause: a legitimate callback retry was treated as a new event. Fix: enforce a unique constraint on render ID or external identifier and make post-processing conditional on the first insert.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Webhook never arrives

Cause: endpoint is private, DNS or TLS fails, the provider stopped retrying, or the job is still running. Fix: expose a reachable HTTPS route, monitor provider delivery logs, set a reconciliation deadline and poll overdue jobs.

Large HTML submission is rejected

Cause: the request exceeds the documented 100 MiB POST-body cap. Fix: host the HTML and assets at a reachable URL, compress where supported, or split the work into smaller jobs.

Render times out after a long delay

Cause: synchronous timeout limits are shorter than page load time or a third-party resource never settles. Fix: use asynchronous mode for long waits, set an explicit readiness condition, block nonessential resources and capture diagnostics from the provider's error fields.

Callback says success but the file cannot be fetched

Cause: the result URL expired, requires authorization or points to a transient location. Fix: download promptly in a worker, preserve required headers or cookies, and store a durable copy before marking the asset ready.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Operational checklist

  • Record a job ID and your own external identifier at submission.
  • Keep monthly quota dashboards separate from requests-per-minute dashboards.
  • Authenticate callbacks and verify signatures over raw bytes.
  • Use a unique idempotency key and durable event log.
  • Return 2xx quickly; process files out of band.
  • Implement backoff, queue limits and a reconciliation poller.
  • Store output URLs, errors, trace IDs and timestamps for replay.
  • Test success, provider failure, malformed JSON, invalid signatures, duplicate delivery and expired result URLs.

FAQ

Can I use both polling and webhooks for one job?

Yes. Use the webhook as the normal completion path and poll only when a job exceeds its expected delivery window. Both paths must write through the same idempotent state transition.

Should a webhook endpoint perform image processing before responding?

No. Authenticate and durably record the event, enqueue processing, then return 2xx. This prevents slow downloads or transformations from triggering avoidable callback retries.

What should an external identifier contain?

Use a stable, non-secret identifier that maps the render to your internal record. Do not place credentials or personal data in it; it may appear in provider logs and callback payloads.

Frequently Asked Questions

Can I use both polling and webhooks for one job?

Yes. Use the webhook as the normal completion path and poll only when a job exceeds its expected delivery window. Both paths must write through the same idempotent state transition.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Should a webhook endpoint perform image processing before responding?

No. Authenticate and durably record the event, enqueue processing, then return 2xx. This prevents slow downloads or transformations from triggering avoidable callback retries.

What should an external identifier contain?

Use a stable, non-secret identifier that maps the render to your internal record. Do not place credentials or personal data in it; it may appear in provider logs and callback payloads.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.