Webhooks
Signed callbacks for run and browser lifecycle events, verified with the Standard Webhooks scheme.
What is sent
A callback per subscribed event, containing ids and status only: fetch anything private through the resource API.
{ "id": "evt_…", "type": "run.succeeded", "timestamp": "2026-09-22T10:14:02.000Z", "project_id": "prj_…", "data": { "id": "run_…", "status": "succeeded", "session_id": "as_…", "browser_id": "br_…", "workspace_id": "ws_…", "profile_id": null }}Subscribable events: browser.starting, browser.running, browser.stopping, browser.stopped, browser.failed, browser.cleanup_failed, run.queued, run.running, run.succeeded, run.failed, run.cancelled. Scheduled runs produce the same events. A test delivery (webhook.test, data: { "id": "wh_…", "test": true }) is sent only on request.
Verify a delivery
Requests follow Standard Webhooks v1: headers webhook-id, webhook-timestamp (Unix seconds) and webhook-signature (v1,<Base64 HMAC-SHA256>). The signed message is the exact UTF-8 bytes of id.timestamp.raw_body, keyed with the Base64 part of the secret after its whsec_ prefix. Verify before parsing; compare in constant time; reject timestamps outside five minutes; deduplicate by event id in the same transaction as your own write.
from stealth_browser import verify_webhook # in your HTTP handlerevent = verify_webhook(raw_body, request.headers, os.environ["STEALTH_WEBHOOK_SECRET"]) # raises StealthApiError("webhook_signature")print(event["type"], event["data"]["id"])import { verifyWebhook } from '@stealth-browser/cloud-sdk'; // in your HTTP handlerconst event = await verifyWebhook({ body: rawBody, headers: request.headers, secret: process.env.STEALTH_WEBHOOK_SECRET }); // throws webhook_signatureconsole.log(event.type, event.data.id);The extra x-stealth-delivery-id and x-stealth-attempt-id headers are diagnostics; only the signed message establishes trust.
Delivery
A 2xx acknowledges. Connection, DNS and timeout errors, 408, 425, 429 and 5xx are retried up to six attempts at 30 s, 2 m, 10 m, 1 h and 6 h (Retry-After can extend a delay to 24 h); any other response, redirects included, fails permanently. Attempts have a ten-second budget and keep 4 KiB of the response. Events can arrive more than once or out of order, particularly after a crash; a retried notification never re-executes its run.
Callback URLs must be public HTTPS without userinfo, query or fragment; every DNS answer is checked and private, loopback, link-local and metadata addresses are refused. Redirects are not followed.
Managing endpoints
Endpoints are managed by owners and admins with an account session in the console (Configuration → Webhooks); keys cannot manage or inspect them.
| Method and path | Result |
|---|---|
GET /v1/webhooks/policy | Supported events and limits |
GET /v1/webhooks, GET /v1/webhooks/{id} | Endpoints with delivery counters; one endpoint (never its secret) |
POST /v1/webhooks | { "name", "url", "events", "enabled" } → the endpoint with signing_secret, once |
PATCH /v1/webhooks/{id} | Changed fields plus revision; edits and pauses cancel pending deliveries |
DELETE /v1/webhooks/{id} | { "revision" } |
POST /v1/webhooks/{id}/rotate | { "revision" } → a new secret, once; already dispatched requests may still use the old key |
POST /v1/webhooks/{id}/test | {} with Idempotency-Key → a real webhook.test delivery |
GET /v1/webhooks/{id}/deliveries?before= | The newest 50 records and a cursor |
GET /v1/webhooks/{id}/deliveries/{delivery} | Payload, attempts, HTTP status, timings and the bounded response text |
POST /v1/webhooks/{id}/deliveries/{delivery}/replay | {} with Idempotency-Key → a new delivery with the same event id and body |
Limits: 20 live endpoints per project; pending deliveries expire after seven days and history is kept seven days after completion.
