ScreenshotNeo

BlogHow-to

Browshot Webhook Setup for Completed Screenshot Jobs

Set up Browshot’s screenshot completion webhook, handle success and error callbacks safely, and use polling when a callback is delayed.

By the ScreenshotNeo team4 October 20267 min read

To receive a callback when a Browshot screenshot job finishes, provide your publicly reachable endpoint in the hook parameter when creating the screenshot. Browshot sends a POST with the JSON returned by /api/v1/screenshot/info when the job reaches finished or error. Return a 20X response promptly. Browshot documents up to two retries when the receiver is slow or does not return a 20X status; it does not specify retry intervals or promise exactly-once delivery. [Browshot API documentation]

1. Create a screenshot job with a webhook

Your create request needs the screenshot target URL, an instance_id, and the hook callback URL. The endpoint must be reachable by Browshot; a localhost address on your development machine is not publicly reachable.

curl -X POST "https://api.browshot.com/api/v1/screenshot/create" \
  --data-urlencode "url=https://example.com" \
  --data-urlencode "instance_id=YOUR_INSTANCE_ID" \
  --data-urlencode "hook=https://your.example.com/webhooks/browshot" \
  --data-urlencode "key=YOUR_BROWSHOT_API_KEY"

Use the API key parameter format required by your Browshot account and API configuration. Keep credentials on the server; do not put them in browser code or public pages. The official create endpoint is documented at Browshot’s API reference.

2. Receive and process the callback

The hook is an HTTP POST. Its body contains the JSON data returned by /api/v1/screenshot/info for the job. Handle both terminal statuses: finished and error. The documented status values also include in_queue and processing.

Runnable Python receiver

This minimal Flask example parses the JSON callback, branches on the status, and responds promptly. Install Flask with python -m pip install flask, save as app.py, and run flask --app app run --host 0.0.0.0 --port 8000. Put the deployed public HTTPS endpoint URL in the create request’s hook parameter.

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.post("/webhooks/browshot")
def browshot_webhook():
    payload = request.get_json(silent=True)
    if not isinstance(payload, dict):
        return jsonify(error="Expected a JSON object"), 400

    screenshot_id = payload.get("id")
    status = payload.get("status")

    if status == "finished":
        # Enqueue follow-up work; avoid doing slow work in this request.
        print("Screenshot finished", screenshot_id)
    elif status == "error":
        print("Screenshot failed", screenshot_id, payload)
    else:
        # The documented hook is for terminal states. Log unexpected payloads.
        print("Unexpected Browshot status", screenshot_id, status)

    return jsonify(ok=True), 200

Confirm the exact response field names against the callback JSON and your Browshot API version before relying on fields beyond the documented status and screenshot information. The example treats id as a convenient identifier if present; it does not assume a particular error schema.

Runnable Node.js receiver

This Express endpoint uses the same handling pattern. Install with npm install express; save as server.js and run node server.js.

const express = require('express');
const app = express();
app.use(express.json());

app.post('/webhooks/browshot', (req, res) => {
  const payload = req.body;
  if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
    return res.status(400).json({ error: 'Expected a JSON object' });
  }

  const screenshotId = payload.id;
  if (payload.status === 'finished') {
    console.log('Screenshot finished', screenshotId);
    // Enqueue follow-up work and return without waiting for it.
  } else if (payload.status === 'error') {
    console.error('Screenshot failed', screenshotId, payload);
  } else {
    console.warn('Unexpected Browshot status', screenshotId, payload.status);
  }

  return res.status(200).json({ ok: true });
});

app.listen(8000, '0.0.0.0', () => console.log('Webhook receiver listening on 8000'));

Inspect a callback with cURL

For local development, expose your local receiver through a secure tunnel or deploy a temporary endpoint, then use that public URL as hook. To exercise your handler yourself, send a representative JSON body:

curl -i -X POST "https://your.example.com/webhooks/browshot" \
  -H "Content-Type: application/json" \
  --data '{"status":"finished","id":"REPLACE_WITH_SAMPLE_ID"}'

This tests your endpoint, not Browshot delivery. Browshot’s documentation describes the hook body as the JSON from screenshot/info; use an actual or documented info response to validate fields your application consumes.

3. Make callback handling safe to retry

