Skip to content

Webhook Endpoints

Webhooks allow you to receive real-time HTTP callbacks when events occur in VeraID. Each webhook is configured with a target URL and a list of event types to subscribe to. All webhook payloads are signed with HMAC-SHA256 for verification.

These routes take a signed-in session (the dashboard): any member may list and read webhooks, and only an organization admin may create, change, test or delete them. API keys are not accepted.

List Webhooks

GET /api/v1/webhooks

Example Response

{
"data": [
{
"id": "c3f1e2d4-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
"name": "Production Event Handler",
"url": "https://api.company.com/webhooks/veraid",
"events": ["identity.created", "credential.revoked", "alert.triggered"],
"isActive": true,
"lastTriggeredAt": "2026-03-19T09:30:00.000Z",
"failureCount": 0,
"createdAt": "2026-01-15T10:00:00.000Z",
"updatedAt": "2026-03-01T14:30:00.000Z"
}
]
}

Create Webhook

POST /api/v1/webhooks

Request Body

FieldTypeRequiredDescription
namestringYes2 to 100 characters
urlstringYesWhere deliveries are sent: an HTTPS URL to a public address
eventsstring[]YesAt least one of the event types

Example Request

{
"name": "Security Event Handler",
"url": "https://api.company.com/webhooks/veraid-security",
"events": ["identity.suspended", "credential.revoked", "anomaly.detected", "alert.triggered"]
}

Example Response (201)

{
"id": "d4e5f6a7-8b9c-4d0e-9f1a-2b3c4d5e6f70",
"organizationId": "8f14e45f-ceea-4c6c-9e7b-1a2b3c4d5e6f",
"name": "Security Event Handler",
"url": "https://api.company.com/webhooks/veraid-security",
"events": ["identity.suspended", "credential.revoked", "anomaly.detected", "alert.triggered"],
"secret": "9f2c...64 hex characters...a1",
"isActive": true,
"failureCount": 0,
"lastTriggeredAt": null,
"createdAt": "2026-03-19T10:00:00.000Z",
"updatedAt": "2026-03-19T10:00:00.000Z"
}

Get Webhook

GET /api/v1/webhooks/{id}

Answers the webhook as in the list (id, name, url, events, isActive, lastTriggeredAt, failureCount, createdAt, updatedAt), never its secret.


Update Webhook

PUT /api/v1/webhooks/{id}

Takes any of name, url and events, as on create, and answers the updated webhook without its secret. A withdrawn event name (credential.used) is dropped rather than refused, so a list read from an older client can be sent back unchanged; if nothing else is left, the update fails validation.

{
"events": ["identity.created", "identity.suspended", "credential.revoked", "credential.expired", "anomaly.detected", "alert.triggered"]
}

Delete Webhook

DELETE /api/v1/webhooks/{id}

No further events are delivered to the endpoint. Answers { "success": true }.


Test Webhook

Sends a test delivery (event identity.created, with data.test: true) to check connectivity and signature verification.

POST /api/v1/webhooks/{id}/test

Example Response

{
"success": true,
"statusCode": 200,
"duration": 120
}

On failure, success is false and error says why. A test delivery is signed like any other, but carries X-Webhook-Id (the webhook’s id) instead of X-Webhook-Delivery-Id.

Delivery History

GET /api/v1/webhooks/{id}/deliveries lists the webhook’s delivery attempts, each with its status code and error.


Event Types

Subscribe to any combination of the following event types:

Identity Events

EventDescription
identity.createdA new identity was created
identity.updatedAn identity’s configuration was modified
identity.suspendedAn identity was suspended
identity.revokedAn identity was permanently revoked or deleted

Credential Events

EventDescription
credential.createdA new credential was issued
credential.expiredA credential reached its expiration date
credential.revokedA credential was manually revoked or rotated

credential.used is no longer accepted in a new subscription. It was never sent: every API call uses a credential, so it would be one delivery per call, and each use is already recorded in the audit log as credential.verify. A webhook saved with it keeps it, where it matches nothing; it is left out of the webhook as answered, and dropped from an update that sends it back.

Security Events

EventDescription
policy.violatedA policy refused a request: a DENY policy matched, or an ALLOW policy’s condition failed
anomaly.detectedAnomalous behavior was detected by the analysis engine
alert.triggeredA security alert was raised, or an open alert escalated to a higher severity

Webhook Payload Format

Every delivery is a POST with a JSON body in this envelope:

{
"event": "credential.created",
"timestamp": "2026-03-19T10:00:00.000Z",
"organizationId": "8f14e45f-ceea-4c6c-9e7b-1a2b3c4d5e6f",
"data": {
"credential": {
"id": "cred_xyz789",
"name": "deploy-key",
"type": "API_KEY",
"identityId": "id_def456"
}
}
}

