Webhooks API
Register HTTPS endpoints to receive real-time events from AIdenID. Webhook payloads are signed with HMAC-SHA256 for verification.
Create webhook endpoint
Register a new webhook endpoint. The create/rotate response carries one-time secret material and should be handled only by trusted backend code paths.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | HTTPS URL to receive webhook events |
events | string[] | Yes | Event types to subscribe to |
description | string | No | Human-readable description |
curl -X POST https://api.aidenid.com/v1/webhooks \
-H "Authorization: Bearer aid_your_api_key" \
-H "X-Org-Id: org_abc123" \
-H "X-Project-Id: proj_def456" \
-H "Idempotency-Key: <GENERATED_UNIQUE_KEY>" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/aidenid",
"events": ["email.received", "extraction.completed"],
"description": "Production webhook"
}'Use each idempotency key for one exact mutation intent. Keep the same key only when retrying the same request payload.
Response
{
"id": "wh_p1q2r3s4",
"url": "https://your-app.com/webhooks/aidenid",
"events": ["email.received", "extraction.completed"],
"secret_issued": true,
"secretPreview": "whsec_••••••••••••",
"description": "Production webhook",
"created_at": "2026-04-04T10:00:00Z"
}Treat create/rotate responses as non-cacheable secret envelopes: return Cache-Control: no-store and Pragma: no-cache from your proxy, return full secret material once, immediately store it in backend secret management, and only persist secretPreview in normal app records.
HTTP/1.1 200 OK
Cache-Control: no-store
Pragma: no-cacheList webhook endpoints
List all registered webhook endpoints for the current project.
Rotate webhook secret
Generate a new signing secret for a webhook endpoint. Rotate with a controlled overlap window: deploy verifier code that accepts both active and previous secrets first, then switch traffic to the new secret, and finally retire the previous secret after the overlap TTL.
Treat rotate responses exactly like create responses: include Cache-Control: no-store and Pragma: no-cache, return full secret material only once, and redact it after initial delivery.
Required: include a unique Idempotency-Key header for every rotate request. Reusing a key returns the original rotation response instead of generating another secret.
curl -X POST https://api.aidenid.com/v1/webhooks/wh_p1q2r3s4/rotate-secret \
-H "Authorization: Bearer aid_your_api_key" \
-H "X-Org-Id: org_abc123" \
-H "X-Project-Id: proj_def456" \
-H "Idempotency-Key: <GENERATED_UNIQUE_KEY>"Replay event
Re-deliver a specific event to a webhook endpoint. Useful for debugging or recovering from delivery failures.
Required: include a unique Idempotency-Key header for each replay attempt to prevent duplicate side effects during retries.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
event_id | string | Yes | The event ID to replay |
curl -X POST https://api.aidenid.com/v1/webhooks/wh_p1q2r3s4/replay \
-H "Authorization: Bearer aid_your_api_key" \
-H "X-Org-Id: org_abc123" \
-H "X-Project-Id: proj_def456" \
-H "Idempotency-Key: <GENERATED_UNIQUE_KEY>" \
-H "Content-Type: application/json" \
-d '{ "event_id": "evt_x1y2z3" }'Event types
Subscribe to any combination of these event types when creating a webhook:
| Event type | Description |
|---|---|
email.received | New email arrived at an identity inbox |
extraction.completed | Authentication code or link successfully extracted |
identity.provisioned | New identity created |
identity.extended | Identity TTL extended |
identity.squashed | Identity revoked |
identity.expired | Identity TTL elapsed |
Consumer notification events
When a human consumer's disposable email receives a verification code, AIdenID generates additional events for consumer notification delivery:
| Event type | Description |
|---|---|
consumer.notification.sent | OTP or magic link forwarded to consumer's personal email |
consumer.notification.failed | Notification delivery to personal email failed |
Webhook payload format
All webhook deliveries include two signature headers:
| Header | Description |
|---|---|
X-Signature | HMAC-SHA256 signature of the payload |
X-Timestamp | Unix timestamp of the delivery |
Payload example
{
"id": "evt_x1y2z3",
"type": "extraction.completed",
"timestamp": "2026-04-04T10:05:02Z",
"data": {
"identity_id": "ident_a1b2c3d4e5",
"extraction": {
"type": "otp",
"has_secret": true,
"redacted_value": "***",
"confidence": 0.99
}
}
}Verifying signatures
To verify a webhook delivery, compute the HMAC-SHA256 of {timestamp}.{payload} using your webhook secret, and compare it to the X-Signature header using a timing-safe comparison. Also enforce timestamp freshness and event-id replay protection before enqueueing any work.
import { createHmac, timingSafeEqual } from "crypto";
function verifyDelivery(
payload: string,
eventId: string,
signature: string | undefined,
timestamp: string | undefined,
secret: string
): boolean {
if (!signature || !timestamp) return false;
if (!/^[a-f0-9]{64}$/i.test(signature)) return false;
if (!/^[0-9]{10}$/.test(timestamp)) return false;
const ts = Number(timestamp);
const now = Math.floor(Date.now() / 1000);
if (!Number.isSafeInteger(ts) || Math.abs(now - ts) > 300) {
return false; // stale or malformed timestamp
}
const replayRecorded = tryRecordReplayEvent(eventId, ts);
if (!replayRecorded) {
return false; // event id already processed in freshness window
}
const signed = `${timestamp}.${payload}`;
const expected = createHmac("sha256", secret).update(signed).digest("hex");
const provided = Buffer.from(signature, "hex");
const computed = Buffer.from(expected, "hex");
if (provided.length !== computed.length) return false;
return timingSafeEqual(provided, computed);
}
// Replay guard must be one atomic write scoped to endpoint + event id.
// Redis example: SET replay:{endpointId}:{eventId} "1" NX EX 300
// SQL example:
// INSERT INTO webhook_replay_guard (webhook_endpoint_id, event_id, expires_at)
// VALUES ($1, $2, now() + interval '5 minutes')
// ON CONFLICT (webhook_endpoint_id, event_id) DO NOTHING;
// Return false on duplicate, and throw on storage errors so handlers can fail closed (503).
function tryRecordReplayEvent(eventId: string, timestampSec: number): boolean {
void eventId;
void timestampSec;
return true;
}Map stale, malformed, or replayed deliveries to stable error envelopes. If replay-store writes fail, return a deterministic 503 dependency_timeout response and skip side effects instead of failing open.
For a complete integration guide, see Webhook Integration.
Error codes
| Code | Status | Description |
|---|---|---|
webhook_not_found | 404 | Webhook endpoint does not exist |
event_not_found | 404 | Event ID not found for replay |
quota_exceeded | 403 | Webhook endpoint limit reached for your plan |
missing_idempotency_key | 400 | Mutation request missing Idempotency-Key header |