Verify and recover webhooks
Authenticate notifications, deduplicate events, and handle delivery retries.
Webhooks announce changes; retrieve the current report with a key scoped to reports:read. Notifications contain identifiers and status, never resumes, candidate contact details, interview transcripts, or signed storage URLs.
The event catalog covers every supported event with its trigger, exact payload example, and recommended action. Choose subscriptions for the transitions your integration handles.
Reports created through the app and API produce events when outbound webhooks are enabled. There is no automatic historical backfill.
{
"id": "a1cbb923-8b3c-4d05-b74f-623efc6d5f8d",
"type": "report.completed",
"schemaVersion": 1,
"occurredAt": "2026-09-25T12:00:00.000Z",
"data": {
"reportId": "report_example",
"attemptId": "attempt_example",
"status": "completed",
"code": null
}
}Set up a receiver
- Implement a POST handler on your public HTTPS server. This is your URL, not an endpoint on the ProBB AI application.
- In Settings → Developer → Webhooks, add the URL and choose event subscriptions.
- Store the one-time signing secret on your server and configure raw-body handling before JSON middleware.
- Send a synthetic test, verify its signature, and durably record its event ID before returning 2xx.
- Check Delivery history, then submit a synthetic workflow in the sandbox and confirm your backend fetches the corresponding result.
Use a unique event-ID constraint and an outbox or durable job record in the same receiver transaction. Acknowledge only after that transaction commits; perform slow downstream work afterward. If persistence fails, return a retryable server error. Do not return success merely because JSON parsed.
Verify before parsing
ProBB AI follows the Standard Webhooks signing format. Read the raw request bytes and the webhook-id, webhook-timestamp, and webhook-signature headers. Compute HMAC-SHA256 over id.timestamp.rawBody using the base64-decoded signing secret after removing whsec_. Compare with any v1 signature using a constant-time comparison. During rotation there can be two signatures separated by a space.
Reject timestamps more than five minutes in the past or future and keep your clock synchronised. Every retry gets a fresh timestamp and signature. Verification must happen before JSON parsing or reserialization, since changing whitespace changes the signature.
Download the tested TypeScript verifier. It uses Node's crypto module and accepts a Buffer, Headers, and your server-side signing secret:
const rawBody = Buffer.from(await request.arrayBuffer());
const eventId = verifyWebhook(rawBody, request.headers, signingSecret);
const event = JSON.parse(rawBody.toString("utf8"));
if (event.id !== eventId) throw new Error("Mismatched event ID");
// In one database transaction: insert eventId with a UNIQUE constraint
// and enqueue processing. If already present, acknowledge without reprocessing.
return new Response(null, { status: 204 });Apply a small request-body limit in your HTTP framework before reading. Validate the envelope against the OpenAPI schema, durably store its event ID and processing intent, then respond promptly. Your receiver should authenticate with the signing secret; ProBB AI does not send your customer API key.
Duplicates, ordering, and retries
Delivery is at least once and can arrive out of order. Deduplicate by the verified event ID, including manual replays. Compare attemptId with the report's current status before applying a delayed event. Fetching a report returns its current state; it is not a historical snapshot.
Any 2xx acknowledges a delivery. Network failures, timeouts, 408, 429, and 5xx are retried. Other HTTP failures, including redirects, end that delivery. The sender has a ten-second deadline and ignores response bodies.
There are at most seven HTTP attempts: immediately, then after approximately one minute, five minutes, 30 minutes, two hours, eight hours, and 24 hours, with jitter. A valid Retry-After on retryable responses can extend the wait up to 24 hours. Delivery stops when its 30-day retention window expires. Transport recovery can produce duplicate requests even when only one attempt is recorded.
Recover a receiver
- Check Settings → Developer → Delivery history for safe outcome codes, HTTP status, and next retry time.
- Fix HTTPS/certificate/DNS issues, response latency, authentication, or your receiver's processing/storage failure.
- Use Send test. It contains no report or candidate data.
- Use Replay for a retained succeeded or failed delivery to the current active endpoint. This preserves its event ID and body and starts a fresh retry budget.
The UI shows the seven most recent HTTP attempts per delivery. Paused endpoints hold pending deliveries; deleted endpoints and changed URLs cancel unsent deliveries to the old destination. In-flight requests cannot be recalled. A report deleted from ProBB AI can return 404 when an older notification reaches you; handle that without endlessly retrying the event.
Event catalogue and missed events
Alongside report.completed, report.requires_action, and report.failed, subscribe to report.result_ready for a versioned publication and report.deleted for erasure. Interview events are interview.invited, interview.started, interview.completed, interview.expired, interview.preparation_failed, interview.result_ready, interview.result_failed, and interview.invitation_failed. batch.completed provides a batch ID and success/failure counts. See each payload in the event catalog.
Interview completion and evaluation readiness are separate events. A result revision identifies the publication; retrieve the result using its resource ID. Events exclude candidate emails, external reference values, transcripts, documents, tokens, and signed URLs.
With events:read, poll GET /api/v1/events?limit=100. Save nextCursor after durably processing the page, even when the page is empty. Follow it with the same filters (type, reportId, or interviewId). The feed follows durable publication order within your workspace; webhook delivery can still be out of order. Filters and tenant identity are bound to the cursor. A deleted body's tombstone retains only event ID, type, and occurrence time.
Retained events are available for 30 days. 410 EVENT_CURSOR_EXPIRED means the recovery position is too old: reconcile current reports/interviews, then obtain a fresh feed position. Polling never sends webhooks or charges for new analysis.
To recover notifications missed while paused, use Preview catch-up on the current active endpoint. Choose an ISO timestamp window and inspect the destination and missing-event count before confirming. One preview is limited to 500 retained events and expires after ten minutes. It is bound to the endpoint revision; changing its URL or subscriptions requires a fresh preview. Already-created deliveries are not duplicated. Use Replay for an existing retained delivery.
Delivery history accepts report and interview/attempt ID filters. The request ID, event ID, and safe outcome code let you correlate a failure without exposing receiver response bodies or credentials.
Download the Python verifier if your receiver uses Python. Both examples verify raw bytes and rotation signatures before parsing JSON. Persist deduplication and processing intent transactionally in your own receiver.