AbuseZero

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" }
}
FieldTypeDescription
object"event"The object type.
idstringThe event ID, starting with evt_. The same on every attempt and every resend, and equal to the abusezero-webhook-id header.
typestringThe event type.
api_version"v1"The API version data follows.
created_atstringWhen the event was recorded.
workspacestringThe workspace ID, starting with ws_.
dataobjectThe object the event is about, in the shape the AbuseZero REST API returns. See each event type below.

Request headers

HeaderValue
content-typeapplication/json
user-agentAbuseZero-Webhooks/1.0
abusezero-webhook-idThe event ID, evt_….
abusezero-webhook-timestampWhen this attempt was signed, in Unix seconds.
abusezero-webhook-signatureOne 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 replyResult
Any 2xx status within 10 secondsThe delivery succeeded.
A 3xx statusFailed. Redirects aren't followed.
A 4xx or 5xx statusFailed, and retried.
No reply within 10 seconds, a refused connection or a certificate problemFailed, 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:

AttemptWhen
1As soon as the event is recorded
21 minute after the first attempt
35 minutes after
430 minutes after
52 hours after
612 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

TypeSent whenTied to a benefit
evaluation.completedAn evaluation returns allow or deny.Yes
evaluation.skippedAn evaluation is skipped.Yes
outcome.updatedAn outcome's state changes.Yes
access_list_entry.createdAn allow or block list entry is added.When the entry is for one benefit
access_list_entry.deletedAn allow or block list entry is deleted.When the entry is for one benefit
import.completedAn import finishes.No
export.completedAn export is ready.No
data_deletion.completedA user's data deletion finishes.No
webhook.testA 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 with previously_authorized, return the original evaluation and send no new event.
  • An allowed claim's starting outcome arrives here, in outcome. Later changes arrive as outcome.updated.
  • In observe mode, mode is observe, result is allow, and observed_result holds 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"
}
Changestateprevious_state
You report or correct an outcomeThe reported stateThe state before the report
You record a grant after a denygranted, with source: "override"null when the claim had no outcome
An import changes an evaluation's outcomegrantedThe state before the import
No report arrives by due_atoutcome_unknownawaiting_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.

How is this guide?

Last updated on

On this page