ScreenshotNeo

BlogHow-to

How to receive screenshot API results with a webhook

Receive screenshot API results reliably with an async webhook: submit jobs, verify signatures, store results, and handle retries and failures.

By the ScreenshotNeo team4 October 202610 min read

To receive screenshot API results with a webhook, submit the capture in asynchronous mode with a callback URL. The API returns an accepted job response first; after rendering, it sends a separate HTTP POST to your server. Save the job ID from the first response, verify the callback signature against the exact raw request body when the provider supports signing, persist the result URL or storage location, then return a 2xx response promptly.

Webhook parameters, payloads, signatures, failure notifications, and retry policies vary by provider. The examples below follow the documented patterns for ScreenshotOne and ScreenshotMAX; use the reference for your chosen provider rather than assuming these details are universal. ScreenshotOne async and webhook docs · ScreenshotMAX async and webhook docs.

1. Set up a publicly reachable webhook endpoint

Your application needs an endpoint that can accept an HTTP POST from the screenshot provider. Use a public HTTPS URL in production. A server listening only on localhost cannot receive requests from an external service unless you expose it through a suitable public development tunnel.

Make the handler do as little work as possible: read the raw body, verify the signature, validate and persist the event, enqueue any lengthy follow-up work, and respond with a 2xx status. A fast acknowledgment avoids tying delivery to image processing, database-heavy work, or downstream API calls. Confirm the selected provider’s timeout and retry behavior in its documentation.

2. Submit the screenshot job asynchronously

Include the provider’s asynchronous-mode option and your endpoint URL in the screenshot request. The immediate response means the job was accepted or queued; it does not necessarily contain a finished image.

Provider Documented request pattern Initial response and result
ScreenshotOne async=true and webhook_url. Its storage example also uses store=true, response_type=json, and storage_return_location=true. Returns an immediate response while capture continues. JSON mode can provide screenshot_url; a storage workflow can return its location with the storage option.
ScreenshotMAX async=true and webhook_url. Its guide describes an immediate 202 Accepted. Example callback fields include id, file, expires, and created.

Save the initial job or request identifier together with your own operation ID and the URL being captured. This lets the callback update the correct record, including when several captures are in flight. Do not treat a provider-specific ID or payload field as universal.

3. Receive, verify, and persist the callback

Keep the exact raw request bytes until signature verification is complete. Parsing and re-serializing JSON can change whitespace or byte representation and cause a valid signature check to fail. Use the provider’s documented signing secret and algorithm; an API key is not automatically the webhook secret.

Provider signature details

  • ScreenshotOne: documents the X-ScreenshotOne-Signature header and HMAC SHA-256 verification with a secret key that is separate from the API key.
  • ScreenshotMAX: documents optional signed webhooks in X-Screenshotmax-WebHook-Signature, using HMAC SHA-256 over the raw JSON body.

Header names, signature formatting, secret provisioning, and payload schemas are provider-specific. Follow the provider’s current verification example, compare signatures using a constant-time comparison function, and reject invalid signatures before triggering downstream actions.

After verification, parse the JSON, validate the fields you need, and persist the event state, provider job ID, and result URL or storage location. Treat callback delivery as potentially repeated unless your provider explicitly guarantees otherwise. Where a stable event ID exists, record it with a uniqueness constraint; otherwise make completion updates idempotent using the job ID and state. This is general webhook design guidance, not a claim about a particular screenshot provider’s delivery guarantees.

4. Runnable receiver example: Node.js and Express

This receiver demonstrates raw-body preservation, HMAC SHA-256 validation, idempotent job updates, and prompt acknowledgment. It expects a generic hexadecimal HMAC-SHA256 signature and a JSON payload with an id and file; adapt the signature encoding and field names to the provider’s documented format before using it. ScreenshotOne and ScreenshotMAX use different headers and provider payloads, so do not deploy this generic format unchanged.

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

const app = express();
const port = Number(process.env.PORT || 3000);
const webhookSecret = process.env.WEBHOOK_SECRET;

if (!webhookSecret) throw new Error('Set WEBHOOK_SECRET');

function verifyHmacSha256(rawBody, suppliedHex, secret) {
  if (!suppliedHex || !/^[a-f0-9]+$/i.test(suppliedHex)) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest();
  const supplied = Buffer.from(suppliedHex, 'hex');
  return supplied.length === expected.length && crypto.timingSafeEqual(supplied, expected);
}

