Webhook events
The AbuseZero webhook event object, request headers, signatures, retries and every event type with its payload.
Every webhook request is a POST with one event in its body. This page describes the event, the headers that carry its signature, how AbuseZero treats your reply, and the payload of every event type. To set up an endpoint, see Receive webhooks.
The event object
{
"object": "event",
"id": "evt_9b3e7c21-4d6a-4f58-a1e2-7c0d5b8f3a64",
"type": "evaluation.completed",
"api_version": "v1",
"created_at": "2026-10-29T14:02:47.000Z",
"workspace": "ws_2c8f4a1d-7e3b-4c69-9d05-6a1f3e7b2c48",
"data": { "object": "evaluation", "id": "eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93" }
}| Field | Type | Description |
|---|---|---|
object | "event" | The object type. |
id | string | The event ID, starting with evt_. The same on every attempt and every resend, and equal to the abusezero-webhook-id header. |
type | string | The event type. |
api_version | "v1" | The API version data follows. |
created_at | string | When the event was recorded. |
workspace | string | The workspace ID, starting with ws_. |
data | object | The object the event is about, in the shape the AbuseZero REST API returns. See each event type below. |
Request headers
| Header | Value |
|---|---|
content-type | application/json |
user-agent | AbuseZero-Webhooks/1.0 |
abusezero-webhook-id | The event ID, evt_…. |
abusezero-webhook-timestamp | When this attempt was signed, in Unix seconds. |
abusezero-webhook-signature | One or more signatures separated by spaces, each in the form v1,<base64>. |
Signatures
Each signature is HMAC-SHA256 of {abusezero-webhook-id}.{abusezero-webhook-timestamp}.{body}, keyed with the endpoint's signing secret after its whsk_ prefix, decoded from base64. The result is base64-encoded and sent after v1,.
abusezero-webhook-signature holds one signature for each secret the endpoint is signing with. After a rotation, that's two for 24 hours: one with the new secret and one with the previous secret. Accept the request when one of them matches.
Refuse requests whose abusezero-webhook-timestamp is more than 5 minutes from your clock. Every retry is signed again with a new timestamp, so an older timestamp means the request was captured and replayed.
verifyWebhook() in the AbuseZero Node.js SDK does all of this. For other languages, see Verify in other languages.
Responding to a delivery
| Your reply | Result |
|---|---|
Any 2xx status within 10 seconds | The delivery succeeded. |
A 3xx status | Failed. Redirects aren't followed. |
A 4xx or 5xx status | Failed, and retried. |
| No reply within 10 seconds, a refused connection or a certificate problem | Failed, and retried. |
The first 1 KB of your reply's body is kept with the attempt in the delivery log.
Retries
An event delivery that fails is attempted up to 6 times, measured from the first attempt:
| Attempt | When |
|---|---|
| 1 | As soon as the event is recorded |
| 2 | 1 minute after the first attempt |
| 3 | 5 minutes after |
| 4 | 30 minutes after |
| 5 | 2 hours after |
| 6 | 12 hours after |
After the sixth failed attempt, the delivery's status is failed. You can send it again at any time while the delivery log keeps it. Test events and resends are attempted once.
An endpoint whose counted attempts have all failed for 3 days is paused. New events aren't sent to it, and the workspace owner and admins are emailed. See The webhook endpoint object.
Event types
| Type | Sent when | Tied to a benefit |
|---|---|---|
evaluation.completed | An evaluation returns allow or deny. | Yes |
evaluation.skipped | An evaluation is skipped. | Yes |
outcome.updated | An outcome's state changes. | Yes |
access_list_entry.created | An allow or block list entry is added. | When the entry is for one benefit |
access_list_entry.deleted | An allow or block list entry is deleted. | When the entry is for one benefit |
import.completed | An import finishes. | No |
export.completed | An export is ready. | No |
data_deletion.completed | A user's data deletion finishes. | No |
webhook.test | A test event is sent. | No |
An endpoint's benefit filter applies only to events tied to a benefit. Events tied to no benefit go to every endpoint that subscribes to them.
evaluation.completed
Sent when an evaluation returns allow or deny, including on benefits in observe mode. data is the evaluation object, exactly as GET /v1/evaluations/{id} returns it. Signal assessments and evidence are never included. Read them with GET /v1/evaluations/{id}/evidence.
{
"object": "event",
"id": "evt_9b3e7c21-4d6a-4f58-a1e2-7c0d5b8f3a64",
"type": "evaluation.completed",
"api_version": "v1",
"created_at": "2026-10-29T14:02:47.000Z",
"workspace": "ws_2c8f4a1d-7e3b-4c69-9d05-6a1f3e7b2c48",
"data": {
"object": "evaluation",
"id": "eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93",
"benefit": "pro_trial",
"claim_id": "trial_user_4821",
"result": "deny",
"reason": {
"code": "prior_claim_device",
"message": "This device is linked to earlier claims."
},
"outcome": { "state": null, "due_at": null },
"policy_version": 3,
"created_at": "2026-10-29T14:02:47.000Z",
"mode": "protect",
"risk": { "score": null, "band": "not_assessed" },
"inputs": {
"email": "received",
"ip": "received",
"user_id": "received",
"phone": "missing",
"payment": "missing",
"device": "received",
"browser": "received"
},
"browser_data": "collected",
"policy": {
"id": "pol_c4d8e2a7-1b5f-4a93-8e6d-2f0b9c7a1e35",
"version": 3
}
}
}- One evaluation sends one event. A retry with the same
Idempotency-Key, and a repeat claim answered withpreviously_authorized, return the original evaluation and send no new event. - An allowed claim's starting outcome arrives here, in
outcome. Later changes arrive asoutcome.updated. - In observe mode,
modeisobserve,resultisallow, andobserved_resultholds the result your policy decided. An endpoint with Denied evaluations only receives no events from benefits in observe mode.
evaluation.skipped
Sent when an evaluation is skipped. data holds the benefit key and the skip reason. It carries no claim ID: your server learns which claim was skipped from the evaluation response.
{
"object": "evaluation",
"status": "skipped",
"reason": "insufficient_credits",
"benefit": "pro_trial"
}Skip reasons are insufficient_credits, billing_overdue, service_inactive and benefit_paused. New reasons can be added within v1. A retry with the same Idempotency-Key sends no second event.
outcome.updated
Sent when an outcome's state changes. data is the outcome object with one more field, previous_state.
{
"object": "outcome",
"id": "out_5a9c3e17-2b4d-4f80-9c6e-1d7b3a5f8e02",
"evaluation": "eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93",
"state": "granted",
"source": "evaluation",
"occurred_at": "2026-10-29T14:02:47.000Z",
"due_at": "2026-11-05T14:02:47.000Z",
"previous_state": "awaiting_outcome"
}| Change | state | previous_state |
|---|---|---|
| You report or correct an outcome | The reported state | The state before the report |
| You record a grant after a deny | granted, with source: "override" | null when the claim had no outcome |
| An import changes an evaluation's outcome | granted | The state before the import |
No report arrives by due_at | outcome_unknown | awaiting_outcome |
Reporting the state an outcome already has sends no event. Outcomes an import adds without an earlier evaluation are reported together by import.completed.
access_list_entry.created
Sent when an allow or block list entry is added, from the dashboard, the AbuseZero REST API, the AbuseZero CLI or the AbuseZero MCP server. A value that was already_listed sends no event. data is the access list entry object. Like the REST API, it doesn't include the entry's value.
{
"object": "access_list_entry",
"id": "lst_2b7d9e41-6a3c-4f85-9e17-c0d4a8b52f63",
"kind": "block",
"type": "domain",
"benefit": null,
"reason": "Repeat trial claims traced to this domain",
"expires_at": "2026-11-28T11:20:45.000Z",
"created_at": "2026-10-29T11:20:45.000Z"
}access_list_entry.deleted
Sent when an allow or block list entry is deleted. data is the deleted entry, in the same shape as access_list_entry.created. An entry that reaches its expires_at stops matching without an event.
import.completed
Sent when an import finishes applying its rows. data is the import object with state: "complete" and its final counts.
{
"object": "import",
"id": "imp_4c1e8a2d-9b3f-4d67-a5e0-2f8c6b1d9e74",
"state": "complete",
"failure": null,
"total": 1200,
"validated": 1200,
"processed": 1200,
"imported": 1187,
"skipped": 9,
"rejected": 4,
"created_at": "2026-10-29T09:12:05.000Z",
"completed_at": "2026-10-29T09:14:41.000Z"
}export.completed
Sent when an export is ready to download. data is the export object with state: "complete". Download it before expires_at.
{
"object": "export",
"id": "exp_6e0a2c4b-8d1f-4b3e-9a5c-7f2d4b6e8a01",
"state": "complete",
"failure": null,
"rows": 4812,
"include_evidence": false,
"completed_at": "2026-10-29T12:04:37.000Z",
"expires_at": "2026-10-30T12:03:58.000Z"
}data_deletion.completed
Sent when a user's data deletion finishes. data is the deletion object.
{
"object": "deletion",
"id": "del_8b4f0d2a-6c1e-4a7b-9d3f-5e2a8c0b4d16",
"kind": "subject",
"state": "complete",
"created_at": "2026-10-29T13:40:12.000Z",
"completed_at": "2026-10-29T13:40:31.000Z"
}The deletion also removes that user's events and deliveries from your delivery logs.
webhook.test
Sent when someone sends a test event. Endpoints don't subscribe to it.
{
"object": "webhook_test",
"endpoint": "whe_7d2e9a40-3c1b-4f65-8e27-b9a0c4d61f53",
"message": "This is a test event from AbuseZero."
}Versioning
Events follow API version v1. Within v1, AbuseZero only makes additive changes to events: new event types, new fields in data, and new values for codes and enums. Write your endpoint so it ignores event types and fields it doesn't recognize, replies 2xx to them, and handles unknown values as a generic case.
Related
How is this guide?
Last updated on