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.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

The reliable pattern is to treat a screenshot callback as a server-to-server webhook. Your browser starts a job through your backend; the screenshot service posts completion data to a public HTTPS endpoint; your backend verifies and stores the result; then the page receives a safe, usually same-origin image URL. Do not send provider secrets to browser JavaScript or expect a provider to POST directly into an open tab.

The callback architecture

A callback (webhook) separates rendering from page display. The browser submits a target URL and options to your application. Your server submits the screenshot job with a callback URL. When rendering finishes, the provider sends a completion or failure payload to that endpoint. Your server validates the request, persists the image or provider URL, marks the job complete, and lets the browser obtain the result.

  1. Start: the browser posts capture parameters to your backend.
  2. Submit: the backend calls the screenshot API and records its job or render ID.
  3. Receive: the provider POSTs status, image information, and authentication data to your callback.
  4. Validate: verify the signature, match the job ID, check status, MIME type, and size.
  5. Persist: copy bytes to your storage, or retain a provider URL only when its lifetime is acceptable.
  6. Notify: polling, Server-Sent Events, WebSocket, or the next normal application response tells the page that an image is ready.
  7. Render: the browser assigns an approved URL to an <img> element.

Choose the callback payload you will render

Hosted image URL

If the callback supplies an image URL and MIME type, validate both before returning the URL. A provider URL can expire, so copy the bytes to your storage or issue a short-lived URL from your own domain when users must revisit the image.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
<img id="preview" alt="Generated page screenshot">
<script>
function showScreenshotUrl(url) {
  document.querySelector('#preview').src = url;
}
</script>

Never accept an arbitrary URL from an untrusted payload and place it directly in the page. Use an allowlist or an application proxy to prevent unexpected hosts and to enforce authorization.

Binary image bytes

Some APIs return a download URL or binary response. Fetch it, check the HTTP status and content type, then create a temporary Blob URL.

async function showScreenshotBinary(downloadUrl) {
  const response = await fetch(downloadUrl, { credentials: 'omit' });
  if (!response.ok) throw new Error(`Screenshot download failed: ${response.status}`);
  const type = response.headers.get('content-type') || '';
  if (!['image/png', 'image/jpeg', 'image/webp'].includes(type.split(';')[0])) {
    throw new Error('Unexpected screenshot content type');
  }
  const blob = await response.blob();
  const image = document.querySelector('#preview');
  const previous = image.dataset.objectUrl;
  if (previous) URL.revokeObjectURL(previous);
  const objectUrl = URL.createObjectURL(blob);
  image.dataset.objectUrl = objectUrl;
  image.src = objectUrl;
}

URL.createObjectURL() creates a blob URL for the Blob. Revoke the previous URL when replacing it and when a component is destroyed; revoking immediately after setting src can make the image fail before it loads.

Base64 data

For a payload such as {data, content_type}, validate the alphabet and construct a complete data URL.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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
function showScreenshotBase64(data, contentType = 'image/png') {
  if (!/^[A-Za-z0-9+/=rn]+$/.test(data)) {
    throw new Error('Unexpected base64 data');
  }
  const allowed = ['image/png', 'image/jpeg', 'image/webp'];
  if (!allowed.includes(contentType)) throw new Error('Unexpected image type');
  document.querySelector('#preview').src =
    `data:${contentType};base64,${data.replace(/s/g, '')}`;
}