// express.raw preserves the exact bytes needed for signature verification.
app.post('/webhooks/screenshot', express.raw({ type: 'application/json', limit: '1mb' }), async (req, res) => {
  const signature = req.get('X-Provider-Signature');
  if (!Buffer.isBuffer(req.body) || !verifyHmacSha256(req.body, signature, webhookSecret)) {
    return res.status(401).send('Invalid signature');
  }

  let event;
  try {
    event = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.status(400).send('Invalid JSON');
  }

  // Replace these generic fields with the provider's documented schema.
  if (typeof event.id !== 'string' || typeof event.file !== 'string') {
    return res.status(400).send('Missing job ID or result URL');
  }

  // Persist idempotently. For example, upsert by provider job ID and enqueue
  // any slow download, transformation, or notification for a worker.
  await saveScreenshotResult({ providerJobId: event.id, resultUrl: event.file });
  return res.sendStatus(204);
});

async function saveScreenshotResult(result) {
  // Replace with a database upsert and, if needed, a background-queue insert.
  console.log('Persist result', result.providerJobId, result.resultUrl);
}

app.listen(port, () => console.log(`Webhook receiver listening on ${port}`));

Install Express with npm install express, set WEBHOOK_SECRET and optionally PORT, then run the file in a Node.js environment configured for ES modules. Configure your provider to call https://your-host.example/webhooks/screenshot. Before production, replace the illustrative verifier’s header, encoding, and payload assumptions with the provider’s exact requirements, and replace the logging persistence stub with a durable database and queue if needed.

5. Complete request examples for documented providers

ScreenshotOne

This cURL request follows ScreenshotOne’s documented async webhook pattern and asks for a JSON response. Add the documented storage options when you use its S3-compatible storage workflow and need the stored location in the result.

curl -G "https://api.screenshotone.com/take" \
  --data-urlencode "access_key=YOUR_SCREENSHOTONE_API_KEY" \
  --data-urlencode "url=https://example.com" \
  --data-urlencode "async=true" \
  --data-urlencode "webhook_url=https://your-host.example/webhooks/screenshot" \
  --data-urlencode "response_type=json"

For its documented storage-location example, add store=true and storage_return_location=true. To receive error details through the webhook, ScreenshotOne documents opting in with webhook_errors=true; errors are not sent to the webhook by default. Check the current docs for the full request and response schema.

ScreenshotMAX

curl -G "https://api.screenshotmax.com/v1/screenshot" \
  --data-urlencode "api_key=YOUR_SCREENSHOTMAX_API_KEY" \
  --data-urlencode "url=https://example.com" \
  --data-urlencode "async=true" \
  --data-urlencode "webhook_url=https://your-host.example/webhooks/screenshot"

ScreenshotMAX documents an immediate 202 Accepted response for this flow. Use its current API reference for the precise endpoint, authentication parameters, and payload; the values above illustrate the guide’s request pattern.

6. Test the complete flow safely

  1. Deploy the receiver to a public development URL and confirm it accepts POST requests.
  2. Configure the callback URL and signing secret in the provider’s supported settings.
  3. Submit one asynchronous screenshot request and store the returned job ID.
  4. Inspect the callback headers and raw body in a safe development log. Avoid logging API keys, signing secrets, or sensitive captured URLs.
  5. Verify the signature before parsing, then confirm your database stores the callback against the original job.
  6. Send the same valid event twice in a controlled test and confirm the second delivery does not create duplicate work.
  7. Exercise invalid signatures and malformed payloads, then confirm they are rejected without running downstream actions.
  8. Test the provider’s documented failure-notification path and verify your job state can represent failure as well as completion.

7. Handle failures, duplicates, and expired results

  • Capture errors: Do not assume every failed render triggers a webhook. ScreenshotOne says error callbacks are off by default and documents webhook_errors=true to opt in. Its JSON response mode can include error codes and messages, and it documents error headers. ScreenshotMAX’s guide describes checking request status as done or failed in its dashboard.
  • Duplicate deliveries: Make database updates idempotent. Store a stable event ID when supplied; otherwise use the provider job ID as an upsert key and avoid repeating side effects.
  • Missing callbacks: Track jobs that remain pending and use the provider’s documented status lookup or dashboard to reconcile them. Do not invent a retry window or guarantee; check the selected service’s terms and docs.
  • Result URL lifecycle: Persist the returned location and any expiry timestamp. Download or copy the image to storage you control if your workflow needs it beyond the provider’s retention period.
  • Slow downstream work: Acknowledge after durable acceptance of the event, then process expensive downloads, conversions, and notifications in a background worker.