Browshot says it may retry up to two times if the endpoint responds too slowly or does not return a 20X response. The documentation does not state a retry schedule or exactly-once guarantee. Design the handler so receiving the same job notification more than once does not duplicate expensive or irreversible work.

  1. Identify the job using the screenshot ID from the callback, after confirming the field in the payload.
  2. Record that the terminal event has been accepted, using a database uniqueness constraint or an idempotency key.
  3. Enqueue downstream work and return a 20X response quickly.
  4. Make the worker safe to retry too; a callback can be accepted even if later processing fails.

These are reliability recommendations based on the documented possibility of retries, not claims about Browshot’s internal delivery system. The docs do not describe webhook signing or an authentication header. Do not assume an undocumented signature exists; if you need to restrict access, use controls you can configure at your endpoint and confirm Browshot’s supported request capabilities first.

4. Poll status if the callback is delayed

Keep the screenshot ID returned by the create operation. If you do not observe a callback, query /api/v1/screenshot/info with that ID and check whether status is finished or error. Browshot documents polling as a way to check job status; it does not publish a required polling interval here.

curl -G "https://api.browshot.com/api/v1/screenshot/info" \
  --data-urlencode "id=YOUR_SCREENSHOT_ID" \
  --data-urlencode "key=YOUR_BROWSHOT_API_KEY"

Use a bounded polling policy with a delay between requests rather than tight-looping. Stop once you have observed a terminal status. Treat polling as a reconciliation path for missed or delayed notifications, not as evidence that the webhook delivery itself has a particular latency.

5. Common problems and fixes

Symptom Likely cause What to do
No callback arrives The hook URL is not publicly reachable, the route or method is wrong, or the job has not reached a terminal state. Verify the exact URL and POST route from outside your network. Check the job with screenshot/info using its ID.
Browshot retries the notification The receiver is slow or returned a non-20X status. Return a 20X promptly after validating and recording/enqueuing the event. Move slow work to a background worker.
Handler reports malformed or missing JSON The request parser is absent, runs after the route, or the test payload does not match the actual callback. Enable JSON parsing before the route, inspect request headers and body safely, and compare with the info response.
Application processes the same screenshot twice Retry delivery caused repeated handling. Use a durable idempotency record keyed by the confirmed screenshot ID and make downstream actions idempotent.
Job remains in_queue or processing The screenshot is not yet in a terminal state. Continue with a bounded status check strategy; the documented hook notification is for finished or error.
Local tests pass but Browshot cannot connect localhost, a private address, firewall, TLS, or proxy configuration prevents external access. Use a publicly routable endpoint with correct HTTPS and allow inbound requests to the configured route.

6. Performance, reliability, and cost considerations

A webhook avoids repeatedly asking for status while the job is pending, while polling provides a direct status check by screenshot ID. Browshot’s reviewed documentation provides no comparative latency or reliability measurements, so choose based on your application flow: use the callback for event-driven processing and retain status lookup for reconciliation.

Keep the HTTP callback path short. Persist or enqueue the event, return success, and let a worker do file downloads or other slow tasks. Track terminal jobs and callback processing separately so you can identify jobs whose callback was not recorded. The source material does not specify webhook charges or API pricing; check your Browshot account’s current pricing and usage terms for cost details. Poll at a measured interval to avoid unnecessary status requests.

7. Or skip the browser setup

If your goal is simply to retrieve a screenshot, ScreenshotNeo provides a screenshot API and MCP server. A single GET request returns an image or PDF; it can also run asynchronously with signed webhooks.

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

See the ScreenshotNeo API documentation for request options. Cookie banners are accepted and removed before capture, along with known newsletter popups and chat widgets. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed. An MCP server lets AI agents use screenshot tools. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots.

Sign up free for ScreenshotNeo: 1,000 screenshots a month, no card required.

8. FAQ

What does Browshot send to the hook URL?

A POST containing the JSON data returned by screenshot/info for the screenshot job.

Which statuses trigger the callback?

The documented terminal statuses are finished and error.

How many times can Browshot retry?

The documentation says up to two retries if the endpoint is slow or does not return a 20X response. It does not specify timing.

Should I use webhooks or polling?

Use hook to receive a completion notification and screenshot/info to check a job by ID, including as a fallback.

Sources