data depends on the event. No payload carries a secret value.

alert.triggered

Sent when a security alert is raised, and again when an open alert escalates to a higher severity (escalation is present then). Repeats of an open alert at the same severity are not sent.

{
"event": "alert.triggered",
"timestamp": "2026-10-15T09:00:00.000Z",
"organizationId": "8f14e45f-ceea-4c6c-9e7b-1a2b3c4d5e6f",
"data": {
"alert": {
"id": "a1b2c3d4-0000-4000-8000-000000000001",
"type": "AI_AGENT_QUOTA_EXCEEDED",
"severity": "CRITICAL",
"status": "OPEN",
"identityId": "id_def456",
"title": "Budget exceeded: support-bot (daily)",
"createdAt": "2026-10-15T08:41:12.000Z"
},
"escalation": { "previousSeverity": "HIGH" }
}
}

The payload is a reference to the alert: it never includes the alert’s message or details, which can hold a source IP, a commit author or a preview of a leaked secret. The full alert is on VeraID’s Alerts page.

policy.violated

Sent when policy enforcement refuses a request because of a policy: a DENY policy matched it (condition is null), or an ALLOW policy applied to it but one of its conditions failed (an IP range, a time window, a rate limit). A request refused only because no policy applies to it is not a violation and is not sent. At most one is sent a minute for the same policy, caller and action, so a client retrying a refused call sends one, not one per attempt.

{
"event": "policy.violated",
"timestamp": "2026-10-15T09:00:00.000Z",
"organizationId": "8f14e45f-ceea-4c6c-9e7b-1a2b3c4d5e6f",
"data": {
"policy": { "id": "pol_abc123", "name": "Office network only", "effect": "ALLOW" },
"condition": { "type": "ip_range", "reason": "IP 203.0.113.42 not in allowed ranges" },
"action": "credential.read",
"resource": { "type": "credential", "id": null },
"actor": { "type": "API_KEY", "userId": null, "identityId": "id_def456", "credentialId": "cred_xyz789" }
}
}
Field
policyThe policy that refused the request. effect is DENY for a DENY policy that matched, ALLOW for an ALLOW policy whose condition failed
conditionnull for a DENY policy. Otherwise the failed condition: type as in the policy (ip_range, time_window, rate_limit, geo_fencing, require_mfa, …), and reason, the one the refused caller sees in its 403’s details.conditions, or null when the condition gave none
actionThe policy action that was refused, such as credential.read or identity.update
resourcetype, and id when the route names one (null for a list or a create)
actortype (USER or API_KEY), with the user’s id, or the identity’s and credential’s ids. The credential is named by id, never its value

type, action and resource.type are open sets: new values can appear as enforcement reaches more routes and conditions.


Signature Verification

Every delivery is signed with HMAC-SHA256 using the webhook’s signing secret. Verify the signature to make sure the payload came from VeraID and was not altered.

Headers

HeaderDescription
X-Webhook-Signaturesha256= followed by the hex HMAC-SHA256 of {timestamp}.{body}
X-Webhook-TimestampUnix time, in seconds, when the delivery was signed
X-Webhook-EventThe event name, as in the body’s event
X-Webhook-Delivery-IdThe delivery’s id, the same on every retry of it: use it to ignore duplicates

Verification Process

  1. Concatenate the timestamp header and the raw request body: {timestamp}.{body}.
  2. Compute the HMAC-SHA256 of that string with your webhook’s signing secret, as hex.
  3. Compare it with the X-Webhook-Signature header, after its sha256= prefix.
import crypto from 'crypto';
function verifyWebhookSignature(
rawBody: string,
signatureHeader: string | undefined,
timestamp: string | undefined,
secret: string
): boolean {
if (!signatureHeader || !timestamp) return false;
const expected = Buffer.from(
'sha256=' + crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex')
);
const received = Buffer.from(signatureHeader);
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

The SDKs’ helpers do this, with the timestamp check: verifyWebhookSignature(rawBody, signature, timestamp, secret) in TypeScript and Python (verify_webhook_signature), and VerifyWebhookSignature(payload, signature, timestamp, secret) in Go.


Retry Policy

A delivery is attempted up to 5 times. After a failed attempt, the next waits:

After attemptDelay
1st30 seconds
2nd2 minutes
3rd8 minutes
4th32 minutes

A delivery counts as failed if:

  • The endpoint returns a non-2xx HTTP status code.
  • The endpoint does not respond within 10 seconds.
  • A network error occurs (DNS resolution failure, connection refused, etc.).

A delivery to a URL that is not allowed (an internal or private address) fails at once and is not retried. After the fifth failed attempt, a delivery is no longer retried. Every attempt, with its status code and error, is listed in the webhook’s delivery history (GET /api/v1/webhooks/{id}/deliveries).