8. Security and reliability checklist

  • Use a public HTTPS endpoint in production and keep the signing secret outside source control.
  • Preserve the raw body for signature checking; do not parse it first with ordinary JSON middleware.
  • Use the provider’s exact signature header, secret, and algorithm, and compare signatures in constant time.
  • Reject invalid signatures and malformed payloads before database changes or side effects.
  • Validate expected job identifiers and result URL schemes before fetching callback-provided URLs.
  • Persist state durably and use idempotent writes so a repeated callback is safe.
  • Respond with 2xx promptly after accepting the event; retry your own background work independently.
  • Monitor pending, completed, failed, and invalid-signature counts without recording credentials or unnecessary sensitive data.
  • Confirm payload size limits, result URL expiry, callback retries, and failure notifications in the screenshot provider’s docs.

9. Performance, reliability, and cost considerations

Async capture is useful when rendering may take longer than a synchronous request should remain open or when many independent captures need to run without tying up request workers. It moves completion notification to a second request; it does not itself guarantee faster rendering. Keep webhook handling short and let background workers perform large downloads or transformations.

Reliability depends on both sides: the provider must deliver to a reachable endpoint, and your application must durably record and safely process the event. The dossier does not establish universal retry timing, delivery guarantees, or price comparisons for screenshot providers. Check those details with the service you choose, and avoid assuming another platform’s webhook retry policy applies.

For cost control, reconcile submitted jobs with completed, failed, and stored results according to the provider’s billing rules. The cited provider documentation here does not substantiate cross-provider pricing or a universal billing rule for failed captures.

10. Troubleshooting

Symptom Likely cause Fix
No callback arrives Endpoint is private, callback URL is mistyped, or the job is still running. Check public reachability, provider request records or dashboard, and the callback URL. Use the provider’s status mechanism to reconcile pending jobs.
Signature validation always fails Body was parsed or transformed before verification, wrong secret or header, or signature encoding differs. Capture raw bytes, use the signing secret rather than the API key, and follow the provider’s exact signature format.
Callback returns a non-2xx status Handler threw an error, timed out, or rejected a valid body. Inspect server logs and provider request details; validate the schema and return 2xx after durable acceptance. Verify what retry behavior the provider documents.
Job accepted but no result URL is present The initial response is only an acknowledgment, or the chosen response/storage mode does not return a location. Read the callback schema and enable the provider’s documented JSON or storage-location options where applicable.
Failed render has no webhook Error callbacks may be disabled or unsupported for that configuration. For ScreenshotOne, enable webhook_errors=true if appropriate. For other providers, check their failure flow and status lookup.
Duplicate notifications create duplicate records Handler assumes one delivery per job. Use a unique event ID or idempotent upsert keyed by provider job ID; move side effects behind a deduplicating queue.
Stored screenshot link later fails The provider’s result URL may expire. Use the expiry field when available and copy results to storage you control when longer retention is required.
Development endpoint never receives provider traffic Localhost is not reachable from the provider. Expose a temporary public development URL with a suitable tunnel, or deploy a staging receiver.

11. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. For a synchronous one-call capture, it returns an image or PDF directly; for asynchronous bulk work, its API also supports jobs and signed webhooks. See the ScreenshotNeo API docs for request options and webhook setup.

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}`);
  • Cookie and consent banners are accepted like a visitor, and 60+ known consent platforms, newsletter popups, and chat widgets are removed before the shot; each step can be turned off.
  • Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are never billed; response headers say which page verdict applied and whether the request was billed.
  • An MCP server gives Claude, Cursor, and other MCP clients the take_screenshot, get_page_info, and capture_pdf tools.
  • 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000 screenshots. Every feature is on every plan.

Start with 1,000 free screenshots a month, no card required.

12. FAQ

Is a webhook the same response as the screenshot API call?

No. The initial call submits or queues work; the webhook is a separate POST sent after the provider has a result.

Can I use a webhook endpoint on my laptop?

Not if it is only bound to localhost. The provider needs a network-reachable URL; use a public development tunnel or a staging deployment.

Should I return success before downloading the screenshot?

Usually acknowledge once the event has been verified and durably recorded. Put a potentially slow download into a background job so callback handling stays quick.

Will every provider retry if my server is down?

There is no universal policy. Check the screenshot provider’s current documentation for retry behavior and delivery guarantees.

Sources