Guide
Webhooks
Subscribe to hiring events, verify signatures and handle retries.
Webhooks push hiring events to your server as they happen, so you don't have to poll.
Subscribing
Subscribe one HTTPS endpoint per event type (scope webhooks:write):
curl -X POST https://api.aiviue.com/v1/webhooks/subscriptions \
-H "Authorization: Bearer $TOKEN" \
-H "X-Tenant-ID: $TENANT_ID" \
-H "X-Idempotency-Key: sub-screening-completed" \
-H "Content-Type: application/json" \
-d '{"event_type": "screening.completed", "url": "https://example.com/aiviue/webhooks"}'
GET /v1/webhooks/subscriptionslists your subscriptions (webhooks:read).DELETE /v1/webhooks/subscriptions/{subscription_id}removes one.GET /v1/reference-datareturns the full event catalog, including when each event fires.
Event types
creative.ready, campaign.live, screening.ready, voice.ready, distribution.started,
candidate.created, candidate.updated, screening.started, screening.completed, interview.scheduled,
interview.completed, grading.completed, campaign.analytics.updated, campaign.paused, campaign.closed.
The API reference describes each event's payload.
The request we send
A POST with a JSON body:
{
"eventId": "...",
"eventType": "screening.completed",
"eventVersion": "...",
"occurredAt": "2026-11-02T10:00:00Z",
"sourceSystem": "AIVIUE",
"targetSystem": "YOUR_PARTNER_SLUG",
"idempotencyKey": "...",
"payload": { }
}
Optional fields: correlationId, parentEventId, applicationId, sequenceNumber.
Headers:
| Header | Meaning |
|---|---|
X-Aiviue-Signature | sha256=<hex HMAC> of the raw body — verify it |
X-Aiviue-Event | The event type |
X-Aiviue-Delivery | Unique ID of this delivery attempt |
X-Aiviue-Sequence | Event sequence number, when present |
X-Correlation-Id | Correlation ID, when present |
Verifying the signature
Your webhook secret is issued with your partner account. Compute an HMAC-SHA256 of the raw request body bytes with that secret and compare it to the header in constant time. Don't parse and re-serialize the JSON first.
import hashlib, hmac
def is_valid(secret: str, raw_body: bytes, header: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header or "")
import crypto from "node:crypto"
function isValid(secret, rawBody, header) {
const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex")
const a = Buffer.from(expected)
const b = Buffer.from(header ?? "")
return a.length === b.length && crypto.timingSafeEqual(a, b)
}
The signature does not include a timestamp, so de-duplicate on eventId (or idempotencyKey) to ignore
replays and retries you have already processed.
Responding and retries
- Return any
2xxstatus within 30 seconds to acknowledge. Do slow work after responding. - Anything else, or a timeout, is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 8 hours — 6 attempts in total.
- After the last failed attempt the delivery is marked dead and our team is alerted.
- Deliveries can arrive more than once and out of order. Use
eventIdfor de-duplication andsequenceNumber/occurredAtfor ordering.
Delivery log and replay
GET /v1/webhooks/deliveriesshows recent deliveries and their status.POST /v1/webhooks/deliveries/{delivery_id}/replay(webhooks:write) sends a delivery again with the sameidempotencyKey. Deliveries older than 30 days can't be replayed (403 REPLAY_EXPIRED), a pending delivery returns409 CONFLICT, and replays are rate limited (429 RATE_LIMITED).