Base64 is convenient for small previews but increases page state and memory because the bytes are encoded into text. Store larger screenshots and return a URL instead. If you have a Blob and specifically need a data URL, FileReader.readAsDataURL() produces one asynchronously; remove its data:*/*;base64, prefix only when an API requires raw base64.

A minimal callback backend

The following Express-style example illustrates the important controls. Adapt field names to your provider and use its documented signature scheme.

import express from 'express';
import crypto from 'node:crypto';

const app = express();
app.use('/callbacks/screenshots', express.raw({ type: 'application/json', limit: '1mb' }));
const jobs = new Map(); // Use a database and durable object storage in production.

function verifySignature(raw, signature, secret) {
  if (!signature) return false;
  const expected = crypto.createHmac('sha256', secret).update(raw).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

app.post('/callbacks/screenshots', async (req, res) => {
  const signature = req.get('x-webhook-signature');
  if (!verifySignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }
  let event;
  try { event = JSON.parse(req.body.toString('utf8')); }
  catch { return res.sendStatus(400); }

  const job = jobs.get(event.render_id);
  if (!job) return res.sendStatus(404); // Do not create jobs from unsolicited callbacks.
  if (job.callback_delivery_id === event.delivery_id) return res.sendStatus(204);
  job.callback_delivery_id = event.delivery_id;

  if (event.status !== 'succeeded') {
    job.status = 'failed';
    job.error = String(event.error || 'Screenshot failed');
    return res.sendStatus(204);
  }
  if (!['image/png', 'image/jpeg', 'image/webp'].includes(event.content_type)) {
    return res.status(422).send('Unsupported content type');
  }
  // Download and scan event.image_url here, or enqueue durable processing.
  job.status = 'ready';
  job.image_url = '/api/screenshot-images/' + encodeURIComponent(event.render_id);
  res.sendStatus(204);
});

app.get('/api/screenshot-jobs/:id', (req, res) => {
  const job = jobs.get(req.params.id);
  if (!job) return res.sendStatus(404);
  res.json({ status: job.status, image_url: job.status === 'ready' ? job.image_url : null, error: job.error || null });
});

Authenticate before parsing expensive content, bind the callback to a job you created, and make processing idempotent. Providers retry failed deliveries; a delivery ID or event hash prevents duplicate downloads and duplicate records. Return a fast 2xx response after durable validation. Queue image downloads, virus scanning, resizing, and storage rather than keeping the webhook request open.

Polling or pushing completion to the page

Polling

Polling is the simplest option and works through most firewalls.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
async function waitForScreenshot(id) {
  for (;;) {
    const r = await fetch(`/api/screenshot-jobs/${encodeURIComponent(id)}`);
    if (!r.ok) throw new Error(`Job lookup failed: ${r.status}`);
    const job = await r.json();
    if (job.status === 'ready') return job.image_url;
    if (job.status === 'failed') throw new Error(job.error || 'Capture failed');
    await new Promise(resolve => setTimeout(resolve, 1500));
  }
}

Use exponential backoff and a deadline in a real application. Show “queued,” “rendering,” “ready,” and “failed” states instead of waiting forever.

Server-Sent Events or WebSockets

After the callback updates the job, publish an event keyed by the authenticated user and job ID. The browser then receives the same-origin image URL. These transports reduce polling but require connection cleanup, reconnection handling, and authorization on every subscription.

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

Security, CORS, and retention

  • Keep API keys, webhook secrets, storage credentials, and signing keys on the server.
  • Require HTTPS and verify the provider’s HMAC or equivalent signature over the raw request body.
  • Check job identity, callback delivery ID, status, content type, byte size, and any provider request ID.
  • Apply URL allowlists, authentication, rate limits, and an SSRF policy when users can submit target URLs.
  • For direct browser fetches, the provider must send Access-Control-Allow-Origin for your page origin. Credentialed requests need an explicit origin and permitted credentials; * is not valid for credentialed access.
  • Prefer a same-origin proxy when CORS is unavailable or when you need authorization and content checks.
  • Define retention and deletion rules. Provider URLs may expire; copying bytes gives you predictable availability but adds storage cost.

Output and capture choices

Cloudflare’s screenshot endpoint accepts a URL or HTML and documents viewport, full-page capture, clipping, wait conditions, binary or base64 encoding, and PNG, JPEG, or WebP output. A callback design should record the requested options with the job so a retry reproduces the same render. Use PNG for crisp UI text, JPEG when photographic compression matters, and WebP when supported by your delivery pipeline. Wait for a selector, a delay, or network idle when the page contains client-rendered content; otherwise the callback can legitimately report a screenshot of an incomplete page.

Common failures and fixes

Symptom Likely cause Fix
No callback arrives Endpoint is private, HTTP-only, or blocked Expose a public HTTPS route, test it externally, and log provider delivery attempts.
401 or repeated retries Signature calculated from parsed JSON or wrong secret Verify the raw bytes with the exact header and secret, then return 2xx only after validation.
Image shows as broken Expired URL, wrong MIME type, or CORS denial Copy bytes to storage or proxy them; validate and return an image content type; inspect browser CORS errors.
Duplicate files Webhook retry processed twice Use a unique render ID and delivery ID with an atomic database update.
Page waits forever Failure and timeout states are ignored Persist terminal failures, impose a client deadline, and display a retry action.
Memory keeps growing Blob URLs are never revoked Revoke the previous URL on replacement and component teardown.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

ScreenshotNeo provides a screenshot API and MCP server. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Only clean shots are billed: bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and billing result. Its MCP tools—take_screenshot, get_page_info, and capture_pdf—work with Claude, Cursor, and other MCP clients.

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

For a direct image response, see the ScreenshotNeo documentation and call:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo also supports callbacks through asynchronous jobs with signed webhooks, bulk capture, custom waits, headers and cookies, device presets, full-page and element capture, PDF output, caching, and signed links. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

Frequently Asked Questions

Should the browser call the screenshot provider directly?

Usually no. A backend keeps API keys private, verifies callbacks, applies URL and size policies, and can provide a same-origin image URL when CORS is unavailable.

How long should a callback endpoint wait before responding?

Only long enough to authenticate and durably record the event. Queue downloads and image processing, then return a 2xx response so provider retries do not pile up.

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

Which delivery format is best for large screenshots?

Persist binary bytes and return an authorized URL. Base64 is simplest for small previews but consumes more page memory and bandwidth.

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.