Webhooks
Webhooks send an HTTPS POST to your server when something happens, so you do not have to poll.
Events
Section titled “Events”| Event | Sent when | data |
|---|---|---|
alert.created | A detector raises an alert | alert_id, event_id |
screening.potential_match | A payee screening needs review | screening_id, event_id |
pack.ready | A pack has finished building | pack_id |
The request
Section titled “The request”{ "type": "alert.created", "created_at": "2026-10-17T10:05:12.440Z", "data": { "alert_id": "alr_01JA8X4K3T9V2C7M5N6P8R0S1W", "event_id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W" }}The body is small on purpose. Fetch the full record with the id, using GET /v1/alerts/{id} for an alert or GET /v1/packs
for a pack. That way a webhook never carries data you did not ask for.
| Header | Value |
|---|---|
HalvorAI-Signature | v1= followed by a hex HMAC-SHA256 |
HalvorAI-Timestamp | The time the delivery was made, as an RFC 3339 timestamp |
Content-Type | application/json |
Verify the signature
Section titled “Verify the signature”Always verify a delivery before you act on it. The signature is an HMAC-SHA256, keyed with your endpoint secret, over the timestamp, a dot, and the exact raw request body.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyHalvorWebhook(secret: string, timestamp: string, rawBody: string, signature: string): boolean { const expected = `v1=${createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex')}`; const a = Buffer.from(expected); const b = Buffer.from(signature); const fresh = Math.abs(Date.now() - Date.parse(timestamp)) < 5 * 60_000; return fresh && a.length === b.length && timingSafeEqual(a, b);}- Use the raw body bytes, not a re-serialized copy.
- Compare in constant time.
- Reject a timestamp more than five minutes old, which stops replays.
Respond and retry
Section titled “Respond and retry”Return any 2xx status to acknowledge a delivery. HalvorAI treats a 5xx, a 429, or a timeout as a failure and retries up to
five times, waiting 0.5, 1, 2 and 4 seconds between attempts. A 4xx other than 429 is treated as a refusal and not retried.
Deliveries can repeat, and can arrive out of order. Make your handler idempotent by using the ids in data, and fetch current
state rather than trusting the order.
Set up an endpoint
Section titled “Set up an endpoint”Webhook endpoints and their secrets are configured per organization. A secret is at least 32 characters, and a new endpoint needs one. Ask your HalvorAI contact to add or change an endpoint during the pilot; self-service setup in the dashboard is planned.