# AbuseZero AI Risk Engine (/docs/ai-risk-engine)
The AbuseZero AI Risk Engine is the intelligence at the heart of AbuseZero. AI works hand in hand with AbuseZero's proprietary algorithm and its 140,000+ risk signal combinations, so the claims you protect are met with far more than rules alone.
## Capabilities [#capabilities]
- **Proprietary algorithm.** AbuseZero's proprietary algorithm is built on 140,000+ risk signal combinations. It reads a claim's signals together, never one at a time, because repeat claims hide in how signals line up.
- **AI cross-check.** AbuseZero's AI checks the claim again, alongside the algorithm. Together they reach a depth that no single check could reach on its own.
- **Single-call execution.** The AI Risk Engine works within the evaluation's [time limit](/docs/api-reference#time-limits), in the same call that returns `allow` or `deny`. Your server never waits on a second request.
- **Consistent response.** However deep the analysis, the response keeps the same shape: a result, the [AbuseZero Risk Score](/docs/concepts/risk-score-and-signals) and its band, and a [reason](/docs/reason-codes). The proprietary analysis behind them stays inside AbuseZero.
- **Upgrades without integration changes.** AbuseZero can improve its proprietary engine behind the API at any time. Your code, [policy](/docs/concepts/policies-and-rules), [lists](/docs/concepts/allow-and-block-lists) and settings keep working when it does.
```mermaid
flowchart LR
A[Your claim] --> B[AbuseZero AI Risk Engine]
B --> C[allow or deny]
```
## Increase evidence coverage [#increase-evidence-coverage]
The AI Risk Engine works with the evidence each claim carries. The more you send, the more of the engine comes into play.
1. **Send every identifier you have.** See [Send identifiers](/docs/guides/send-identifiers).
2. **Add browser protection** to every page where a claim starts. See [Set up browser protection](/docs/guides/browser-setup).
3. **Report outcomes,** so every granted claim becomes history for the next one. See [Report outcomes](/docs/guides/report-outcomes).
## Next steps [#next-steps]
What every evaluation reports back.
One request, one final result.
Give the AI Risk Engine more evidence on every claim.
# Allow and block lists (/docs/concepts/allow-and-block-lists)
Allow and block lists decide specific claims outright. An allow list entry allows every claim it matches. A block list entry denies every claim it matches. Your [policy](/docs/concepts/policies-and-rules) doesn't need to change.
## Why it matters [#why-it-matters]
Some decisions are already made before a claim arrives. A partner's team should always get its trial. Your QA accounts should never be blocked. A domain behind a wave of repeat claims should be stopped today, not after a policy review. Lists handle these cases in seconds, with a reason your team can read later.
## Key capabilities [#key-capabilities]
- **Final for matching claims.** A matching entry decides the claim, whatever the policy says. If a claim matches both an allow and a block entry, the block entry wins.
- **Immediate effect.** Entries apply to new evaluations as soon as they are saved.
- **Consistent matching.** Each value is matched exactly as AbuseZero matches identifiers during an [evaluation](/docs/concepts/evaluations), so an entry catches the claims it's meant to catch.
- **Expiry.** An entry can expire at an exact time or after a set number of days, measured on AbuseZero's clock. Expired entries stop matching on their own.
- **Bulk changes.** Add up to 50 values in one request. Every value gets its own result, and one bad value never blocks the rest.
- **Audit trail.** Every entry carries a reason, and every change is recorded in your [audit log](/docs/api-reference/audit-logs).
## Configuration [#configuration]
| Field | Values |
|---|---|
| `kind` | `allow` or `block`. |
| `type` | What the value is. See the table below. |
| `value` or `values` | One value, or up to 50 values. Each is up to 254 characters. |
| `benefit_id` | A `ben_` ID to limit the entry to one [benefit](/docs/concepts/benefits), or `null` for every benefit. |
| `reason` | Why the entry exists, 3 to 500 characters. |
| `expires_at` or `expires_in_days` | An exact expiry time, or 1 to 3,650 days from now. Send one or neither. Without either, the entry never expires. |
| Type | Matches |
|---|---|
| `email` | The email address, including variations that reach the same mailbox. |
| `email_exact` | Only this exact email address. |
| `domain` | Every email address at the domain, such as `example.com`. |
| `ip` | One IP address. |
| `user` | Your user ID, as sent in [`subject.external_id`](/docs/api-reference/evaluations#create-an-evaluation). |
| `device_id` | Your device ID, as sent in `identifiers.device_id`. |
```json title="Block a domain for 30 days"
{
"kind": "block",
"type": "domain",
"value": "example.net",
"benefit_id": null,
"reason": "Repeat trial claims traced to this domain",
"expires_in_days": 30
}
```
```json title="Allow your QA team on one benefit"
{
"kind": "allow",
"type": "email_exact",
"values": ["qa1@example.com", "qa2@example.com", "not-an-email"],
"benefit_id": "ben_8c2f1d64-3a7e-4b19-9f05-2e6d7c1a9b38",
"reason": "Internal QA accounts"
}
```
### Programmatic access [#programmatic-access]
| Interface | Use |
|---|---|
| AbuseZero REST API | [`POST /v1/access-lists`](/docs/api-reference/access-lists#add-an-entry), [`GET /v1/access-lists`](/docs/api-reference/access-lists#list-entries), [`GET /v1/access-lists/{id}`](/docs/api-reference/access-lists#retrieve-an-entry), [`DELETE /v1/access-lists/{id}`](/docs/api-reference/access-lists#remove-an-entry) |
| AbuseZero Node.js SDK | [`addAccessListEntry()`, `addAccessListEntries()`, `accessLists()`, `deleteAccessListEntry()`](/docs/tools/server-sdk#additional-methods) |
| AbuseZero CLI | [`abusezero access-lists add`, `list`, `get`, `delete`](/docs/tools/cli#cli-command-reference) |
| AbuseZero MCP server | [`abusezero_add_access_list_entry`, `abusezero_list_access_lists`, `abusezero_get_access_list_entry`, `abusezero_delete_access_list_entry`](/docs/tools/mcp#mcp-tools) |
| Dashboard | **Policies → Allow & block lists** |
## Response data [#response-data]
A single value returns the saved entry's ID. A batch returns a result for each value:
```json
{
"object": "list",
"data": [
{ "object": "access_list_result", "value": "qa1@example.com", "status": "created", "id": "lst_2b7d9e41-6a3c-4f85-9e17-c0d4a8b52f63" },
{ "object": "access_list_result", "value": "qa2@example.com", "status": "already_listed", "id": "lst_7f1a3c58-2e9b-4d06-b84a-5c9e1d7f2a30" },
{ "object": "access_list_result", "value": "not-an-email", "status": "invalid", "reason": "Use an email such as name@example.com. Quoted and international local parts are not supported." }
]
}
```
`already_listed` means an active entry with the same kind, type, value and benefit already exists. Its ID is returned, and nothing is duplicated.
Each entry looks like this:
```json
{
"object": "access_list_entry",
"id": "lst_2b7d9e41-6a3c-4f85-9e17-c0d4a8b52f63",
"kind": "allow",
"type": "email_exact",
"benefit": "ben_8c2f1d64-3a7e-4b19-9f05-2e6d7c1a9b38",
"reason": "Internal QA accounts",
"expires_at": null,
"created_at": "2026-10-28T11:20:45.000Z"
}
```
The API doesn't return an entry's value, so put the context your team needs in `reason`.
When an entry decides a claim, the evaluation's reason code is [`allow_list` or `block_list`](/docs/reason-codes#your-lists-and-rules).
## Next steps [#next-steps]
Add, read and delete entries.
Decide whole classes of claims with rules.
What each reason means.
# Association graph (/docs/concepts/association-graph)
The association graph shows the earlier claims that one claim connects to through shared identifiers, and which kind of identifier makes each connection. [`GET /v1/evaluations/{id}/graph`](/docs/api-reference/evaluations#retrieve-an-evaluations-association-graph) returns it for any [evaluation](/docs/concepts/evaluations) in your [workspace](/docs/keys-and-access#workspaces).
## Why it matters [#why-it-matters]
A result tells you what happened. The graph shows the context. When you review a claim, allowed or denied, the graph shows every earlier claim it connects to. When you investigate a wave of repeat claims, it shows how far the cluster reaches.
## Key capabilities [#key-capabilities]
- **Connections across every identifier.** Email addresses, user IDs, phone numbers, payment methods, devices, networks and your [custom identifiers](/docs/concepts/custom-identifiers) all form connections.
- **Beyond direct matches.** The graph includes claims connected through other claims, so a chain of accounts that share identifiers in turn appears together.
- **`complete` flag.** Large clusters are capped so the graph returns quickly. When it is capped, the response says so.
- **Scoped to your workspace and benefit.** The graph only ever contains claims from your own workspace, for the same [benefit](/docs/concepts/benefits).
A shared device or network doesn't prove that two accounts belong to one person. Use the graph to
understand a decision and to investigate. Don't treat it as proof of identity.
## Response data [#response-data]
```json
{
"object": "evaluation_links",
"evaluation": "eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93",
"links": [
{ "outcome": "out_6e3b9a12-4c7d-4f58-b0a9-1d2e8c5f7a36", "family": "identity" },
{ "outcome": "out_6e3b9a12-4c7d-4f58-b0a9-1d2e8c5f7a36", "family": "device" },
{ "outcome": "out_0d5c8e27-1b4a-4f93-8c62-a7e9b3d1f045", "family": "benefit_history" }
],
"linked_outcomes": 2,
"complete": true
}
```
| Field | Meaning |
|---|---|
| `links` | One entry for each connection. `outcome` is the connected [outcome](/docs/concepts/outcomes). `family` is the kind of identifier behind the connection: `identity`, `device`, `network` or `benefit_history`. Device and browser identifiers both report `device`. Two outcomes can be connected by more than one family. |
| `linked_outcomes` | How many different outcomes the graph connects to. |
| `complete` | `false` when the cluster was larger than the graph returns. The links shown are then a subset. |
Open any connected outcome with [`GET /v1/outcomes/{id}`](/docs/api-reference/outcomes#retrieve-an-outcome), and its evaluation with [`GET /v1/evaluations/{id}`](/docs/api-reference/evaluations#retrieve-an-evaluation).
## How the graph starts [#how-the-graph-starts]
An allowed claim's graph starts from its outcome and shows everything that outcome connects to. A denied claim has no outcome of its own, so its graph starts from the identifiers its evaluation recorded. That makes the graph the fastest way to see the earlier outcomes behind a [prior-claim](/docs/reason-codes#benefit-history) denial. Read it together with the evaluation's `reason` and `inputs`, and the signal family assessments in [its evidence](/docs/api-reference/evaluations#retrieve-an-evaluations-evidence).
## Configuration [#configuration]
- **The identifiers you send.** Every identifier you send can form connections. [Custom identifiers](/docs/concepts/custom-identifiers) bring in connections only your business can see.
- **Scopes.** Reading the graph needs the [`evaluations:read`](/docs/api-reference/authentication#scopes) scope.
### Programmatic access [#programmatic-access]
| Interface | Use |
|---|---|
| AbuseZero REST API | [`GET /v1/evaluations/{id}/graph`](/docs/api-reference/evaluations#retrieve-an-evaluations-association-graph), [`GET /v1/outcomes/{id}`](/docs/api-reference/outcomes#retrieve-an-outcome) |
| AbuseZero Node.js SDK | [`evaluationGraph()`, `getOutcome()`](/docs/tools/server-sdk#additional-methods) |
| AbuseZero CLI | [`abusezero evaluations graph`, `outcomes get`](/docs/tools/cli#cli-command-reference) |
| AbuseZero MCP server | [`abusezero_get_evaluation_graph`, `abusezero_get_outcome`](/docs/tools/mcp#mcp-tools) |
| Dashboard | **Evaluations**, with **Show linked claims** on an evaluation |
## Next steps [#next-steps]
Everything an evaluation records.
Connect claims with identifiers from your business.
The graph endpoint in full.
# Benefits (/docs/concepts/benefits)
A benefit is anything your product gives a user up front or within a limit: a free or paid introductory trial, a free plan, sign-up credits, a referral bonus or a promotional offer. In AbuseZero, each benefit is a named configuration, and every [evaluation](/docs/concepts/evaluations) runs against exactly one of them.
## Why benefits are separate [#why-benefits-are-separate]
Abuse depends on what is being claimed. A user who already took your trial may still be welcome to claim referral credits. So each benefit keeps its own claim history, its own eligibility window, its own [policy](/docs/concepts/policies-and-rules) and its own [automation settings](/docs/concepts/bots-and-ai-agents). Protection fits what you give away, benefit by benefit, without one benefit's history blocking another.
## Key capabilities [#key-capabilities]
- **Full history on every call.** Every claim is checked against the benefit's complete [claim history](/docs/concepts/outcomes), across every identifier you send, on every evaluation.
- **Exact eligibility windows.** A benefit can be claimed once ever, once per calendar day, week or month in the time zone you choose, or once every set number of days. AbuseZero applies the window to the exact boundary.
- **Simultaneous claims.** Claims that arrive at the same moment and share identifiers are settled one at a time. Two parallel sign-ups can't both pass as someone's first claim.
- **Safe concurrent edits.** Every benefit carries a `revision`. An update made against an older revision is refused with [`BENEFIT_CONFLICT`](/docs/api-reference/errors#conflict), so one teammate's change never silently overwrites another's.
- **Versioned decisions.** Policy changes apply to future evaluations only. Each evaluation records the exact [policy version](/docs/concepts/policies-and-rules#versions) it ran under.
```mermaid
flowchart LR
A[Your app] -->|Claim for a benefit| B[AbuseZero]
B -->|allow or deny| A
B -.->|Benefit history grows with every granted claim| B
```
## Configuration [#configuration]
| Setting | Field | Values |
|---|---|---|
| Name | `name` | Up to 80 characters. Shown in the dashboard. |
| Key | `key` | Lowercase letters, digits and underscores, starting with a letter, up to 64 characters. You send it as `benefit` in each evaluation. It is set when the benefit is created and doesn't change. |
| Protection | `policy_key` | `lenient`, `balanced`, `strict`, or the key of one of your custom policies. See [Protection levels](/docs/concepts/protection-levels). |
| Mode | `protection_mode` | `protect` or `observe`. |
| Eligibility | `config.eligibility` | How often one user can receive the benefit. Set when the benefit is created. To change it, create a new benefit. |
| Delivery confirmation | `config.outcome_tracking` | `assume_granted` or `report_outcome`. See [Outcomes](/docs/concepts/outcomes). |
| If delivery isn't confirmed | `config.missing_outcome_behavior` | `hold_eligibility` or `release_eligibility`. |
| Automated traffic | `config.automation` | `assess`, `deny` or `allow`. See [Bots and AI agents](/docs/concepts/bots-and-ai-agents). |
| Automation check | `config.bot_evidence` | `optional` or `required`. |
| Verified AI agents | `config.allowed_agents` | Agent IDs from [`GET /v1/agents`](/docs/api-reference/agents). |
| Valuation | `config.valuation` | `currency`, `face_value` and `delivery_cost`. Used for reporting only. It never changes an evaluation. |
### Protect and observe [#protect-and-observe]
- **Protect** returns your policy's result.
- **Observe** always returns `allow` and records what the policy would have decided. The evaluation shows that as [`observed_result`](/docs/api-reference/evaluations#the-evaluation-object).
Start a new benefit in [observe mode](/docs/guides/test-changes-safely#run-in-observe-mode) to measure how much repeat claiming it attracts before you act on it. Then switch to protect. Your integration code stays the same.
### Eligibility [#eligibility]
```json title="Once per user, ever"
{ "kind": "once" }
```
```json title="Once per calendar month, in a time zone you choose"
{ "kind": "calendar", "period": "month", "timezone": "Europe/Berlin" }
```
```json title="Once every 30 days"
{ "kind": "rolling", "days": 30 }
```
`period` is `day`, `week` or `month`, and `timezone` is a time zone name such as `Europe/Berlin`. `days` is a whole number from 1 to 365.
### Valuation [#valuation]
Valuation records what a user receives (`face_value`) and what the benefit costs you to deliver (`delivery_cost`), in one three-letter `currency`. Analytics in the dashboard totals these per currency.
### Programmatic access [#programmatic-access]
| Interface | Use |
|---|---|
| AbuseZero REST API | [`POST /v1/benefits`](/docs/api-reference/benefits#create-a-benefit), [`GET /v1/benefits`](/docs/api-reference/benefits#list-benefits), [`GET /v1/benefits/{id}`](/docs/api-reference/benefits#retrieve-a-benefit), [`PATCH /v1/benefits/{id}`](/docs/api-reference/benefits#update-a-benefit), [`DELETE /v1/benefits/{id}`](/docs/api-reference/benefits#delete-a-benefit) |
| AbuseZero Node.js SDK | [`createBenefit()`, `benefits()`, `getBenefit()`, `updateBenefit()`, `deleteBenefit()`](/docs/tools/server-sdk#additional-methods) |
| AbuseZero CLI | [`abusezero benefits create`, `list`, `get`, `update`, `delete`](/docs/tools/cli#cli-command-reference) |
| AbuseZero MCP server | [`abusezero_create_benefit`, `abusezero_list_benefits`, `abusezero_get_benefit`, `abusezero_update_benefit`, `abusezero_delete_benefit`](/docs/tools/mcp#mcp-tools) |
| Dashboard | **Benefits** |
## Response data [#response-data]
```json
{
"object": "benefit",
"id": "ben_8c2f1d64-3a7e-4b19-9f05-2e6d7c1a9b38",
"key": "pro_trial",
"name": "Pro trial",
"policy": "pol_51b9e2a7-0c4d-4f6e-8a13-7d2c9e5f0b14",
"mode": "protect",
"config": {
"eligibility": { "kind": "once" },
"outcome_tracking": "assume_granted",
"missing_outcome_behavior": "hold_eligibility",
"automation": "assess",
"bot_evidence": "optional",
"allowed_agents": [],
"valuation": { "currency": "USD", "face_value": 49, "delivery_cost": 6.5 }
},
"revision": 3,
"created_at": "2026-10-12T09:41:27.000Z"
}
```
Your plan sets how many benefits a workspace can have. When a workspace is over its plan's limits, evaluations for a paused benefit return [`status: "skipped"`](/docs/api-reference/evaluations#skipped-evaluations) with the reason `benefit_paused`. Workspace owners manage plans in **Workspace settings → Billing & Subscriptions**.
## Next steps [#next-steps]
What happens when a user claims a benefit.
Create a benefit and protect your first claim flow.
Create, update and delete benefits.
# Bots and AI agents (/docs/concepts/bots-and-ai-agents)
Not all automation is abuse. AI agents now sign up, start trials and claim credits for real people, and blocking every one of them turns away real customers. AbuseZero tells the two apart on every [evaluation](/docs/concepts/evaluations), in the same call that checks the claim.
Agents that can prove who they are show up as [recognized agents](/docs/api-reference/agents), by name. You decide, benefit by benefit, which ones may claim it. Automation that hides what it is gets no pass. It's assessed alongside identity, device, network and benefit-history signals, and your [policy](/docs/concepts/policies-and-rules) decides the outcome.
All of this runs inside the evaluation's time limit. Your server makes one call and gets one answer: allow or deny.
## Why it matters [#why-it-matters]
Scripted sign-ups are how one person turns a single trial into hundreds. Blanket bot blocking stops them, but it also stops the browser assistants and AI agents your real customers increasingly rely on. AbuseZero Automation Defense lets you be strict with the first and welcoming to the second, per benefit.
## Key capabilities [#key-capabilities]
- **Automation assessed on every evaluation.** Automation is one of the five [signal families](/docs/concepts/risk-score-and-signals#signal-families), assessed together with the others inside the evaluation's time limit.
- **A bot check at the moment of submission.** When your page runs a [bot check](/docs/guides/bot-check) as the person submits, AbuseZero verifies it during the evaluation. A check is valid only for its own claim and for a short time.
- **Verified AI agents.** Your server verifies an incoming agent request with [one SDK call](/docs/tools/server-sdk#verify-ai-agents). AbuseZero then recognizes the agent by name during the evaluation.
- **Allow verified agents.** A recognized agent gets special treatment only on the benefits where you allowed it.
- **Rules apply to agents.** Verified agents you allowed skip the benefit's automation setting, but your rules, [lists](/docs/concepts/allow-and-block-lists) and eligibility apply to them like anyone else. An agent is automation, so a custom rule on [`automation.detected`](/docs/concepts/policies-and-rules#available-signals) matches recognized agents too.
```mermaid
flowchart LR
A[Person or agent] -->|Claims a benefit| B[Your server]
B -->|Evaluation with device token and agent evidence| C[AbuseZero]
C -->|allow or deny| B
```
## Configuration [#configuration]
Each benefit has three automation settings, in the dashboard under **Policies → Automation** or in the benefit's [`config`](/docs/api-reference/benefits#automation):
| Setting | Field | Values |
|---|---|---|
| Automated traffic | `automation` | `assess` (default): automated activity counts toward the AbuseZero Risk Score. `deny`: claims with detected automation are denied. `allow`: detected automation doesn't count against the claim. |
| Automation check | `bot_evidence` | `optional` (default): a bot check is used when the evaluation carries one. `required`: every evaluation for this benefit must carry a completed bot check. |
| Verified AI agents | `allowed_agents` | The recognized agents allowed to claim this benefit. IDs come from [`GET /v1/agents`](/docs/api-reference/agents). |
`deny` can also block legitimate browser assistants. Allow the verified agents you trust on the
same benefit, and start in [observe mode](/docs/concepts/benefits#protect-and-observe) to see the effect first.
### Add a bot check [#add-a-bot-check]
In the browser, call [`prepareBotObservation`](/docs/tools/browser-sdk#preparebotobservation) instead of `observe` when the person submits. Send its `deviceToken` with the evaluation within 2 minutes.
```ts title="In the browser"
import { prepareBotObservation } from "abusezero/browser";
const { deviceToken } = await prepareBotObservation({ key: "pk_…", claimId });
```
When `bot_evidence` is `required` and the evaluation has no usable bot check, the evaluation returns an error instead of a result:
| Error | Status | What to do |
|---|---|---|
| [`BOT_EVIDENCE_REQUIRED`](/docs/api-reference/errors#invalid-request) | 422 | Run `prepareBotObservation` for this claim, then evaluate again. |
| [`BOT_EVIDENCE_UNAVAILABLE`](/docs/api-reference/errors#unavailable) | 503, retryable | Collect a fresh bot check and retry shortly. |
Evaluations that carry evidence from a verified agent you allowed don't need a bot check. See [Add a bot check](/docs/guides/bot-check).
### Recognize verified agents [#recognize-verified-agents]
On your server, verify the incoming request with [`verifyAgentRequest`](/docs/tools/server-sdk#verify-ai-agents), then send the result with the evaluation as `agent_evidence`:
```ts title="On your server"
import { AbuseZero, verifyAgentRequest } from "abusezero/server";
const abusezero = new AbuseZero({ key: process.env.ABUSEZERO_SECRET_KEY!, outage: "throw" });
export async function POST(request: Request) {
const { email, claimId, deviceToken } = await request.clone().json();
const agentEvidence = await verifyAgentRequest(request, claimId, "https://app.example.com");
const evaluation = await abusezero.evaluate(
{
benefit: "pro_trial",
claim_id: claimId,
subject: { email },
identifiers: { device_token: deviceToken },
agent_evidence: agentEvidence,
},
`claim:${claimId}`,
);
// Act on evaluation.result
}
```
- Pass the incoming request, the claim's ID and your site's public origin.
- Send the returned `AgentEvidence` token unchanged. It is opaque and bound to that claim. Evidence for another claim is refused with [`AGENT_EVIDENCE_SCOPE`](/docs/api-reference/errors#invalid-request).
- Verify and evaluate while handling the same request. Agent evidence is short-lived.
- Requests from people return evidence too. Always send it. AbuseZero handles both.
See [Verify AI agents](/docs/guides/verify-ai-agents).
### Programmatic access [#programmatic-access]
| Interface | Use |
|---|---|
| AbuseZero REST API | [`GET /v1/agents`](/docs/api-reference/agents#list-recognized-agents), and [`PATCH /v1/benefits/{id}`](/docs/api-reference/benefits#update-a-benefit) for `config.automation`, `config.bot_evidence` and `config.allowed_agents` |
| AbuseZero Node.js SDK | [`verifyAgentRequest()`](/docs/tools/server-sdk#verify-ai-agents), [`agents()`, `updateBenefit()`](/docs/tools/server-sdk#additional-methods) |
| AbuseZero browser SDK | [`prepareBotObservation()`](/docs/tools/browser-sdk#preparebotobservation) |
| AbuseZero CLI | [`abusezero agents list`, `benefits update`](/docs/tools/cli#cli-command-reference) |
| AbuseZero MCP server | [`abusezero_list_agents`, `abusezero_update_benefit`](/docs/tools/mcp#mcp-tools) |
| Dashboard | **Policies → Automation** |
## Response data [#response-data]
- [The evaluation's evidence](/docs/api-reference/evaluations#retrieve-an-evaluations-evidence) and the dashboard show the `automation` family, with an assessment of `clear`, `elevated`, `high` or `unavailable`.
- When AbuseZero Automation Defense decides a claim, the reason code is [`automation`](/docs/reason-codes#network-intelligence-identity-intelligence-and-automation-defense): "AbuseZero Automation Defense detected automated activity."
- [`GET /v1/agents`](/docs/api-reference/agents#list-recognized-agents) lists the agents AbuseZero recognizes. Each has an `id` for `allowed_agents` and a display `name`.
## Next steps [#next-steps]
Run a bot check when the person submits.
Recognize agents on your server.
How evidence reaches your evaluations.
# Browser protection (/docs/concepts/browser-protection)
Browser protection brings the browser behind each claim into the evaluation. The [AbuseZero browser SDK](/docs/tools/browser-sdk), or the [standalone browser script](/docs/tools/browser-sdk#standalone-browser-script), collects device and automation evidence for the claim and hands your page a **device token**. Your server sends that token with the evaluation, and AbuseZero assesses the evidence there.
## Why it matters [#why-it-matters]
Your server sees an email address and an IP address. It can't see the device or tell whether a real person is at the keyboard. Browser protection gives every evaluation that view, so a second account created on the same device connects to the first, and [scripted sign-ups](/docs/concepts/bots-and-ai-agents) are recognized as scripted.
## Key capabilities [#key-capabilities]
- **Always on.** Browser protection runs on every page that loads the SDK or script. There is nothing to switch on.
- **One device token per claim.** Evidence is collected for a specific [`claimId`](/docs/tools/browser-sdk#observe), travels as one token and is used by that claim's evaluation.
- **Browser data status.** The device token carries the browser data, or the reason there is none, such as declined consent. Every evaluation records exactly what happened in the browser.
- **Your sites.** AbuseZero accepts browser data only from the HTTPS sites in your [site list](/docs/api-reference/browser-collection#update-the-site-list). Data from anywhere else is refused.
- **Script version checks.** Script versions that AbuseZero no longer accepts are refused and reported, so outdated pages never feed stale evidence into your evaluations.
- **Continuous monitoring.** [Browser setup](/docs/api-reference/browser-collection) shows where the script runs, which version each site uses, and what your last 7 days of evaluations received. When a problem you can fix persists, AbuseZero emails your workspace's owners and admins.
- **A consent option.** When your [`consent`](/docs/tools/browser-sdk#consent) callback resolves to `false`, nothing is collected, and the device token carries the reason instead. The evaluation still runs.
```mermaid
flowchart LR
A[Visitor's browser] -->|abusezero browser SDK| B[AbuseZero]
B -->|Device token| A
A -->|Device token| C[Your server]
C -->|Evaluation with device token| B
B -->|allow or deny| C
```
## Integration flow [#integration-flow]
```ts title="In the browser"
import { init, observe } from "abusezero/browser";
init({ key: "pk_…" }); // once, when your app starts
const { deviceToken } = await observe({ key: "pk_…", claimId });
// Send deviceToken to your server with the sign-up request.
```
```ts title="On your server"
const evaluation = await abusezero.evaluate(
{
benefit: "pro_trial",
claim_id: claimId,
subject: { email },
identifiers: { device_token: deviceToken },
},
`claim:${claimId}`,
);
```
- **Always send `deviceToken`,** whatever its status. It carries the browser data, or the reason there is none.
- **Use the same claim ID** in the browser and in the [evaluation](/docs/concepts/evaluations).
- **Consent tools that block the script:** if your consent tool keeps the script from loading at all, send [`unavailable:not_loaded`](/docs/tools/browser-sdk#unavailable-reasons) as the device token.
Evaluations run without browser data too, with less evidence to work with.
[Set up browser protection](/docs/guides/browser-setup) walks through the integration, including the standalone script for sites without a bundler.
## Browser data statuses [#browser-data-statuses]
Each evaluation records what it received from the browser as [`browser_data`](/docs/api-reference/evaluations#browser-data):
| Status | Meaning | You can fix it |
|---|---|---|
| `collected` | Browser data arrived and was used. | |
| `consent_declined` | The visitor declined consent, so nothing was collected. | |
| `not_loaded` | Your consent tool kept the script from loading. | |
| `blocked` | A blocker in the visitor's browser stopped collection. | |
| `timed_out` | Collection didn't finish in time. | |
| `failed` | Collection failed in the browser. | |
| `site_not_listed` | The page's site isn't in your site list. | Yes: add the site in Developers → Browser setup. |
| `sdk_outdated` | The page uses a script version AbuseZero no longer accepts. | Yes: update the SDK or script. |
| `expired` | The token arrived after it expired. | Send the evaluation sooner after collection. |
| `not_passed_on` | The browser collected data, but your server sent the evaluation without the token. | Yes: pass `deviceToken` through as `identifiers.device_token`. |
| `not_sent` | The evaluation carried no browser data, and none was collected for the claim. | Add the SDK to the page where the claim starts. |
Declined consent, blockers and similar losses are expected on any site. They never count as a broken setup.
## Configuration [#configuration]
- **Your site list.** Up to 20 HTTPS origins, in Developers → Browser setup or with [`PATCH /v1/browser-collection`](/docs/api-reference/browser-collection#update-the-site-list).
- **Consent.** Pass a `consent` callback to `observe`. When it resolves to `false`, nothing is collected. Call [`forgetBrowser()`](/docs/tools/browser-sdk#consent) when a visitor withdraws consent.
- **A bot check at submit time.** Call [`prepareBotObservation`](/docs/tools/browser-sdk#preparebotobservation) instead of `observe` when the person submits. See [Bots and AI agents](/docs/concepts/bots-and-ai-agents).
- **Content Security Policy.** If your site sets one, copy the sources shown in Developers → Browser setup into it.
### Programmatic access [#programmatic-access]
| Interface | Use |
|---|---|
| AbuseZero REST API | [`GET /v1/browser-collection`](/docs/api-reference/browser-collection#retrieve-browser-setup), [`PATCH /v1/browser-collection`](/docs/api-reference/browser-collection#update-the-site-list) |
| AbuseZero browser SDK | [`init()`](/docs/tools/browser-sdk#init), [`observe()`](/docs/tools/browser-sdk#observe), [`prepareBotObservation()`](/docs/tools/browser-sdk#preparebotobservation), [`forgetBrowser()`](/docs/tools/browser-sdk#consent), and the [standalone browser script](/docs/tools/browser-sdk#standalone-browser-script) |
| AbuseZero Node.js SDK | [`browserCollection()`, `updateBrowserCollection()`](/docs/tools/server-sdk#additional-methods) |
| AbuseZero CLI | [`abusezero browser-collection get`, `update`](/docs/tools/cli#cli-command-reference) |
| AbuseZero MCP server | [`abusezero_get_browser_collection`, `abusezero_update_browser_collection`](/docs/tools/mcp#mcp-tools) |
| Dashboard | **Developers → Browser setup** |
## Response data [#response-data]
- On each evaluation: `browser_data`, plus the `device` and `browser` [inputs](/docs/concepts/risk-score-and-signals).
- For your whole [workspace](/docs/keys-and-access#workspaces): [`GET /v1/browser-collection`](/docs/api-reference/browser-collection#retrieve-browser-setup) returns your site list, every site where the script has been seen with its version, the current problem if there is one, and a 7-day breakdown of the statuses above.
```json
{
"object": "browser_collection",
"origins": ["https://app.example.com"],
"revision": 2,
"sites": [
{ "origin": "https://app.example.com", "listed": true, "first_seen_at": "2026-10-02T10:14:00.000Z", "last_seen_at": "2026-10-29T08:51:00.000Z", "sdk_version": "0.11.0" }
],
"issue": null,
"issue_since": null,
"coverage": { "days": 7, "total": 1840, "collected": 1712, "consent_declined": 96, "not_loaded": 0, "blocked": 21, "timed_out": 6, "failed": 2, "site_not_listed": 0, "sdk_outdated": 0, "expired": 3, "not_passed_on": 0, "not_sent": 0 },
"notices": []
}
```
## Next steps [#next-steps]
Add the SDK or script to your pages.
Every function and option.
Stop scripted claims and welcome verified agents.
# Custom identifiers (/docs/concepts/custom-identifiers)
Custom identifiers add identifiers from your own business to AbuseZero's [claim history](/docs/concepts/outcomes): a tax ID, a company registration number, a loyalty number, a hardware serial number. Anything that identifies one person or company in your product can become part of every [evaluation](/docs/concepts/evaluations).
## Why it matters [#why-it-matters]
The strongest link between two accounts is often something only your business knows. When a second account presents the same tax ID or the same hardware serial number as an earlier claim, that connection is decisive. Custom identifiers put that knowledge to work on every claim, next to the identifiers AbuseZero already checks.
## Key capabilities [#key-capabilities]
- **Raw values never leave your systems.** You send each value as a keyed hash made with a secret only you hold. AbuseZero matches hashes against your claim history without ever receiving the value behind them.
- **Full history on every call.** Each custom identifier is checked against the benefit's complete claim history on every evaluation, exactly like the built-in identifiers.
- **Conditions in custom rules.** Every registered key becomes a condition you can use in your [policies](/docs/concepts/policies-and-rules#conditions).
- **Key rotation built in.** Each hash names the key version that made it, so you can introduce a new hashing secret without breaking requests in flight.
- **Strict registration.** An evaluation that sends an unregistered key or key version is refused with [`UNSUPPORTED_FIELD`](/docs/api-reference/errors#invalid-request) before anything is recorded.
## Register a key [#register-a-key]
Register each identifier once with [`POST /v1/custom-identifiers`](/docs/api-reference/custom-identifiers#register-a-custom-identifier), or in the dashboard under **Developers → Custom identifiers**:
```json
{
"key": "tax_id",
"key_version": "2026-10",
"description": "Customer tax ID from the billing profile"
}
```
| Field | Values |
|---|---|
| `key` | Lowercase letters, digits and underscores, starting with a letter, up to 64 characters. |
| `key_version` | Your label for the hashing secret, 1 to 128 printable characters with no spaces. |
| `description` | What the identifier is, 3 to 200 characters. |
## Send it with an evaluation [#send-it-with-an-evaluation]
Hash each value on your server with [`createCustomIdentifier`](/docs/tools/server-sdk#hash-phone-numbers-and-custom-identifiers) from `abusezero/server`, and send the result in `identifiers.custom`. One evaluation can carry up to 10 custom identifiers.
```ts title="Node.js"
import { createCustomIdentifier } from "abusezero/server";
const evaluation = await abusezero.evaluate(
{
benefit: "pro_trial",
claim_id: "signup_8f2d41",
subject: { email: "jordan@example.com" },
identifiers: {
custom: [
createCustomIdentifier({
secret: process.env.ABUSEZERO_IDENTIFIER_SECRET!,
keyVersion: "2026-10",
key: "tax_id",
value: taxId.trim().toUpperCase(),
}),
],
},
},
"claim:signup_8f2d41",
);
```
If your server doesn't run Node.js, compute the same hash yourself. [Send identifiers](/docs/guides/send-identifiers) gives the exact format.
- **Normalize before you hash.** Trim spaces and use one letter case, the same way every time. A different input makes a different hash.
- **Keep the secret stable.** Hashes made with different secrets never match. Earlier claims hashed with an old secret don't connect to claims hashed with a new one.
- **Rotate with a new version.** Register the same `key` with a new `key_version`, then switch your hashing to it. Each `key` and `key_version` pair registers once: registering it again returns [`IDENTIFIER_ALREADY_REGISTERED`](/docs/api-reference/errors#conflict).
Phone numbers travel as keyed hashes too, in `identifiers.phone_hash`, made with `createPhoneHash`. See [Send identifiers](/docs/guides/send-identifiers).
## Use it in rules [#use-it-in-rules]
Each registered key adds a condition that counts earlier claims with that identifier:
```json
{ "signal": "custom.tax_id.prior_outcomes", "operator": "gte", "value": 1 }
```
See [Policies and rules](/docs/concepts/policies-and-rules). A custom identifier can't be deleted while a policy uses it.
## Response data [#response-data]
```json
{
"object": "custom_identifier",
"id": "cid_4d8a2f17-9c3e-4b60-a5d1-7e2b9f0c6a84",
"key": "tax_id",
"key_version": "2026-10",
"description": "Customer tax ID from the billing profile",
"created_at": "2026-10-20T13:05:00.000Z",
"updated_at": "2026-10-20T13:05:00.000Z"
}
```
## Programmatic access [#programmatic-access]
| Interface | Use |
|---|---|
| AbuseZero REST API | [`POST /v1/custom-identifiers`](/docs/api-reference/custom-identifiers#register-a-custom-identifier), [`GET /v1/custom-identifiers`](/docs/api-reference/custom-identifiers#list-custom-identifiers), [`GET /v1/custom-identifiers/{id}`](/docs/api-reference/custom-identifiers#retrieve-a-custom-identifier), [`DELETE /v1/custom-identifiers/{id}`](/docs/api-reference/custom-identifiers#remove-a-custom-identifier) |
| AbuseZero Node.js SDK | [`registerCustomIdentifier()`, `customIdentifiers()`, `deleteCustomIdentifier()`](/docs/tools/server-sdk#additional-methods), and [`createCustomIdentifier()`](/docs/tools/server-sdk#hash-phone-numbers-and-custom-identifiers) to hash values |
| AbuseZero CLI | [`abusezero custom-identifiers register`, `list`, `get`, `delete`](/docs/tools/cli#cli-command-reference) |
| AbuseZero MCP server | [`abusezero_register_custom_identifier`, `abusezero_list_custom_identifiers`, `abusezero_get_custom_identifier`, `abusezero_delete_custom_identifier`](/docs/tools/mcp#mcp-tools) |
| Dashboard | **Developers → Custom identifiers** |
## Next steps [#next-steps]
Every identifier you can send, and how.
Register, read and delete keys.
See the earlier claims an evaluation connects to.
# Evaluations (/docs/concepts/evaluations)
An evaluation is one decision on one claim: should this user receive this [benefit](/docs/concepts/benefits), right now? Your server calls [`POST /v1/evaluations`](/docs/api-reference/evaluations) before it grants anything, and AbuseZero returns `allow` or `deny`.
## Why it matters [#why-it-matters]
Everything AbuseZero knows about a claim comes together in this one call: the user's identifiers, the benefit's full claim history, your [policy](/docs/concepts/policies-and-rules), and the device, network and automation assessments. Your code stays simple. It asks one question and acts on one answer. The decision logic lives in AbuseZero, where you can change it without shipping code.
## Key capabilities [#key-capabilities]
- **One final result.** Every successful evaluation returns `allow` or `deny`, within the evaluation's time limit. A technical problem is always an explicit error, never a disguised result.
- **Safe retries.** Every evaluation requires an [`Idempotency-Key`](/docs/api-reference#idempotency) header. A retry with the same key returns the original result instead of creating a second evaluation.
- **Stable claim IDs.** `claim_id` identifies the claim in your system. If a claim was already approved, evaluating it again returns the same approval with the reason [`previously_authorized`](/docs/reason-codes#repeat-requests), instead of counting a second claim.
- **Five signal families.** Each evaluation assesses five [signal families](/docs/concepts/risk-score-and-signals#signal-families) (identity, device, network, automation and benefit history) and checks every identifier against the benefit's full claim history.
- **Evaluation time limit.** If a source of evidence is slow or unavailable, the evaluation finishes on time with what it has. The evaluation reports exactly which [inputs](/docs/concepts/risk-score-and-signals#inputs) it received.
- **Saved before success.** The evaluation, the claim and its idempotency record are saved before AbuseZero returns a successful response.
- **Recorded policy version.** Each evaluation records the [policy version](/docs/concepts/policies-and-rules#versions) it ran under, so you can explain any past decision, even after your rules change.
```mermaid
flowchart LR
A[Your server] -->|POST /v1/evaluations| B[AbuseZero]
B -->|allow or deny, risk, reason| A
A -->|Grant or refuse the benefit| C[Your user]
```
## What you send [#what-you-send]
| Field | Required | What it is |
|---|---|---|
| `benefit` | Yes | The [benefit's key](/docs/concepts/benefits#configuration), such as `pro_trial`. |
| `claim_id` | Yes | Your ID for this claim. 1 to 128 printable characters, no spaces. Use the same value for retries of the same claim. |
| `subject.email` | Yes | The user's email address. |
| `subject.external_id` | No | Your user ID. Recommended. Without it, AbuseZero identifies the user by email address. |
| `subject.email_verified` | No | `true` when your app has verified the email address. |
| `context.ip` | No | The user's public IP address, not your server's. |
| `context.user_agent`, `context.session_id` | No | The browser's user agent and your session ID. |
| `identifiers.device_token` | No | The value the [browser SDK](/docs/concepts/browser-protection) returned. Send it whenever you have it. |
| `identifiers.device_id` | No | Your own device ID, such as a mobile installation ID. |
| `identifiers.browser_cookie_id` | No | Your own first-party browser cookie ID. |
| `identifiers.payment` | No | The payment method, as `provider`, `scope` and `fingerprint` from your payment processor. |
| `identifiers.phone_hash`, `identifiers.phone_verified` | No | A keyed hash of the phone number, made with [`createPhoneHash`](/docs/tools/server-sdk#hash-phone-numbers-and-custom-identifiers), and whether your app verified it. Send both or neither. |
| `identifiers.custom` | No | Up to 10 [custom identifiers](/docs/concepts/custom-identifiers). |
| `agent_evidence` | No | The result of [`verifyAgentRequest`](/docs/concepts/bots-and-ai-agents) for this claim. |
The more identifiers you send, the more history each evaluation can connect. [Send identifiers](/docs/guides/send-identifiers) explains each one.
```ts title="Node.js"
import { AbuseZero } from "abusezero/server";
const abusezero = new AbuseZero({ key: process.env.ABUSEZERO_SECRET_KEY!, outage: "throw" });
const evaluation = await abusezero.evaluate(
{
benefit: "pro_trial",
claim_id: "signup_8f2d41",
subject: { external_id: "user_4821", email: "jordan@example.com", email_verified: true },
context: { ip: "203.0.113.42" },
identifiers: { device_token: deviceToken },
},
"claim:signup_8f2d41",
);
if ("status" in evaluation) {
// Skipped: no result was decided. Handle it as its own branch.
} else if (evaluation.result === "allow") {
// Grant the trial.
}
```
`203.0.113.42` comes from a range reserved for documentation. In real calls, send your visitor's public address: documentation, private and other non-routable addresses return [`IP_NOT_ROUTABLE`](/docs/api-reference/errors#invalid-request).
## Response data [#response-data]
```json title="Allowed"
{
"object": "evaluation",
"id": "eval_9a41c7e2-5b3d-4e8f-a216-0c7d3b9e5f81",
"result": "allow",
"risk": { "score": 10, "band": "low" },
"reason": { "code": "default_allow", "message": "No rule matched, so the policy's default allowed this claim." },
"outcome": { "state": "assumed_granted", "due_at": null }
}
```
```json title="Denied"
{
"object": "evaluation",
"id": "eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93",
"result": "deny",
"risk": { "score": 80, "band": "high" },
"reason": { "code": "risk_threshold", "message": "The AbuseZero Risk Score reached this policy's threshold." },
"outcome": null
}
```
| Field | Meaning |
|---|---|
| `result` | `allow` or `deny`. Act on this field. |
| `risk` | The [AbuseZero Risk Score](/docs/concepts/risk-score-and-signals) and its band. |
| `reason` | A stable `code` and a readable `message`. Every code is listed in [Reason codes](/docs/reason-codes). |
| `outcome` | For allowed claims, the [outcome](/docs/concepts/outcomes) AbuseZero is tracking, and when it's due. |
### Skipped evaluations [#skipped-evaluations]
When a [workspace](/docs/keys-and-access#workspaces) can't run an evaluation, the response has no `result`:
```json
{ "object": "evaluation", "status": "skipped", "reason": "insufficient_credits" }
```
No decision was made, so handle a skip as its own branch in your flow. The reason is `insufficient_credits`, `billing_overdue`, `service_inactive` or `benefit_paused`. New reasons can be added within `v1`, so treat an unfamiliar reason like any other skip.
## Find an evaluation [#find-an-evaluation]
[`GET /v1/evaluations/{id}`](/docs/api-reference/evaluations#retrieve-an-evaluation) returns the full record:
```json
{
"object": "evaluation",
"id": "eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93",
"benefit": "pro_trial",
"claim_id": "signup_8f2d41",
"result": "deny",
"reason": { "code": "risk_threshold", "message": "The AbuseZero Risk Score reached this policy's threshold." },
"outcome": { "state": null, "due_at": null },
"policy_version": 4,
"created_at": "2026-10-28T16:02:11.000Z",
"mode": "protect",
"risk": { "score": 80, "band": "high" },
"inputs": { "email": "received", "ip": "received", "user_id": "received", "phone": "missing", "payment": "missing", "device": "received", "browser": "received" },
"browser_data": "collected",
"policy": { "id": "pol_51b9e2a7-0c4d-4f6e-8a13-7d2c9e5f0b14", "version": 4 }
}
```
- `inputs` shows what the evaluation received from you.
- `mode` is `protect` or `observe`. In observe mode, `result` is always `allow`, and `observed_result` shows what the policy decided.
- [`GET /v1/evaluations/{id}/graph`](/docs/concepts/association-graph) shows the earlier claims this one connects to.
- [`GET /v1/evaluations/{id}/evidence`](/docs/api-reference/evaluations#retrieve-an-evaluations-evidence) returns the request exactly as AbuseZero received it, with the assessment for each [signal family](/docs/concepts/risk-score-and-signals#signal-families). It needs the `evaluation_evidence:read` [scope](/docs/api-reference/authentication#scopes).
Evaluations, previews and Test console runs all use credits from your workspace. Workspace owners
see balances and plans in **Workspace settings → Billing & Subscriptions**.
## Programmatic access [#programmatic-access]
| Interface | Use |
|---|---|
| AbuseZero REST API | [`POST /v1/evaluations`](/docs/api-reference/evaluations#create-an-evaluation), [`POST /v1/evaluation-previews`](/docs/api-reference/previews-simulations-replays#preview-an-evaluation), [`GET /v1/evaluations`](/docs/api-reference/evaluations#list-evaluations), [`GET /v1/evaluations/{id}`](/docs/api-reference/evaluations#retrieve-an-evaluation), [`GET /v1/evaluations/{id}/evidence`](/docs/api-reference/evaluations#retrieve-an-evaluations-evidence) |
| AbuseZero Node.js SDK | [`evaluate()`](/docs/tools/server-sdk#evaluate-a-claim), [`previewEvaluation()`](/docs/tools/server-sdk#preview-an-evaluation), [`evaluations()`, `getEvaluation()`, `evaluationEvidence()`](/docs/tools/server-sdk#additional-methods) |
| AbuseZero CLI | [`abusezero evaluations create`, `preview`, `list`, `get`, `evidence`](/docs/tools/cli#cli-command-reference) |
| AbuseZero MCP server | [`abusezero_create_evaluation`, `abusezero_preview_evaluation`, `abusezero_list_evaluations`, `abusezero_get_evaluation`, `abusezero_get_evaluation_evidence`](/docs/tools/mcp#mcp-tools) |
| Dashboard | **Evaluations**, and **Developers → Test console** for single test requests |
## Next steps [#next-steps]
What the score and signal families tell you.
Close the loop after you grant a benefit.
Every field, response and error.
# Outcomes (/docs/concepts/outcomes)
An outcome records what happened after AbuseZero allowed a claim: did the user actually receive the benefit? Outcomes are the claim history that every later [evaluation](/docs/concepts/evaluations) is checked against.
## Why it matters [#why-it-matters]
An allowed claim isn't always a delivered benefit. A card can fail after the trial starts, or you may refund a purchase and take the credits back. Outcomes keep AbuseZero's history in step with what really happened. A user whose claim fell through can try again, and a [benefit](/docs/concepts/benefits) you actually delivered keeps counting.
## Key capabilities [#key-capabilities]
- **Recorded outcomes.** Every granted outcome becomes history that later evaluations check, across every identifier the claim carried.
- **Two tracking models.** AbuseZero can treat every allowed claim as delivered, or wait for your app to confirm delivery.
- **Missing outcome behavior.** When your app is expected to confirm delivery but doesn't, AbuseZero settles the outcome by the rule you chose for that benefit.
- **Outcome corrections.** Only valid changes are accepted, such as granted to reversed. Conflicting reports are refused with [`OUTCOME_CONFLICT`](/docs/api-reference/errors#conflict), and every change is recorded in your [audit log](/docs/api-reference/audit-logs).
- **Outcome conflicts.** Under `release_eligibility`, when a delivery is confirmed after its deadline and other claims have since used the same identifiers, the response lists those overlapping outcomes in `conflicts` so you can review them.
```mermaid
flowchart LR
A[Your server] -->|Evaluate the claim| B[AbuseZero]
B -->|allow| A
A -->|Report the outcome| B
B -.->|The outcome becomes history for future evaluations| B
```
## Tracking models [#tracking-models]
Each benefit chooses one in [`config.outcome_tracking`](/docs/api-reference/benefits#benefit-configuration).
| Model | How it works | Choose it when |
|---|---|---|
| `assume_granted` (default) | Every allowed claim counts as delivered straight away, with the state `assumed_granted`. You report only corrections. | Your app grants the benefit as soon as AbuseZero allows it. |
| `report_outcome` | An allowed claim starts as `awaiting_outcome`, with a `due_at` time returned in the evaluation. Your app reports `granted` or `not_granted` before then. | Delivery can still fail after the evaluation, for example at a payment step. |
When a report is due but doesn't arrive, the outcome becomes `outcome_unknown`. The benefit's `config.missing_outcome_behavior` then decides whether it counts:
- `hold_eligibility` (default): the claim still counts as an earlier claim.
- `release_eligibility`: the claim doesn't count, and the user can claim again.
## Outcome states [#outcome-states]
| State | Meaning | Counts as an earlier claim |
|---|---|---|
| `assumed_granted` | Allowed under `assume_granted`. | Yes |
| `awaiting_outcome` | Allowed under `report_outcome`, waiting for your report. | Yes |
| `granted` | Your app confirmed the user received the benefit. | Yes |
| `not_granted` | Your app reported that the user didn't receive it. | No |
| `reversed` | The benefit was delivered, then taken back. | Yes |
| `outcome_unknown` | The report window closed without a report. | Depends on `missing_outcome_behavior` |
## Reporting an outcome [#reporting-an-outcome]
Report against the evaluation that allowed the claim, with [`POST /v1/evaluations/{id}/outcome`](/docs/api-reference/outcomes#report-an-outcome):
```json title="Request"
{ "outcome": "not_granted", "reason": "Card declined at checkout", "external_outcome_id": "order_20481" }
```
```json title="Response"
{
"object": "outcome_report",
"evaluation": "eval_9a41c7e2-5b3d-4e8f-a216-0c7d3b9e5f81",
"state": "not_granted",
"conflicts": []
}
```
| Field | Meaning |
|---|---|
| `outcome` | `granted`, `not_granted` or `reversed`. |
| `reason` | 3 to 500 characters. Required for `not_granted` and `reversed`. |
| `external_outcome_id` | Optional. Your own ID for this outcome, unique within your workspace. |
| You report | Accepted from |
|---|---|
| `granted` | `awaiting_outcome`, `outcome_unknown`, `assumed_granted` |
| `not_granted` | `awaiting_outcome`, `outcome_unknown`, `assumed_granted` |
| `reversed` | `granted`, `assumed_granted` |
Reporting the state an outcome already has is safe and changes nothing. Every report needs an [`Idempotency-Key`](/docs/api-reference#idempotency), so a retry never applies twice.
## Outcomes and eligibility [#outcomes-and-eligibility]
`claim_id` ties evaluations and outcomes to one claim in your system:
- **Already allowed:** evaluating the same claim again returns `allow` with the reason [`previously_authorized`](/docs/reason-codes#repeat-requests). It doesn't count a second claim.
- **Reported `not_granted`:** the claim is evaluated afresh.
- **Report overdue:** under `report_outcome`, a claim whose report window closed without a report returns [`OUTCOME_CONFLICT`](/docs/api-reference/errors#conflict) when evaluated again. Report its outcome first.
## Response data [#response-data]
[`GET /v1/outcomes`](/docs/api-reference/outcomes#list-outcomes) lists outcomes, and each one looks like this:
```json
{
"object": "outcome",
"id": "out_6e3b9a12-4c7d-4f58-b0a9-1d2e8c5f7a36",
"evaluation": "eval_9a41c7e2-5b3d-4e8f-a216-0c7d3b9e5f81",
"state": "granted",
"source": "evaluation",
"occurred_at": "2026-10-28T16:02:11.000Z",
"due_at": "2026-10-28T16:12:11.000Z"
}
```
`source` is `evaluation` for outcomes created by an evaluation, and `import` for outcomes you [imported](/docs/guides/import-claim-history).
## Programmatic access [#programmatic-access]
| Interface | Use |
|---|---|
| AbuseZero REST API | [`POST /v1/evaluations/{id}/outcome`](/docs/api-reference/outcomes#report-an-outcome), [`GET /v1/outcomes`](/docs/api-reference/outcomes#list-outcomes), [`GET /v1/outcomes/{id}`](/docs/api-reference/outcomes#retrieve-an-outcome), and [`POST /v1/imports`](/docs/api-reference/imports#import-outcomes-as-json) for outcome history |
| AbuseZero Node.js SDK | [`outcome()`, `importOutcomes()`](/docs/tools/server-sdk#report-an-outcome), [`outcomes()`, `getOutcome()`](/docs/tools/server-sdk#additional-methods) |
| AbuseZero CLI | [`abusezero outcomes report`, `list`, `get`](/docs/tools/cli#cli-command-reference) |
| AbuseZero MCP server | [`abusezero_report_outcome`, `abusezero_list_outcomes`, `abusezero_get_outcome`](/docs/tools/mcp#mcp-tools) |
| Dashboard | **Evaluations**, and **Developers → Data import** for outcome history |
## Next steps [#next-steps]
Wire outcome reporting into your delivery flow.
Start with every benefit you've already granted.
Every field, response and error.
# Policies and rules (/docs/concepts/policies-and-rules)
A policy decides `allow` or `deny` for every claim of every [benefit](/docs/concepts/benefits) that uses it. [Protection levels](/docs/concepts/protection-levels) are policies AbuseZero manages. Custom policies are yours: an optional protection level to build on, your own rules, a risk threshold and a default result.
## Why it matters [#why-it-matters]
Every product has its own idea of a fair claim. You may want to approve every user whose email and phone your app verified, or deny claims from networks you never serve. Custom policies let you write that down once, in AbuseZero, and change it at any time without touching your code.
## Key capabilities [#key-capabilities]
- **Validation on save.** Unsupported conditions, values of the wrong type and out-of-range numbers are rejected when you save, so a broken rule never reaches a live [evaluation](/docs/concepts/evaluations).
- **Immutable versions.** Each change creates a new policy version. Every evaluation records the version it ran under, and the full history stays readable.
- **Consistent conditions.** Conditions mean exactly the same thing in live evaluations, [previews, simulations and replays](/docs/api-reference/previews-simulations-replays).
- **Shared policies.** A change applies to every benefit that uses the policy, for future evaluations.
- **Safe concurrent edits.** Updates carry a `revision`. An update against an older revision is refused with [`POLICY_CONFLICT`](/docs/api-reference/errors#conflict).
## Rule order [#rule-order]
A policy reads from top to bottom:
1. **Your rules,** in the order you list them. The first rule that matches decides the claim.
2. **The [protection level](/docs/concepts/protection-levels)** you build on, if any, for the claims your rules don't decide.
3. **The risk threshold.** A claim whose [AbuseZero Risk Score](/docs/concepts/risk-score-and-signals) reaches it is denied.
4. **The default result** for everything else.
Allow and block list entries decide a matching claim outright, whatever the policy says. So does a benefit whose automation setting is `deny`, for claims with detected automation. See [Allow and block lists](/docs/concepts/allow-and-block-lists) and [Bots and AI agents](/docs/concepts/bots-and-ai-agents).
```json title="A custom policy definition"
{
"base": "balanced",
"risk_threshold": 75,
"default_result": "allow",
"rules": [
{
"id": "allow_verified_first_claims",
"name": "Verified users with no earlier claims",
"result": "allow",
"when": {
"all": [
{ "signal": "email.verified", "operator": "eq", "value": true },
{ "signal": "phone.verified", "operator": "eq", "value": true },
{ "signal": "user.prior_claims", "operator": "eq", "value": 0 }
]
}
},
{
"id": "deny_disposable_on_anonymous_network",
"name": "Disposable email on an anonymous network",
"result": "deny",
"when": {
"all": [
{ "signal": "email.disposable", "operator": "eq", "value": true },
{ "signal": "network.anonymous", "operator": "eq", "value": true }
]
}
}
]
}
```
| Field | Required | Meaning |
|---|---|---|
| `base` | No | `lenient`, `balanced` or `strict`. The protection level this policy builds on. |
| `risk_threshold` | No | A multiple of 5 from 5 to 100; any other value returns [`INVALID_FIELD`](/docs/api-reference/errors#invalid-request). It compares against the AbuseZero Risk Score, which also moves in steps of 5. When you leave it out or send `null`, the base level's threshold applies, or AbuseZero's standard threshold if there is no base. Policy versions show it as `null` unless you set it yourself. |
| `default_result` | Yes | `allow` or `deny`, for claims nothing else decides. |
| `rules` | Yes | Up to 50 rules. Send an empty array for a policy with no rules of its own. |
## Rules [#rules]
| Field | Meaning |
|---|---|
| `id` | Your ID for the rule, up to 64 characters, unique within the policy. |
| `name` | A name your team will recognize, up to 100 characters. It appears in the reason when you read the evaluation. |
| `result` | `allow` or `deny`. |
| `when` | One condition, or a group: `{ "all": [...] }` matches when every entry matches, `{ "any": [...] }` when at least one does. |
A group holds 1 to 10 entries. Groups can contain groups, up to two levels deep. A rule can have up to 10 conditions in total.
When one of your rules decides a claim, the evaluation's reason has the code [`custom_rule`](/docs/reason-codes#your-lists-and-rules) and the rule's `id` as `rule`. When you read the evaluation, the message names the rule:
```json
"reason": { "code": "custom_rule", "message": "Your rule “Disposable email on an anonymous network” matched this claim.", "rule": "deny_disposable_on_anonymous_network" }
```
## Conditions [#conditions]
Each condition compares one signal with a value:
```json
{ "signal": "network.risk", "operator": "eq", "value": "high" }
```
| Operator | Matches when |
|---|---|
| `eq`, `neq` | The signal equals, or doesn't equal, the value. |
| `gt`, `gte`, `lt`, `lte` | The signal is greater than, at least, less than or at most the value. Numeric signals only. |
| `is_missing` | You didn't send the input this signal needs. Takes no value. |
| `is_unknown` | The input was sent, but its value couldn't be determined. Takes no value. |
| `is_unavailable` | The assessment wasn't available for this evaluation. Takes no value. |
A comparison never matches when the signal has no known value. To act on a missing or unavailable signal, add a condition with `is_missing`, `is_unknown` or `is_unavailable`.
### Available signals [#available-signals]
| Signal | Type | Meaning |
|---|---|---|
| `email.verified` | boolean | Your app verified the email address. |
| `email.disposable` | boolean | The email address is disposable. |
| `phone.verified` | boolean | Your app verified the phone number. |
| `user.prior_claims` | whole number | Earlier claims by this user. |
| `email.prior_claims` | whole number | Earlier claims with this email address. |
| `device.prior_claims` | whole number | Earlier claims from this device. |
| `payment.prior_claims` | whole number | Earlier claims with this payment method. |
| `phone.prior_claims` | whole number | Earlier claims with this phone number. |
| `network.risk` | `low`, `elevated` or `high` | The network risk AbuseZero Network Intelligence assessed. |
| `network.anonymous` | boolean | The connection comes through an anonymous network. |
| `network.repeat_attempts` | whole number | Recent claim attempts from this network. |
| `network.country` | ISO country code | The country of the network, such as `DE`. |
| `automation.detected` | boolean | AbuseZero Automation Defense detected automated activity. |
| `custom..prior_outcomes` | whole number | Earlier claims with one of your [custom identifiers](/docs/concepts/custom-identifiers). |
[`GET /v1/policy-signals`](/docs/api-reference/policies#list-conditions) returns the built-in signals with their labels, types and allowed values, so your tools can build rule editors from it.
## Versions [#versions]
Each save creates a new version, and the policy's `active_version` moves to it. [`GET /v1/policies/{id}/versions`](/docs/api-reference/policies#list-policy-versions) lists every version, and [`GET /v1/policies/{id}/versions/{version}`](/docs/api-reference/policies#retrieve-a-policy-version) returns your definition as it was in that version. When an evaluation surprises you, open it, note its `policy.version`, and read that version.
## Configuration [#configuration]
- **Create, update, duplicate and delete** custom policies in the dashboard or through the [Policies API](/docs/api-reference/policies). A policy can't be deleted while a benefit uses it. Reassign those benefits first.
- **Test before you save.** Preview a claim, simulate a sequence of claims, or replay recent traffic against a new policy. See [Test changes safely](/docs/guides/test-changes-safely).
- **Attach the policy** by setting a benefit's [`policy_key`](/docs/api-reference/benefits#update-a-benefit) to the policy's key.
### Programmatic access [#programmatic-access]
| Interface | Use |
|---|---|
| AbuseZero REST API | [`POST /v1/policies`](/docs/api-reference/policies#create-a-policy), [`GET /v1/policies`](/docs/api-reference/policies#list-policies), [`GET /v1/policies/{id}`](/docs/api-reference/policies#retrieve-a-policy), [`PATCH /v1/policies/{id}`](/docs/api-reference/policies#update-a-policy), [`POST /v1/policies/{id}/duplicate`](/docs/api-reference/policies#duplicate-a-policy), [`DELETE /v1/policies/{id}`](/docs/api-reference/policies#delete-a-policy), [`GET /v1/policies/{id}/versions`](/docs/api-reference/policies#list-policy-versions), [`GET /v1/policy-signals`](/docs/api-reference/policies#list-conditions) |
| AbuseZero Node.js SDK | [`createPolicy()`, `policies()`, `getPolicy()`, `updatePolicy()`, `duplicatePolicy()`, `deletePolicy()`](/docs/tools/server-sdk#additional-methods); versions and conditions through [`call()`](/docs/tools/server-sdk#call-for-any-endpoint) |
| AbuseZero CLI | [`abusezero policies create`, `list`, `get`, `update`, `duplicate`, `delete`, `versions`, `version`, `signals`](/docs/tools/cli#cli-command-reference) |
| AbuseZero MCP server | [`abusezero_create_policy`, `abusezero_list_policies`, `abusezero_get_policy`, `abusezero_update_policy`, `abusezero_duplicate_policy`, `abusezero_delete_policy`, `abusezero_list_policy_versions`, `abusezero_get_policy_version`, `abusezero_list_policy_signals`](/docs/tools/mcp#mcp-tools) |
| Dashboard | **Policies** |
## Next steps [#next-steps]
See which results change before you publish.
Create and version policies programmatically.
What each reason means.
# Protection levels (/docs/concepts/protection-levels)
Protection levels are complete policies that AbuseZero defines and maintains: **Lenient**, **Balanced** and **Strict**. Every workspace has all three. Point a [benefit](/docs/concepts/benefits) at one and it is protected, with no rules to write.
## Why it matters [#why-it-matters]
Writing a good abuse policy from scratch takes data most teams don't have yet. Protection levels give every benefit sound protection from its first claim. You choose the level. AbuseZero handles what it contains.
## Key capabilities [#key-capabilities]
- **Complete policies.** Each level is a full policy, built on AbuseZero's assessment of every [evaluation](/docs/concepts/evaluations) and its own risk threshold. Point a benefit at it and nothing else is needed.
- **Managed for you.** AbuseZero defines and maintains each level. Levels can't be edited or deleted, so no one in your workspace can weaken one by accident.
- **Custom rules on a protection level.** A [custom policy](/docs/concepts/policies-and-rules) can build on any level. Your rules decide the claims they match, and the level handles everything else.
- **Versioned like any policy.** Every evaluation records the policy and version it ran under, including when that policy is a protection level.
## Choosing a level [#choosing-a-level]
| Level | Key | Posture | Choose it when |
|---|---|---|---|
| Lenient | `lenient` | Favors conversion. | A benefit costs little to deliver, and a smooth claim matters most. |
| Balanced | `balanced` | Suits most products. | You're starting out, or you want a dependable default. |
| Strict | `strict` | Favors protection. | Each claim carries a real cost, such as generous credits or a trial with paid usage. |
To see how a level treats your traffic before it affects anyone, set the benefit's [mode](/docs/concepts/benefits#protect-and-observe) to
`observe`. Every claim is allowed, and each evaluation records what the level would have decided.
Switch to `protect` when the results look right.
## Configuration [#configuration]
- **Which level a benefit uses.** Set the benefit's [`policy_key`](/docs/api-reference/benefits#update-a-benefit) to `lenient`, `balanced` or `strict`. The change applies to future evaluations.
- **Your own rules on top.** Create a custom policy with `"base": "balanced"` (or another level) and add rules for your product. See [Policies and rules](/docs/concepts/policies-and-rules).
- **Duplicate policy.** Duplicating a protection level creates a custom policy built on that level, ready for your rules.
- **Test against past traffic.** Replay your recent evaluations against another level and see every result that would change before any user does. See [Test changes safely](/docs/guides/test-changes-safely).
### Programmatic access [#programmatic-access]
| Interface | Use |
|---|---|
| AbuseZero REST API | [`GET /v1/policies`](/docs/api-reference/policies#list-policies) lists the levels with `kind` set to `managed`; [`PATCH /v1/benefits/{id}`](/docs/api-reference/benefits#update-a-benefit) sets a benefit's `policy_key`; [`POST /v1/policies/{id}/duplicate`](/docs/api-reference/policies#duplicate-a-policy); [`POST /v1/replays`](/docs/api-reference/previews-simulations-replays#run-a-replay) |
| AbuseZero Node.js SDK | [`policies()`, `updateBenefit()`, `duplicatePolicy()`, `replay()`](/docs/tools/server-sdk#additional-methods) |
| AbuseZero CLI | [`abusezero policies list`, `benefits update`, `policies duplicate`, `simulations replay`](/docs/tools/cli#cli-command-reference) |
| AbuseZero MCP server | [`abusezero_list_policies`, `abusezero_update_benefit`, `abusezero_duplicate_policy`, `abusezero_replay_evaluations`](/docs/tools/mcp#mcp-tools) |
| Dashboard | **Policies**, and **Test against past traffic** |
## Response data [#response-data]
Protection levels appear in [`GET /v1/policies`](/docs/api-reference/policies#list-policies) alongside your custom policies, with `kind` set to `managed`:
```json
{
"object": "policy",
"id": "pol_51b9e2a7-0c4d-4f6e-8a13-7d2c9e5f0b14",
"key": "balanced",
"name": "Balanced",
"kind": "managed",
"active_version": 1,
"created_at": "2026-10-01T08:00:00.000Z",
"updated_at": "2026-10-01T08:00:00.000Z"
}
```
A protection level's version identifies the level without listing what it contains:
```json
{ "object": "policy_version", "version": 1, "managed": true, "level": "balanced", "created_at": "2026-10-01T08:00:00.000Z" }
```
When a protection level decides a claim, the reason code names the kind of evidence, such as [`prior_claim_email`](/docs/reason-codes#benefit-history) or [`network_risk`](/docs/reason-codes#network-intelligence-identity-intelligence-and-automation-defense). See [Reason codes](/docs/reason-codes).
## Next steps [#next-steps]
Add your own rules on top of a level.
Compare levels against your real traffic.
Point a benefit at a level.
# Risk Score and signals (/docs/concepts/risk-score-and-signals)
Every [evaluation](/docs/concepts/evaluations) reports what AbuseZero assessed at three levels of detail:
- the **AbuseZero Risk Score**, one number and a band;
- **signal families**, one assessment for each area AbuseZero examines;
- **inputs**, which of your identifiers the evaluation received.
## Why it matters [#why-it-matters]
The result tells your code what to do. These fields tell your team why, and show you where your integration can send more. Use them to explain a decision to a colleague, to set a risk threshold in your own [policy](/docs/concepts/policies-and-rules), and to spot an identifier your server stopped sending.
## Key capabilities [#key-capabilities]
- **The AbuseZero AI Risk Engine.** The score comes from the [AbuseZero AI Risk Engine](/docs/ai-risk-engine).
- **Five signal families on every call.** Identity, device, network, automation and benefit history are assessed together, in real time, inside the evaluation's time limit.
- **Your full claim history.** Every identifier is checked against everything your workspace has recorded for the benefit, not a recent sample.
- **`unavailable` assessments.** When a family has nothing to work with, it reports `unavailable` instead of guessing. When a source of evidence is slow, the evaluation finishes on time with what it has.
- **Previews, simulations and replays.** [Previews](/docs/api-reference/previews-simulations-replays#preview-an-evaluation) run the full evaluation. [Simulations](/docs/api-reference/previews-simulations-replays#run-a-simulation) and [replays](/docs/api-reference/previews-simulations-replays#run-a-replay) apply your policy with the same engine as live evaluations.
## The AbuseZero Risk Score [#the-abusezero-risk-score]
```json
"risk": { "score": 80, "band": "high" }
```
| Field | Values | Meaning |
|---|---|---|
| `score` | Number from 0 to 100 in steps of 5, or `null` | How strongly the evidence points to a repeat or abusive claim. Higher means riskier. |
| `band` | `low`, `elevated`, `high` or `not_assessed` | The score's band, for dashboards and quick decisions. |
`not_assessed` means no score was needed or available for this evaluation. The result still stands.
The score feeds your policy through its risk threshold. When nothing else in your policy has decided a claim, a score that reaches the threshold denies it, with the reason [`risk_threshold`](/docs/reason-codes#network-intelligence-identity-intelligence-and-automation-defense). See [Policies and rules](/docs/concepts/policies-and-rules).
## Signal families [#signal-families]
Each family is assessed separately. The assessments are reported in the `signals` array of [an evaluation's evidence](/docs/api-reference/evaluations#retrieve-an-evaluations-evidence), which needs the `evaluation_evidence:read` scope, and on the evaluation in the dashboard.
| Family | What it covers |
|---|---|
| `identity` | The email address and identity details you send, assessed by AbuseZero Identity Intelligence. |
| `device` | The device and browser behind the claim, and how they connect to earlier claims. |
| `network` | The connection behind the claim, assessed by AbuseZero Network Intelligence. |
| `automation` | Bots and scripted activity, assessed by AbuseZero Automation Defense. |
| `benefit_history` | Earlier claims of this benefit that share this claim's identifiers. |
| Assessment | Meaning |
|---|---|
| `clear` | Nothing in this family counts against the claim. |
| `elevated` | Some evidence in this family counts against the claim. |
| `high` | Strong evidence in this family counts against the claim. |
| `unavailable` | This family had nothing to assess, usually because an input was missing. |
```json
"signals": [
{ "family": "identity", "assessment": "clear" },
{ "family": "device", "assessment": "elevated" },
{ "family": "network", "assessment": "clear" },
{ "family": "automation", "assessment": "unavailable" },
{ "family": "benefit_history", "assessment": "high" }
]
```
Evaluation details and previews don't include signal family assessments. The response to `POST /v1/evaluations` stays small: it carries the result, the score and the reason.
## Inputs [#inputs]
`inputs` reports each identifier type as `received` or `missing`. It is the fastest way to confirm that your integration sends what you think it sends.
| Input | Comes from |
|---|---|
| `email` | [`subject.email`](/docs/api-reference/evaluations#create-an-evaluation) |
| `ip` | `context.ip` |
| `user_id` | `subject.external_id`. `missing` when you don't send one, although the email address still identifies the user. |
| `phone` | `identifiers.phone_hash` |
| `payment` | `identifiers.payment` |
| `device` | `identifiers.device_id`, or device data from the browser SDK |
| `browser` | `identifiers.browser_cookie_id`, or browser data from the browser SDK |
```json
"inputs": { "email": "received", "ip": "received", "user_id": "received", "phone": "missing", "payment": "missing", "device": "received", "browser": "received" }
```
A `missing` input means that family had less to work with. It never causes an error. [Send identifiers](/docs/guides/send-identifiers) shows how to send each one.
## Configuration [#configuration]
- **Which identifiers you send.** Each additional identifier gives every later evaluation more history to connect.
- **How much the score counts.** Your policy's risk threshold decides at which score claims are denied. Your [protection level](/docs/concepts/protection-levels) sets a sensible default.
- **How automation counts.** Each benefit's automation setting decides whether automated activity is scored, denied or allowed. See [Bots and AI agents](/docs/concepts/bots-and-ai-agents).
### Programmatic access [#programmatic-access]
| Interface | Use |
|---|---|
| AbuseZero REST API | [`GET /v1/evaluations/{id}`](/docs/api-reference/evaluations#retrieve-an-evaluation) and [`POST /v1/evaluation-previews`](/docs/api-reference/previews-simulations-replays#preview-an-evaluation) return `risk` and `inputs`; [`GET /v1/evaluations/{id}/evidence`](/docs/api-reference/evaluations#retrieve-an-evaluations-evidence) adds `signals` |
| AbuseZero Node.js SDK | [`evaluate()`](/docs/tools/server-sdk#evaluate-a-claim), [`previewEvaluation()`](/docs/tools/server-sdk#preview-an-evaluation), [`getEvaluation()`, `evaluationEvidence()`](/docs/tools/server-sdk#additional-methods) |
| AbuseZero CLI | [`abusezero evaluations get`, `preview`, `evidence`](/docs/tools/cli#cli-command-reference) |
| AbuseZero MCP server | [`abusezero_get_evaluation`, `abusezero_preview_evaluation`, `abusezero_get_evaluation_evidence`](/docs/tools/mcp#mcp-tools) |
| Dashboard | **Evaluations** |
## Next steps [#next-steps]
Set your own risk threshold and conditions.
Every reason an evaluation can return.
Fix missing inputs and browser data.
# Developer FAQ (/docs/developer-faq)
## Product [#product]
An [evaluation](/docs/concepts/evaluations) decides one claim on one [benefit](/docs/concepts/benefits): should this claim receive the benefit? It returns a final `result` of `allow` or `deny`, a `reason` with a stable code and a readable message, and the [AbuseZero Risk Score](/docs/concepts/risk-score-and-signals) with its band. See [the evaluation object](/docs/api-reference/evaluations#the-evaluation-object).
Two other answers are always explicit, never disguised as a result:
- When the workspace can't run an evaluation, the response is a [skipped evaluation](/docs/api-reference/evaluations#skipped-evaluations), with `"status": "skipped"`, a reason and no `result`.
- When a request fails, the response is an error in the standard [error envelope](/docs/api-reference/errors#error-envelope).
Your server calls [`POST /v1/evaluations`](/docs/api-reference/evaluations#create-an-evaluation) at the moment a user claims a benefit, such as starting a trial or redeeming credits, and before your application grants it. Your application acts on the result, then [reports the outcome](/docs/concepts/outcomes) when you choose to track delivery.
Your application keeps everything else: sign-in, sign-up verification, payments, delivering the benefit and enforcing usage limits. AbuseZero answers one question at the moment of the claim. See [How AbuseZero works](/docs/how-it-works).
- **[Benefit](/docs/concepts/benefits):** something you give out a limited number of times, such as a free trial or sign-up credits. You define it once, with a `key`, a policy, eligibility and outcome tracking.
- **Claim:** one attempt by one user to receive a benefit. Your system identifies it with a stable `claim_id`.
- **[Evaluation](/docs/concepts/evaluations):** AbuseZero's decision on a claim, with an `eval_` ID.
- **[Outcome](/docs/concepts/outcomes):** what happened after an `allow`: the benefit was granted, wasn't granted, or was reversed. Recorded outcomes are the history that later evaluations are checked against.
AI agents that can prove who they are appear as [Verified AI agents](/docs/concepts/bots-and-ai-agents#recognize-verified-agents), recognized by name. You decide, benefit by benefit, which ones may claim it: list their IDs in the benefit's `allowed_agents` setting. Read the IDs from [`GET /v1/agents`](/docs/api-reference/agents#list-recognized-agents).
On your server, pass the incoming request to [`verifyAgentRequest`](/docs/tools/server-sdk#verify-ai-agents) and send the opaque token it returns with the evaluation as `agent_evidence`. AbuseZero verifies it during the evaluation. Automation that hides what it is gets no such pass; each benefit's automation setting decides how it counts. See [Verify AI agents](/docs/guides/verify-ai-agents).
## Integration [#integration]
The minimum is the benefit key (`benefit`), a `claim_id` and the user's email address in `subject.email`.
Everything else you add gives AbuseZero more to check the claim against:
- your own user ID in `subject.external_id`, and whether your app verified the email;
- the user's public IP address in `context.ip`;
- the device token from the [AbuseZero browser SDK](/docs/tools/browser-sdk) in `identifiers.device_token`;
- a payment method fingerprint, a hashed and verified phone number, and your own [custom identifiers](/docs/concepts/custom-identifiers).
Each evaluation's [`inputs`](/docs/api-reference/evaluations#inputs) field reports which of these it received. See [Send identifiers](/docs/guides/send-identifiers).
- **Your server, sending evaluations:** a [restricted key](/docs/keys-and-access#secret-and-restricted-keys) (`rk_`) with the `evaluations:create` scope, plus any other scopes it needs.
- **Your server, managing the workspace:** a restricted key with the scopes it needs, or a secret key (`sk_`) with full access.
- **Web pages:** the [publishable key](/docs/keys-and-access#publishable-key) (`pk_`), through the browser SDK or the standalone browser script.
- **The [AbuseZero CLI](/docs/tools/cli) and the [AbuseZero MCP server](/docs/tools/mcp):** [OAuth](/docs/keys-and-access#oauth) sign-in.
- **Scripts and CI using the AbuseZero CLI:** a [machine client](/docs/keys-and-access#machine-clients).
Give each key only the [scopes](/docs/api-reference/authentication#scopes) it needs. See [Keys and access](/docs/keys-and-access#choosing-a-credential).
Send evaluations from your server. Secret and restricted keys must never reach a browser.
The browser has one job: collect browser data with the [AbuseZero browser SDK](/docs/tools/browser-sdk) and the publishable key, then hand the device token to your server. A publishable key sent to a server endpoint is refused with `KEY_TYPE_NOT_ALLOWED` (see [REST API errors](/docs/api-reference/errors#permission)). See [Set up browser protection](/docs/guides/browser-setup).
Use one stable ID for each award in your system, such as the trial's or the redemption's ID. It is 1 to 128 printable characters with no spaces.
- Evaluating a claim that was already approved returns the same approval, with the reason `previously_authorized`, instead of counting a second claim.
- A `claim_id` that already belongs to a different user is refused with `CLAIM_CONFLICT`.
- A `claim_id` from an earlier eligibility period is refused with `CLAIM_PERIOD_ENDED`.
See [Re-evaluating a `claim_id`](/docs/api-reference/evaluations#re-evaluating-a-claim_id).
- **Node.js 22 or later:** the [AbuseZero Node.js SDK](/docs/tools/server-sdk), `abusezero/server`.
- **Web pages:** the [AbuseZero browser SDK](/docs/tools/browser-sdk), through your bundler or as the standalone browser script.
- **Any other server language:** the [AbuseZero REST API](/docs/api-reference) over HTTPS with JSON. To send phone numbers and custom identifiers, compute the hashes yourself as described in [Hash identifiers in other languages](/docs/guides/send-identifiers#hash-identifiers-in-other-languages).
- **Terminals, scripts and CI:** the [AbuseZero CLI](/docs/tools/cli).
- **AI apps:** the [AbuseZero MCP server](/docs/tools/mcp).
All of them reach the same actions. See [REST API, MCP server and CLI coverage](/docs/tools/coverage).
Evaluations run without it. The [AbuseZero browser SDK](/docs/tools/browser-sdk) adds device and automation evidence that your server can't collect on its own, so every evaluation has more to work with.
It is required when a benefit's `bot_evidence` setting is `required`: the evaluation must then carry a completed [bot check](/docs/guides/bot-check), or it is refused with `BOT_EVIDENCE_REQUIRED`. See [Browser protection](/docs/concepts/browser-protection).
Send them as keyed hashes that you compute with your own secret. AbuseZero matches the hashes and never receives the raw values.
- **Node.js:** use [`createPhoneHash` and `createCustomIdentifier`](/docs/tools/server-sdk#hash-phone-numbers-and-custom-identifiers) from the AbuseZero Node.js SDK.
- **Other languages:** follow [Hash identifiers in other languages](/docs/guides/send-identifiers#hash-identifiers-in-other-languages).
Each hash names the `key_version` that made it, so you can rotate your secret by registering a new version. A custom identifier must be [registered](/docs/api-reference/custom-identifiers#register-a-custom-identifier) before you send it, or the evaluation is refused with `UNSUPPORTED_FIELD`. See [Custom identifiers](/docs/concepts/custom-identifiers).
Yes. Pass a [`consent`](/docs/tools/browser-sdk#consent) callback to `observe` or `prepareBotObservation`. When it resolves to `false`, nothing is collected and the device token carries the reason instead. The evaluation still runs.
- If your consent tool keeps the script from loading at all, send `unavailable:not_loaded` as the device token.
- When a visitor withdraws consent, call `forgetBrowser()`.
Whether and how you ask for consent is your decision.
Each benefit's `outcome_tracking` setting decides how outcomes are recorded. See [tracking models](/docs/concepts/outcomes#tracking-models).
- **`assume_granted`:** AbuseZero treats every allowed claim as delivered. You report only corrections, such as a reversal.
- **`report_outcome`:** an allowed claim starts as `awaiting_outcome`, with a `due_at` time returned in the evaluation. Report `granted` or `not_granted` with [`POST /v1/evaluations/{id}/outcome`](/docs/api-reference/outcomes#report-an-outcome), or [`outcome()`](/docs/tools/server-sdk#report-an-outcome) in the AbuseZero Node.js SDK. If no outcome arrives in time, the benefit's `missing_outcome_behavior` decides.
See [Report outcomes](/docs/guides/report-outcomes) and [outcome states](/docs/api-reference/outcomes#outcome-states).
Use the [data deletions](/docs/api-reference/data-deletions) endpoints with the `data_deletions:write` scope:
1. [Preview the deletion](/docs/api-reference/data-deletions#preview-a-deletion) with a `selector` naming the user. The preview shows what would be removed and changes nothing.
2. [Request the deletion](/docs/api-reference/data-deletions#request-a-deletion) with the same selector.
3. [Check its status](/docs/api-reference/data-deletions#retrieve-a-deletion) until it is complete.
A deletion is permanent. See [Export and delete data](/docs/guides/export-and-delete-data#delete-a-users-history).
## Testing [#testing]
- **Observe mode:** set the benefit's `mode` to `observe`. Every claim is allowed, and each evaluation records what AbuseZero would have decided. See [Protect and observe](/docs/concepts/benefits#protect-and-observe).
- **Test console:** try individual requests against a benefit's policy in the dashboard.
- **Previews:** run a full evaluation without recording a claim, with [`POST /v1/evaluation-previews`](/docs/api-reference/previews-simulations-replays#preview-an-evaluation).
- **Test against past traffic:** run your recent evaluations against another policy with a [replay](/docs/api-reference/previews-simulations-replays#run-a-replay).
- **Simulations:** run a sequence of retained claims against a policy, step by step, with a [simulation](/docs/api-reference/previews-simulations-replays#run-a-simulation).
When the results look right, switch the benefit to protect mode. See [Test changes safely](/docs/guides/test-changes-safely).
`context.ip` must be a public address. Private, loopback and documentation addresses, such as `203.0.113.42` from the samples in these docs, are refused with `IP_NOT_ROUTABLE`.
Send the user's real public IP address, as your trusted proxy chain reports it, or leave `context.ip` out while you test. See [Send the IP address](/docs/guides/send-identifiers#send-the-ip-address) and [Troubleshooting](/docs/troubleshooting#ip_not_routable-422).
## Debugging [#debugging]
Start with the evaluation's [`reason`](/docs/api-reference/evaluations#reason): a stable code and a readable message. [Reason codes](/docs/reason-codes) lists every code and what to do about it.
For the full picture, [retrieve the evaluation](/docs/api-reference/evaluations#retrieve-an-evaluation). It shows:
- the name of your rule, when one of your custom rules decided it;
- the policy and policy version it ran under;
- the inputs it received.
The assessment for each signal family is in [the evaluation's evidence](/docs/api-reference/evaluations#retrieve-an-evaluations-evidence), which needs the `evaluation_evidence:read` scope, and in the dashboard.
The [association graph](/docs/api-reference/evaluations#retrieve-an-evaluations-association-graph) shows the earlier claims linked to it.
If the user should receive the benefit:
- add an [allow list](/docs/concepts/allow-and-block-lists) entry;
- choose a [protection level](/docs/concepts/protection-levels) that suits your audience;
- or adjust your [custom rules](/docs/concepts/policies-and-rules), testing the change first.
See [Unexpected denials](/docs/troubleshooting#unexpected-denials).
- **Request ID:** every response carries an [`X-Request-ID`](/docs/api-reference#request-ids) header, and every error repeats it as `request_id`. Log it with your own records.
- **Request logs:** [request logs](/docs/api-reference/request-logs#list-request-logs) list your evaluation requests, including failed ones, with their status and error `code`. Read them through the REST API or with `abusezero request-logs list` in the [AbuseZero CLI](/docs/tools/cli#cli-command-reference). Requests that never authenticated and rate-limited requests aren't logged.
- **Evaluation ID:** open the evaluation by its `eval_` ID, in the dashboard or with [`GET /v1/evaluations/{id}`](/docs/api-reference/evaluations#retrieve-an-evaluation).
- **Configuration changes:** the dashboard's Audit trail, and [audit logs](/docs/api-reference/audit-logs) in the REST API, show who changed your workspace's configuration and when.
See [Monitor your integration](/docs/guides/monitor-your-integration).
`inputs.device` is `missing` when the evaluation carried no device data. Check that your server passes the browser SDK's `deviceToken` to the evaluation as `identifiers.device_token`, unchanged, for the same `claim_id`.
The evaluation's [`browser_data`](/docs/api-reference/evaluations#browser-data) status tells you what happened in the browser:
- **You can fix these:**
- `not_passed_on`: the token never reached the evaluation.
- `site_not_listed`: add the site under **Your sites** in Browser setup.
- `sdk_outdated`: update the browser SDK.
- `expired`: send the token sooner after the SDK returns it.
- **Expected losses, nothing to fix:** `consent_declined`, `not_loaded`, `blocked`, `timed_out` and `failed`.
See [Browser data statuses](/docs/concepts/browser-protection#browser-data-statuses) and [Troubleshooting](/docs/troubleshooting#evaluations-show-inputsdevice-missing-or-browser_data-not_sent).
A skipped evaluation means the workspace couldn't run it, so no `allow` or `deny` was decided. The response has `"status": "skipped"`, a reason and no `result`.
- `insufficient_credits`: the workspace doesn't have enough credits.
- `billing_overdue`: the workspace doesn't have enough credits while a payment is overdue.
- `service_inactive`: the workspace can't run evaluations, for example while it's being deleted.
- `benefit_paused`: the benefit is paused because the workspace is over its plan's limits.
Handle a skip as its own branch in your flow, decided in advance, and treat a reason you don't recognize like any other skip. The AbuseZero Node.js SDK's `outage` setting doesn't apply to skips. See [Skipped evaluations](/docs/api-reference/evaluations#skipped-evaluations) and [Handle skipped evaluations](/docs/guides/outages-and-retries#handle-skipped-evaluations).
The AbuseZero Node.js SDK's [`outage` option](/docs/tools/server-sdk#the-outage-option) decides what `evaluate()` returns:
- `"throw"` raises an `AbuseZeroError` for your code to handle.
- `"allow"` and `"deny"` return that result locally, marked with `source: "local_outage"`, so you can reconcile the claim later.
Some errors are always raised, whatever the setting: rate limits, bot check errors and errors that aren't retryable. A skipped evaluation is never turned into a local result. See [Handle outages and retries](/docs/guides/outages-and-retries#choose-your-outage-behavior).
## REST API [#rest-api]
Yes, with the same [`Idempotency-Key`](/docs/api-reference#idempotency). Every write requires one: 1 to 128 printable characters with no spaces.
- The same key and the same body return the original response instead of repeating the write.
- The same key with a different body is refused with `IDEMPOTENCY_CONFLICT`.
- If the first attempt is still running, the retry returns `EVALUATION_PENDING`. Retry again with the same key.
Store the key with the claim, and reuse it for every retry of that request. See [Reuse the idempotency key on every retry](/docs/guides/outages-and-retries#reuse-the-idempotency-key-on-every-retry).
A request over a [rate limit](/docs/api-reference#rate-limits) returns `429` with the code `RATE_LIMITED` and a `Retry-After` header in seconds. Previews and Test console runs have their own limit, separate from evaluations, so testing never uses up the capacity your live traffic needs.
Wait that long, then retry with the same `Idempotency-Key`. The AbuseZero Node.js SDK never turns a rate limit into a local `allow` or `deny`, so traffic spikes can't switch protection off. See [REST API errors](/docs/api-reference/errors#rate-limit).
Every path starts with the version, `/v1`. Within `v1`, AbuseZero can add new values: for example new reason codes, skip reasons, failure reasons or optional fields.
Write your code to handle values it doesn't recognize instead of failing on them. For example, treat an unknown skip reason like any other skip. See [REST API versioning](/docs/api-reference#rest-api-versioning).
- **IDs:** yes. Every ID carries a type prefix, such as `eval_`, `ben_`, `pol_` or `out_`. Store them and send them back exactly as received. See [Object IDs](/docs/api-reference#object-ids).
- **Page cursors:** use them only to fetch the next page. A cursor is sealed to the list and workspace it came from, so pass it back unchanged. A changed cursor, or one from another list, is refused with `CURSOR_INVALID`. See [Pagination](/docs/api-reference#pagination).
## Credits [#credits]
- **Use credits:**
- [evaluations](/docs/api-reference/evaluations#create-an-evaluation), including evaluations on benefits in observe mode;
- [previews](/docs/api-reference/previews-simulations-replays#preview-an-evaluation);
- Test console runs in the dashboard.
- **Use no credits:** [Test against past traffic](/docs/api-reference/previews-simulations-replays#run-a-replay) and [simulations](/docs/api-reference/previews-simulations-replays#run-a-simulation). Both reuse evaluations your workspace already retained.
When a workspace doesn't have enough credits, evaluations are [skipped](/docs/api-reference/evaluations#skipped-evaluations) rather than failing. The workspace owner manages credits in **Workspace settings → Billing & Subscriptions**.
Use [Test against past traffic](/docs/guides/test-changes-safely#test-against-past-traffic) and [simulations](/docs/guides/test-changes-safely#simulate-the-change-in-sequence). They apply a policy to evaluations your workspace already retained, so they use no credits and change nothing.
Previews and Test console runs are real evaluations and use credits. Use them when you need a fresh evaluation of a specific request. See [Test changes safely](/docs/guides/test-changes-safely).
# Glossary (/docs/glossary)
## A [#a]
**AbuseZero AI Risk Engine.** The AI at the heart of AbuseZero, working with a proprietary algorithm built on 140,000+ risk signal combinations. See [AbuseZero AI Risk Engine](/docs/ai-risk-engine).
**AbuseZero Automation Defense.** AbuseZero's assessment of automated activity behind a claim. It is reported as the `automation` signal family, and as the `automation` reason code when it decides a result. See [Bots and AI agents](/docs/concepts/bots-and-ai-agents) and [signals](/docs/api-reference/evaluations#signals).
**AbuseZero Identity Intelligence.** AbuseZero's assessment of the identity behind a claim, starting with its email address. It is reported as the `identity` signal family, and as the `identity_risk` reason code when it decides a result. See [Signal families](/docs/concepts/risk-score-and-signals#signal-families) and [Reason codes](/docs/reason-codes#network-intelligence-identity-intelligence-and-automation-defense).
**AbuseZero Network Intelligence.** AbuseZero's assessment of the connection behind a claim. It is reported as the `network` signal family, and as the `network_risk` reason code when it decides a result. See [Signal families](/docs/concepts/risk-score-and-signals#signal-families) and [Reason codes](/docs/reason-codes#network-intelligence-identity-intelligence-and-automation-defense).
**AbuseZero Risk Score.** A number from 0 to 100 that sums up an evaluation's overall risk, returned as `risk.score` with a band in `risk.band`. See [Risk Score and signals](/docs/concepts/risk-score-and-signals) and [risk](/docs/api-reference/evaluations#risk).
**Allow list.** Entries that always allow a matching claim, by email, exact email, domain, IP address, user or device. They can apply to every benefit or to one. See [Allow and block lists](/docs/concepts/allow-and-block-lists) and the [Allow and block lists API](/docs/api-reference/access-lists).
**Assessment.** The level a signal family reached in one evaluation: `clear`, `elevated`, `high` or `unavailable`. See [Signal families](/docs/concepts/risk-score-and-signals#signal-families) and [signals](/docs/api-reference/evaluations#signals).
**Association graph.** The earlier outcomes linked to an evaluation through shared identifiers, each connection labeled with a signal family. See [Association graph](/docs/concepts/association-graph) and [Retrieve an evaluation's association graph](/docs/api-reference/evaluations#retrieve-an-evaluations-association-graph).
**Audit log.** A record of changes made in a workspace, by members or by AbuseZero. IDs start with `aud_`. The dashboard shows it as the Audit trail. See the [Audit logs API](/docs/api-reference/audit-logs).
## B [#b]
**Benefit.** Anything you give out on a limited basis, such as a free trial, free plan, sign-up credits or a promotional offer. Each benefit has a key, such as `pro_trial`, and an ID starting with `ben_`. See [Benefits](/docs/concepts/benefits) and the [Benefits API](/docs/api-reference/benefits).
**Benefit history.** The claims and outcomes already recorded for a benefit in your workspace. Every evaluation is checked against it. It is also one of the five signal families. See [Outcomes](/docs/concepts/outcomes) and [Reason codes](/docs/reason-codes#benefit-history).
**Block list.** Entries that always deny a matching claim. Same entry types as the allow list. See [Allow and block lists](/docs/concepts/allow-and-block-lists) and [entry types](/docs/api-reference/access-lists#entry-types).
**Bot check.** An extra browser check collected when the person submits, with `prepareBotObservation`. A benefit can require it. See [Add a bot check](/docs/guides/bot-check) and [`prepareBotObservation`](/docs/tools/browser-sdk#preparebotobservation).
**Browser data.** What the browser SDK collected for a claim, carried to the evaluation by the device token. Its status appears on the evaluation as `browser_data`. See [Browser data statuses](/docs/concepts/browser-protection#browser-data-statuses) and [browser data](/docs/api-reference/evaluations#browser-data).
**Browser protection.** AbuseZero's device and automation intelligence in your pages, through the browser SDK or the standalone script. See [Browser protection](/docs/concepts/browser-protection) and the [AbuseZero browser SDK](/docs/tools/browser-sdk).
## C [#c]
**Claim.** One request for a benefit by one user. See [Evaluations](/docs/concepts/evaluations).
**Claim ID.** Your identifier for one award, sent as `claim_id`. Evaluating the same claim ID again refers to the same award. See [Re-evaluating a `claim_id`](/docs/api-reference/evaluations#re-evaluating-a-claim_id).
**Condition.** A check used in a custom rule, such as `email.disposable` or `network.risk`. [`GET /v1/policy-signals`](/docs/api-reference/policies#list-conditions) lists the built-in conditions, and each registered custom identifier adds one more. See [Conditions](/docs/concepts/policies-and-rules#conditions) and [List conditions](/docs/api-reference/policies#list-conditions).
**Credits.** What evaluations, previews and Test console runs use. Simulations and replays use none. Managed by the workspace owner in **Workspace settings → Billing & Subscriptions**. See [Credits](/docs/developer-faq#credits) in the Developer FAQ.
**Custom identifier.** An identifier from your own system, such as an account number, registered with a key and key version and sent as a keyed hash. See [Custom identifiers](/docs/concepts/custom-identifiers) and the [Custom identifiers API](/docs/api-reference/custom-identifiers).
**Custom rule.** A rule you write with conditions, combined with a protection level in a custom policy. See [Policies and rules](/docs/concepts/policies-and-rules) and [Rules](/docs/api-reference/policies#rules).
## D [#d]
**Device token.** The value the browser SDK returns for a claim. Send it unchanged as `identifiers.device_token`. When there is no browser data, it carries the reason instead. See [Browser protection](/docs/concepts/browser-protection) and [`observe`](/docs/tools/browser-sdk#observe).
## E [#e]
**Eligibility.** How often a user can receive a benefit: once, once per calendar day, week or month, or once per rolling number of days. See [Benefits](/docs/concepts/benefits#eligibility) and the [eligibility setting](/docs/api-reference/benefits#eligibility).
**Evaluation.** One check of one claim. It returns `allow` or `deny` with a risk score and a reason. IDs start with `eval_`. See [Evaluations](/docs/concepts/evaluations) and the [Evaluations API](/docs/api-reference/evaluations).
**Evidence.** The request an evaluation received, retained for review. Reading it needs the `evaluation_evidence:read` scope. See [Retrieve an evaluation's evidence](/docs/api-reference/evaluations#retrieve-an-evaluations-evidence).
## I [#i]
**Idempotency key.** A value you send in the `Idempotency-Key` header of every write. Retrying with the same key repeats the request safely instead of creating a second one. See [Idempotency](/docs/api-reference#idempotency).
**Inputs.** The identifier types an evaluation received, each reported as `received` or `missing`: `email`, `ip`, `user_id`, `phone`, `payment`, `device` and `browser`. See [Inputs](/docs/concepts/risk-score-and-signals#inputs) and [inputs](/docs/api-reference/evaluations#inputs).
## M [#m]
**Machine client.** OAuth client credentials for one workspace, used by the AbuseZero CLI in scripts and CI. See [Machine clients](/docs/keys-and-access#machine-clients).
## O [#o]
**Observe mode.** A benefit mode in which every claim is allowed, while AbuseZero records what it would have decided as `observed_result`. See [Protect and observe](/docs/concepts/benefits#protect-and-observe) and [protection mode](/docs/api-reference/benefits#protection-mode).
**Outcome.** What happened after an evaluation: granted, not granted or reversed. IDs start with `out_`. See [Outcomes](/docs/concepts/outcomes) and the [Outcomes API](/docs/api-reference/outcomes).
**Outcome state.** Where a claim's outcome stands: `assumed_granted`, `awaiting_outcome`, `granted`, `not_granted`, `outcome_unknown` or `reversed`. See [Outcome states](/docs/concepts/outcomes#outcome-states) and [outcome states](/docs/api-reference/outcomes#outcome-states).
**Outcome tracking.** A benefit setting. Either allowed claims count as granted automatically, or you report each outcome yourself. See [Tracking models](/docs/concepts/outcomes#tracking-models) and [Report outcomes](/docs/guides/report-outcomes).
## P [#p]
**Policy.** The rules that decide a benefit's results: a protection level, or a custom policy built on one. IDs start with `pol_`. See [Policies and rules](/docs/concepts/policies-and-rules) and the [Policies API](/docs/api-reference/policies).
**Policy version.** A saved version of a policy. Every evaluation records the version it ran under. See [Versions](/docs/concepts/policies-and-rules#versions) and [List policy versions](/docs/api-reference/policies#list-policy-versions).
**Preview.** An evaluation of a request that returns the full result without recording a claim. It is billed like any evaluation. See [Preview an evaluation](/docs/api-reference/previews-simulations-replays#preview-an-evaluation).
**Protect mode.** A benefit mode in which results are applied. See [Protect and observe](/docs/concepts/benefits#protect-and-observe).
**Protection level.** A policy that AbuseZero defines and maintains: Lenient, Balanced or Strict. See [Protection levels](/docs/concepts/protection-levels) and [protection levels](/docs/api-reference/policies#protection-levels) in the Policies API.
**Publishable key.** A key starting with `pk_`, used by the browser SDK. It can only send browser data, so it is safe in page source. See [Publishable key](/docs/keys-and-access#publishable-key).
## R [#r]
**Reason.** Why an evaluation returned its result: a stable `code` and a readable `message`. See [Reason codes](/docs/reason-codes) and [reason](/docs/api-reference/evaluations#reason).
**Recognized agent.** An AI agent that AbuseZero can identify by name. Each benefit decides which recognized agents may claim it. See [Bots and AI agents](/docs/concepts/bots-and-ai-agents) and [List recognized agents](/docs/api-reference/agents#list-recognized-agents).
**Replay.** A benefit's recent evaluations run against another policy, showing which results would change. See [Run a replay](/docs/api-reference/previews-simulations-replays#run-a-replay).
**Request log.** A record of one evaluation request: its HTTP status and, when it failed, its error code. IDs start with `req_`, followed by the request's `X-Request-ID`. See the [Request logs API](/docs/api-reference/request-logs).
**Restricted key.** A server key starting with `rk_`, with only the scopes you choose. See [Secret and restricted keys](/docs/keys-and-access#secret-and-restricted-keys).
**Risk band.** The band of the AbuseZero Risk Score: `low`, `elevated`, `high`, or `not_assessed` when no score was produced. See [risk](/docs/api-reference/evaluations#risk).
## S [#s]
**Scope.** A permission a key or token carries, such as `evaluations:create`. See [Scopes](/docs/api-reference/authentication#scopes).
**Secret key.** A server key starting with `sk_`, with full access to its workspace. See [Secret and restricted keys](/docs/keys-and-access#secret-and-restricted-keys).
**Signal family.** One of the five areas AbuseZero assesses in every evaluation: identity, device, network, automation and benefit history. See [Signal families](/docs/concepts/risk-score-and-signals#signal-families) and [signals](/docs/api-reference/evaluations#signals).
**Simulation.** A benefit's recent claims run again, in order, against a policy you choose, showing the result each step would get. See [Run a simulation](/docs/api-reference/previews-simulations-replays#run-a-simulation).
**Site list.** The HTTPS sites allowed to send browser data for your workspace, managed in **Developers → Browser setup**. It appears as **Your sites** in Browser setup. See [Browser protection](/docs/concepts/browser-protection) and [Update the site list](/docs/api-reference/browser-collection#update-the-site-list).
**Skipped evaluation.** An evaluation the workspace couldn't run, returned with `"status": "skipped"` and a reason, and no result. See [Skipped evaluations](/docs/api-reference/evaluations#skipped-evaluations).
**Subject.** The user making the claim: your user ID in `external_id`, their email address, and whether your app verified it. See [Create an evaluation](/docs/api-reference/evaluations#create-an-evaluation).
## T [#t]
**Test console.** A tool in **Developers** that runs a request through AbuseZero and shows the result. Nothing is stored and no claim is used. Each run is billed. See [Test changes safely](/docs/guides/test-changes-safely).
**Test against past traffic.** The dashboard's replay tool, under **Policies**. See [Run a replay](/docs/api-reference/previews-simulations-replays#run-a-replay).
## W [#w]
**Workspace.** The container for everything in AbuseZero: benefits, policies, evaluations, keys and billing. IDs start with `ws_`. See [Workspaces](/docs/keys-and-access#workspaces) and the [Workspaces API](/docs/api-reference/workspaces).
# Add a bot check (/docs/guides/bot-check)
The bot check gives [AbuseZero Automation Defense](/docs/concepts/bots-and-ai-agents) a verification taken at the moment the person submits a claim. It runs in the browser, and AbuseZero verifies it inside the evaluation that follows. You then decide, [benefit](/docs/concepts/benefits) by benefit, how detected automation is treated. For the bigger picture, see [Bots and AI agents](/docs/concepts/bots-and-ai-agents).
## Before you begin [#before-you-begin]
- [Browser protection](/docs/guides/browser-setup) is set up, and your server sends `identifiers.device_token` with every evaluation.
- A server key with the [scopes](/docs/api-reference/authentication#scopes) `benefits:read`, `benefits:write`, `policies:read`, `evaluations:create` and `evaluations:read`.
## Run the bot check on submit [step] [#run-the-bot-check-on-submit-step]
Call [`prepareBotObservation`](/docs/tools/browser-sdk#preparebotobservation) instead of [`observe`](/docs/tools/browser-sdk#observe) in your submit handler. It takes the same options and returns the same `deviceToken`. Wait for it to finish before you send the form.
```ts title="app/trial-form.ts"
import { prepareBotObservation } from "abusezero/browser";
export async function submitTrial(claimId: string, email: string) {
const { deviceToken } = await prepareBotObservation({ key: "pk_…", claimId });
await fetch("/api/trial", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ claimId, email, deviceToken }),
});
}
```
Call it on submit, not when the page loads. The result is tied to this claim and this moment.
## Evaluate within 2 minutes [step] [#evaluate-within-2-minutes-step]
Send the `deviceToken` to AbuseZero as `identifiers.device_token` within 2 minutes, in [`evaluate()`](/docs/tools/server-sdk#evaluate-a-claim) or [`POST /v1/evaluations`](/docs/api-reference/evaluations#create-an-evaluation). Your server code doesn't change.
- A token sent later than that can't be verified. When the benefit requires a bot check, the evaluation returns [`BOT_EVIDENCE_REQUIRED`](/docs/troubleshooting#bot_evidence_required-422). Run the bot check again and resubmit.
- Each token belongs to one claim. Reusing it for another claim returns `DEVICE_TOKEN_USED` ([conflict](/docs/api-reference/errors#conflict)).
## Choose how the benefit treats automation [step] [#choose-how-the-benefit-treats-automation-step]
Two benefit settings control automation. Change them on the **Benefits** page of the dashboard, with [`PATCH /v1/benefits/{id}`](/docs/api-reference/benefits#update-a-benefit) in the AbuseZero REST API, with `abusezero benefits update` in the [AbuseZero CLI](/docs/tools/cli#cli-command-reference), or with `abusezero_update_benefit` on the [AbuseZero MCP server](/docs/tools/mcp#mcp-tools). Both are described in the [Automation](/docs/api-reference/benefits#automation) settings reference.
| Setting | Value | Effect |
|---|---|---|
| `automation` | `assess` (default) | Detected automation counts toward the [AbuseZero Risk Score](/docs/concepts/risk-score-and-signals). |
| | `deny` | Detected automation is denied, with the [reason](/docs/reason-codes) `automation`. |
| | `allow` | Detected automation doesn't count against the claim. |
| `bot_evidence` | `optional` (default) | Evaluations run with or without a bot check. |
| | `required` | An evaluation without a usable bot check returns an error instead of a result. |
Recognized AI agents that you allow on the benefit are exempt from both settings. See [Verify AI agents](/docs/guides/verify-ai-agents).
A benefit update sends the benefit's full settings with its current `revision`. This helper reads the benefit and its [policy](/docs/concepts/policies-and-rules) with the SDK's [additional methods](/docs/tools/server-sdk#additional-methods), applies your changes and saves it:
```ts title="lib/update-benefit-config.ts"
import { abusezero } from "./abusezero";
type Benefit = {
name: string;
policy: string;
mode: "protect" | "observe";
revision: number;
config: Record;
};
export async function updateBenefitConfig(benefitId: string, changes: Record) {
const benefit = await abusezero.getBenefit(benefitId) as Benefit;
const policy = await abusezero.getPolicy(benefit.policy) as { key: string };
const config = Object.fromEntries(Object.entries(benefit.config).filter(([, value]) => value !== null));
return abusezero.updateBenefit(benefitId, {
name: benefit.name,
policy_key: policy.key,
protection_mode: benefit.mode,
config: { ...config, ...changes },
revision: benefit.revision,
}, `benefit:${benefitId}:revision:${benefit.revision}`);
}
```
```ts
await updateBenefitConfig("ben_6f1d2c3b-4a5e-4f60-8b7c-9d0e1f2a3b4c", {
automation: "deny",
bot_evidence: "required",
});
```
If someone else saved the benefit since you read it, the update returns `BENEFIT_CONFLICT` ([conflict](/docs/api-reference/errors#conflict)). Read it again and retry.
## Handle bot check errors [step] [#handle-bot-check-errors-step]
When `bot_evidence` is `required`, two errors mean the evaluation didn't run:
| Code | Status | What to do |
|---|---|---|
| `BOT_EVIDENCE_REQUIRED` | 422 | The claim had no usable bot check. Run `prepareBotObservation` again and resubmit with the new token. |
| `BOT_EVIDENCE_UNAVAILABLE` | 503 | The bot check couldn't be verified this time. The error is retryable: collect fresh evidence and retry shortly. See [unavailable errors](/docs/api-reference/errors#unavailable). |
The [AbuseZero Node.js SDK](/docs/tools/server-sdk#nodejs-sdk-errors) always throws these errors, even when [`outage`](/docs/tools/server-sdk#the-outage-option) is `"allow"` or `"deny"`. A missing bot check never becomes a local decision.
```ts title="lib/evaluate-with-bot-check.ts"
import { AbuseZeroError, type EvaluationRequest } from "abusezero/server";
import { abusezero } from "./abusezero";
export async function evaluateWithBotCheck(input: EvaluationRequest, attempt: number) {
try {
return await abusezero.evaluate(input, `evaluate:${input.claim_id}:${attempt}`);
} catch (error) {
if (error instanceof AbuseZeroError && error.code.startsWith("BOT_EVIDENCE_")) {
return { status: "run_bot_check_again" as const };
}
throw error;
}
}
```
The resubmitted claim keeps its `claim_id` but carries a new token, so it needs a new [`Idempotency-Key`](/docs/api-reference#idempotency). Here the key includes an attempt number.
## Verify your integration [step] [#verify-your-integration-step]
Submit a claim from your page, then open its evaluation with [`GET /v1/evaluations/{id}`](/docs/api-reference/evaluations#retrieve-an-evaluation).
```bash
curl https://api.abusezero.com/v1/evaluations/eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93 \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
- [`browser_data`](/docs/api-reference/evaluations#browser-data) is `collected`.
- In the evaluation's [evidence](/docs/api-reference/evaluations#retrieve-an-evaluations-evidence) or the dashboard, the `automation` family has an assessment other than `unavailable`.
With `automation` set to `deny`, a claim submitted by automation is denied with the reason `automation`.
Let legitimate AI agents through on the benefits you choose.
Every benefit setting and its values.
# Set up browser protection (/docs/guides/browser-setup)
Browser protection adds device and automation evidence to every evaluation. The [AbuseZero browser SDK](/docs/tools/browser-sdk) collects it on your pages, and your server passes the result to AbuseZero as one opaque value: the device token. Evaluations still run without it, and the response always says whether browser data arrived. For how it fits together, see [Browser protection](/docs/concepts/browser-protection).
## Before you begin [#before-you-begin]
- A [publishable key](/docs/keys-and-access) (`pk_…`) from **Developers → API keys**. It can only send browser data, so it is safe to use in page code.
- A server that already evaluates claims. See [Protect a benefit end to end](/docs/guides/protect-a-benefit).
- A server key with the [scopes](/docs/api-reference/authentication#scopes) `browser_collection:read`, `browser_collection:write` and `evaluations:create`.
- The HTTPS origins of the pages where people claim benefits.
## List your sites [step] [#list-your-sites-step]
AbuseZero accepts browser data only from HTTPS sites in your site list. Add them in **Developers → Browser setup**, or with [`PATCH /v1/browser-collection`](/docs/api-reference/browser-collection#update-the-site-list) in the AbuseZero REST API. The [AbuseZero CLI](/docs/tools/cli#cli-command-reference) command is `abusezero browser-collection update`, and the [AbuseZero MCP server](/docs/tools/mcp#mcp-tools) tool is `abusezero_update_browser_collection`. You can list up to 20 origins. An update replaces the whole list, so send the current `revision` to avoid overwriting a change made at the same time. A `revision` that is no longer current returns `REVISION_CONFLICT` (see [REST API errors](/docs/api-reference/errors#conflict)).
```ts tab="Node.js"
import { abusezero } from "./lib/abusezero";
const current = await abusezero.browserCollection() as { origins: string[]; revision: number };
await abusezero.updateBrowserCollection({
origins: [...current.origins, "https://app.example.com"],
revision: current.revision,
}, `browser-sites:${current.revision}`);
```
```bash tab="cURL"
curl -X PATCH https://api.abusezero.com/v1/browser-collection \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: browser-sites:1" \
-d '{ "origins": ["https://app.example.com"], "revision": 1 }'
```
Pages on a site that isn't listed return the device token `unavailable:site_not_listed`. See [Your sites](/docs/tools/browser-sdk#your-sites).
## Allow the SDK in your Content Security Policy [step] [#allow-the-sdk-in-your-content-security-policy-step]
If your site sets a Content Security Policy, copy the sources shown in **Developers → Browser setup** into it. Without them, the browser can block the SDK, and evaluations arrive without browser data.
## Load the browser SDK [step] [#load-the-browser-sdk-step]
Load the SDK once on every page where people claim a benefit. Choose the [package](/docs/tools) if you use a bundler, or the [standalone browser script](/docs/tools/browser-sdk#standalone-browser-script) if you don't.
```npm
npm install abusezero
```
```ts title="app/abusezero-browser.ts"
import { init } from "abusezero/browser";
init({ key: "pk_…" });
```
```html
```
With your publishable key in its address, the script reports in by itself.
[`init`](/docs/tools/browser-sdk#init) tells AbuseZero, once per page load, that the SDK is installed on this site. It sends only the site's address and the SDK version, never visitor data. **Browser setup** uses it to show where the SDK is running.
Keep browser and server code apart. Import `abusezero/browser` in page code only, and never put a secret key in the browser.
## Collect browser data for the claim [step] [#collect-browser-data-for-the-claim-step]
When the person submits the claim, call [`observe`](/docs/tools/browser-sdk#observe) with the same `claim_id` your server will evaluate, then send the `deviceToken` to your server with the form.
```ts title="app/trial-form.ts"
import { observe } from "abusezero/browser";
export async function submitTrial(claimId: string, email: string) {
const { deviceToken } = await observe({ key: "pk_…", claimId });
await fetch("/api/trial", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ claimId, email, deviceToken }),
});
}
```
```html
```
In the standalone example, your server renders the claim ID into the form as `data-claim-id`.
`observe` always returns a `deviceToken`. When browser data is available, the token carries it. When it isn't, the token carries the reason, such as `unavailable:timed_out`. Send it either way. [Unavailable reasons](/docs/tools/browser-sdk#unavailable-reasons) lists every reason.
A device token belongs to one claim. A token collected for another `claim_id` returns `DEVICE_TOKEN_SCOPE` ([invalid request](/docs/api-reference/errors#invalid-request)), and a token already used by another claim returns `DEVICE_TOKEN_USED` ([conflict](/docs/api-reference/errors#conflict)).
## Respect consent [step] [#respect-consent-step]
Collection runs by default. When you pass a [`consent`](/docs/tools/browser-sdk#consent) callback and it resolves to `false`, nothing is collected and the token reports `unavailable:consent_declined`.
```ts
const { deviceToken } = await observe({
key: "pk_…",
claimId,
consent: () => consentTool.allows("security"),
});
```
`consentTool.allows` stands for your consent tool's own check.
- If your consent tool keeps the SDK from loading at all, send `unavailable:not_loaded` as the device token.
- When a person withdraws consent, call `forgetBrowser()`. It removes what the SDK stored in that browser.
## Send the device token with the evaluation [step] [#send-the-device-token-with-the-evaluation-step]
On your server, pass the token unchanged as `identifiers.device_token` in [`evaluate()`](/docs/tools/server-sdk#evaluate-a-claim) or [`POST /v1/evaluations`](/docs/api-reference/evaluations#create-an-evaluation).
```ts tab="Node.js" title="app/api/trial/route.ts"
import { abusezero } from "@/lib/abusezero";
export async function POST(request: Request) {
const body = await request.json() as { claimId: string; email: string; deviceToken: string };
const evaluation = await abusezero.evaluate({
benefit: "pro_trial",
claim_id: body.claimId,
subject: { email: body.email },
identifiers: { device_token: body.deviceToken },
}, `evaluate:${body.claimId}`);
return Response.json({ evaluation });
}
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/evaluations \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: evaluate:trial_8f2d41" \
-d '{
"benefit": "pro_trial",
"claim_id": "trial_8f2d41",
"subject": { "email": "jordan@example.com" },
"identifiers": { "device_token": "" }
}'
```
A real integration sends the signed-in user's ID and every other identifier it has, as in [Protect a benefit end to end](/docs/guides/protect-a-benefit).
## Verify your integration [step] [#verify-your-integration-step]
Open a page with the SDK, then read your browser setup with [`GET /v1/browser-collection`](/docs/api-reference/browser-collection#retrieve-browser-setup), or `abusezero browser-collection get` in the [AbuseZero CLI](/docs/tools/cli#cli-command-reference).
```bash
curl https://api.abusezero.com/v1/browser-collection \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
```json title="Response (abridged)"
{
"object": "browser_collection",
"origins": ["https://app.example.com"],
"revision": 2,
"sites": [
{
"origin": "https://app.example.com",
"listed": true,
"first_seen_at": "2026-10-29T14:02:11.000Z",
"last_seen_at": "2026-10-29T14:31:47.000Z",
"sdk_version": "0.11.0"
}
],
"issue": null,
"issue_since": null
}
```
Then submit a claim and open its evaluation with [`GET /v1/evaluations/{id}`](/docs/api-reference/evaluations#retrieve-an-evaluation). [`browser_data`](/docs/api-reference/evaluations#browser-data) is `collected`. If it isn't, the value tells you what to fix:
| `browser_data` | What to do |
|---|---|
| `not_passed_on` | Your page collected a token, but your server didn't send it. Pass `deviceToken` through to `identifiers.device_token`. |
| `site_not_listed` | Add the page's origin to your site list. |
| `sdk_outdated` | Update the `abusezero` package or the standalone script address. |
| `not_sent` | The evaluation carried no device token. Send one with every evaluation. |
| `expired` | Send the token sooner after the SDK returns it. |
`consent_declined`, `not_loaded`, `blocked`, `timed_out` and `failed` are expected for some visitors. They don't need a fix. [Troubleshooting](/docs/troubleshooting#browser-data) covers every status. **Browser setup** shows the share of each status over the last seven days.
Run AbuseZero's bot check when the person submits.
Every function and option in `abusezero/browser`.
# Export and delete data (/docs/guides/export-and-delete-data)
You can take your evaluation history out of AbuseZero at any time, and delete a single user's history when you need to. Both run in the background and report their progress. This guide covers both from the [AbuseZero REST API](/docs/api-reference) and the [AbuseZero Node.js SDK](/docs/tools/server-sdk). In the dashboard, **Export CSV** on the **Evaluations** page exports the same file.
## Before you begin [#before-you-begin]
- For exports, a server key with the [scopes](/docs/api-reference/authentication#scopes) `exports:create` and `exports:read`. To include evidence columns, the key also needs `evaluation_evidence:read`.
- For deletions, a server key with `data_deletions:read` and `data_deletions:write`.
## Export your evaluations [#export-your-evaluations]
### Request the export [step] [#request-the-export-step]
Request it with [`POST /v1/exports`](/docs/api-reference/exports#request-an-export), `abusezero exports create` in the [AbuseZero CLI](/docs/tools/cli#cli-command-reference), or `abusezero_request_export` on the [AbuseZero MCP server](/docs/tools/mcp#mcp-tools). Choose how many days back to export, from 1 to 90. The default is 30.
```ts tab="Node.js"
import { abusezero } from "./lib/abusezero";
const requested = await abusezero.requestExport({ days: 30 }, "export-2026-10-29");
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/exports \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: export-2026-10-29" \
-d '{ "days": 30 }'
```
```json title="Response"
{
"object": "export",
"id": "exp_5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170",
"status": "pending",
"status_url": "/v1/exports/exp_5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170"
}
```
An export holds up to 10,000 evaluations. When the range holds more, the export's `state` becomes `failed` and its `failure.code` is `too_many_rows`. Request a new export with fewer days.
### Check export status [step] [#check-export-status-step]
Read `status_url` ([`GET /v1/exports/{id}`](/docs/api-reference/exports#retrieve-an-export)) until `state` is `complete` or `failed`. A failed export's `failure` says why; [The export object](/docs/api-reference/exports#the-export-object) lists every field.
```bash
curl https://api.abusezero.com/v1/exports/exp_5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170 \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
```json title="Response"
{
"object": "export",
"id": "exp_5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170",
"state": "complete",
"rows": 4210,
"include_evidence": false,
"completed_at": "2026-10-29T15:22:09.000Z",
"expires_at": "2026-10-30T15:21:58.000Z"
}
```
### Download the file [step] [#download-the-file-step]
Download the file before the export's `expires_at`. Ask for a download address with [`GET /v1/exports/{id}/download`](/docs/api-reference/exports#download-an-export), here through the SDK's [`call()`](/docs/tools/server-sdk#call-for-any-endpoint), then fetch the file from it straight away. The address is temporary. Request a new one if it expires.
```ts tab="Node.js"
import { writeFile } from "node:fs/promises";
const download = await abusezero.call(
"/v1/exports/exp_5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170/download",
) as { url: string; expires_at: string };
const file = await fetch(download.url);
await writeFile("evaluations.csv", Buffer.from(await file.arrayBuffer()));
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/exports/exp_5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170/download \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
The file is a CSV with one row per evaluation ([file columns](/docs/api-reference/exports#file-columns)):
| Column | Value |
|---|---|
| `evaluation_id` | The evaluation's ID (`eval_…`). |
| `created_at` | When the evaluation ran. |
| `benefit` | The benefit's key. |
| `result` | `allow` or `deny`. |
| `reason_code` and `reason` | The [reason code](/docs/reason-codes) and its message. |
| `risk_score` and `risk_band` | The [AbuseZero Risk Score](/docs/concepts/risk-score-and-signals) and its band. |
| `policy_version` | The policy version the evaluation ran under. |
| `claim_id` | Your claim ID. |
| `outcome_state` | The [outcome](/docs/concepts/outcomes)'s current state. |
With `include_evidence: true`, the file adds one column per signal family (`identity`, `device`, `network`, `automation` and `benefit_history`) with its assessment, plus `device_profile_status` and `device_profile_expires_at`.
## Delete a user's history [#delete-a-users-history]
Deleting a user's history removes their claims and outcomes from your [workspace](/docs/keys-and-access). Because export files and uploaded import files can hold that user's data, the deletion also removes every export file and uploaded import file stored in the workspace. Afterwards, the user can qualify for your benefits again. Shared devices and networks that other users also used are kept.
A deletion is permanent and can't be undone. Preview it first.
### Preview the deletion [step] [#preview-the-deletion-step]
Preview with [`POST /v1/data-deletions/preview`](/docs/api-reference/data-deletions#preview-a-deletion), `abusezero data-deletions preview` in the [AbuseZero CLI](/docs/tools/cli#cli-command-reference), or `abusezero_preview_data_deletion` on the [AbuseZero MCP server](/docs/tools/mcp#mcp-tools). Identify the user the way your evaluations did, with a [selector](/docs/api-reference/data-deletions#the-selector). If they sent `subject.external_id`, use that ID with `email: false`. If they sent only an email address, send the address as `user` with `email: true`.
```ts tab="Node.js"
const preview = await abusezero.previewDataDeletion({
selector: { kind: "subject", user: "user_4821", email: false },
});
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/data-deletions/preview \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "selector": { "kind": "subject", "user": "user_4821", "email": false } }'
```
```json title="Response"
{
"object": "deletion_preview",
"counts": { "claims": 3, "outcomes": 3, "files": 0 },
"confirmation": "9b1f0c7e2a4d6b8f1e3c5a7d9b0f2e4c6a8d0b2f4e6c8a0d2b4f6e8c0a2d4b6f"
}
```
Check `counts` before you continue. `files` is the number of finished export files and uploaded import files the deletion removes.
### Request the deletion [step] [#request-the-deletion-step]
Send the same selector to request the deletion with [`POST /v1/data-deletions`](/docs/api-reference/data-deletions#request-a-deletion), `abusezero data-deletions create`, or `abusezero_request_data_deletion`.
```ts tab="Node.js"
const deletion = await abusezero.requestDataDeletion({
selector: { kind: "subject", user: "user_4821", email: false },
}, "delete-user-4821");
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/data-deletions \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: delete-user-4821" \
-d '{ "selector": { "kind": "subject", "user": "user_4821", "email": false } }'
```
```json title="Response"
{
"object": "deletion",
"id": "del_0c9b8a7f-6e5d-4c3b-a291-807f6e5d4c3b",
"status": "pending",
"status_url": "/v1/data-deletions/del_0c9b8a7f-6e5d-4c3b-a291-807f6e5d4c3b"
}
```
A deletion can't start while an import is unfinished (`IMPORTS_IN_PROGRESS`) or while the same user's history is already being deleted (`DELETION_IN_PROGRESS`). Both are [conflict errors](/docs/api-reference/errors#conflict). The API deletes users' histories only. The workspace owner deletes a whole workspace in the dashboard.
### Check deletion status [step] [#check-deletion-status-step]
Read `status_url` ([`GET /v1/data-deletions/{id}`](/docs/api-reference/data-deletions#retrieve-a-deletion)) until `state` is `complete`. While the deletion runs, new claims for that user return `DELETION_IN_PROGRESS`.
```bash
curl https://api.abusezero.com/v1/data-deletions/del_0c9b8a7f-6e5d-4c3b-a291-807f6e5d4c3b \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
## Verify the result [#verify-the-result]
- The export's `state` is `complete`, and the CSV's row count matches `rows`.
- The deletion's `state` is `complete`. A new preview for the same user returns zero `claims` and `outcomes`.
Every export field and state.
Selectors, states and errors.
# Go-live checklist (/docs/guides/go-live-checklist)
Work through this list before you route real claims through AbuseZero. Each item links to the guide or reference that covers it. Every evaluation is real and uses credits, including the ones you run during these checks.
## Secure your keys [step] [#secure-your-keys-step]
- [ ] Secret and restricted keys exist only in server code and your secrets manager. None is in a browser bundle, a mobile app or a repository.
- [ ] Your production service uses a restricted key with only the [scopes](/docs/api-reference/authentication#scopes) it calls, such as `evaluations:create` and `outcomes:write`. See [Keys and access](/docs/keys-and-access).
- [ ] Browser code uses only your publishable key (`pk_…`).
- [ ] You know each key's expiry date, if it has one, and who replaces it. Keys are managed in **Developers → API keys**.
## Review each benefit [step] [#review-each-benefit-step]
- [ ] Each [benefit](/docs/concepts/benefits) uses the [protection level](/docs/concepts/protection-levels) or [policy](/docs/concepts/policies-and-rules) you intend. Read each one with [`GET /v1/benefits`](/docs/api-reference/benefits#list-benefits) or `abusezero benefits list` in the [AbuseZero CLI](/docs/tools/cli#cli-command-reference).
- [ ] `protection_mode` is `protect`. A benefit left in `observe` allows every claim. See [Protection mode](/docs/api-reference/benefits#protection-mode).
- [ ] [`eligibility`](/docs/api-reference/benefits#eligibility) matches how often a user may claim the benefit.
- [ ] `outcome_tracking` matches your flow. Use `report_outcome` when granting can still fail after an `allow`. See [Report outcomes](/docs/guides/report-outcomes).
- [ ] `automation`, `bot_evidence` and `allowed_agents` are set the way you want. See [Add a bot check](/docs/guides/bot-check) and [Verify AI agents](/docs/guides/verify-ai-agents).
## Integrate every claim path [step] [#integrate-every-claim-path-step]
- [ ] Every place a user can receive the benefit evaluates the claim first: web sign-up, in-app upgrades, mobile apps and your own API.
- [ ] Each claim has a stable `claim_id`, and each request an [`Idempotency-Key`](/docs/api-reference#idempotency) that you store with the claim.
- [ ] Every evaluation sends every identifier you have, including the real visitor IP from your trusted proxy chain. See [Send identifiers](/docs/guides/send-identifiers).
- [ ] `email_verified` and `phone_verified` are `true` only when your app verified them.
- [ ] A `deny` shows the person a neutral message, and your team can see `reason.code`. See [Reason codes](/docs/reason-codes).
## Check browser protection [step] [#check-browser-protection-step]
- [ ] Every site where people claim a benefit is in your site list, in **Developers → Browser setup** or [`PATCH /v1/browser-collection`](/docs/api-reference/browser-collection#update-the-site-list).
- [ ] Your Content Security Policy includes the sources shown in **Browser setup**.
- [ ] Every evaluation sends `identifiers.device_token`, including when the [AbuseZero browser SDK](/docs/tools/browser-sdk) reports no browser data.
- [ ] **Browser setup** shows your sites as seen, with no open issue. See [Set up browser protection](/docs/guides/browser-setup).
## Import outcome history [step] [#import-outcome-history-step]
- [ ] Benefits you granted before AbuseZero are imported, so earlier users are recognized from day one. See [Import outcome history](/docs/guides/import-claim-history).
- [ ] If a benefit waits for outcome reports, your code [reports every outcome](/docs/api-reference/outcomes#report-an-outcome) before `outcome.due_at`.
## Set the outage behavior [step] [#set-the-outage-behavior-step]
- [ ] You chose an [`outage`](/docs/tools/server-sdk#the-outage-option) behavior on purpose. See [Handle outages and retries](/docs/guides/outages-and-retries).
- [ ] Retries reuse the same `Idempotency-Key` and request body.
- [ ] [Skipped evaluations](/docs/api-reference/evaluations#skipped-evaluations) have their own branch and alert the workspace owner.
- [ ] Your logs keep the [`X-Request-ID`](/docs/api-reference#request-ids) of every AbuseZero call.
## Confirm billing [step] [#confirm-billing-step]
- [ ] The workspace owner has checked credits and plan limits in **Workspace settings → Billing & Subscriptions**.
- [ ] Your team knows that every evaluation, preview and **Test console** run uses credits.
## Run final verification [step] [#run-final-verification-step]
Run one real claim through each claim path, then open each evaluation with [`GET /v1/evaluations/{id}`](/docs/api-reference/evaluations#retrieve-an-evaluation):
- [ ] `result` is the one you expect, and `mode` is `protect`.
- [ ] [`inputs`](/docs/api-reference/evaluations#inputs) shows `received` for every identifier you send.
- [ ] [`browser_data`](/docs/api-reference/evaluations#browser-data) is `collected` for claims from your web pages.
- [ ] `policy.id` and `policy.version` match the policy you reviewed.
Then claim the same benefit again as the same user with a new `claim_id`. The second evaluation is denied.
Finally, set up the scheduled check in [Monitor your integration](/docs/guides/monitor-your-integration), so your team hears about a problem before your users do.
Symptoms, causes and fixes.
Change policies after launch without surprises.
# Import outcome history (/docs/guides/import-claim-history)
[Evaluations](/docs/concepts/evaluations) check every claim against your workspace's claim history. Importing the [benefits](/docs/concepts/benefits) you granted before AbuseZero means a user who already had a trial is recognized from the first evaluation. You can send history from code, up to 1,000 outcomes per request, or upload a CSV file of up to 100,000 rows. For how history is used, see [Outcomes](/docs/concepts/outcomes).
## Before you begin [#before-you-begin]
- The benefits you're importing exist in your workspace. Each row names a benefit by its `key`.
- A server key with the [scopes](/docs/api-reference/authentication#scopes) `imports:read` and `imports:write`.
- Only granted benefits are imported. Each record needs the user's email, the benefit key, the outcome `granted` and the time it was granted, with a timezone offset. That time can't be in the future.
## Import from code [#import-from-code]
Use this path when your history lives in a database you can read from your server.
### Send the outcomes [step] [#send-the-outcomes-step]
Send up to 1,000 outcomes per request with the SDK's [`importOutcomes()`](/docs/tools/server-sdk#additional-methods) or [`POST /v1/imports`](/docs/api-reference/imports#import-outcomes-as-json). The [AbuseZero CLI](/docs/tools/cli#cli-command-reference) command is `abusezero imports create`, and the [AbuseZero MCP server](/docs/tools/mcp#mcp-tools) tool is `abusezero_create_outcome_import`. Split larger histories into batches, each with its own [`Idempotency-Key`](/docs/api-reference#idempotency).
```ts tab="Node.js"
import { abusezero } from "./lib/abusezero";
const importJob = await abusezero.importOutcomes([
{
outcome: "granted",
benefit: "pro_trial",
subject: { external_id: "user_1093", email: "sam@example.com" },
occurred_at: "2025-06-14T09:30:00Z",
external_outcome_id: "trial_1093",
},
], "import-trials-batch-0001");
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/imports \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: import-trials-batch-0001" \
-d '{
"outcomes": [{
"outcome": "granted",
"benefit": "pro_trial",
"subject": { "external_id": "user_1093", "email": "sam@example.com" },
"occurred_at": "2025-06-14T09:30:00Z",
"external_outcome_id": "trial_1093"
}]
}'
```
```json title="Response"
{
"object": "import",
"id": "imp_2d4f6a8c-1b3e-4c5d-9e7f-0a1b2c3d4e5f",
"state": "queued"
}
```
`external_outcome_id` is optional. Use your own record ID so a repeated import can't create the same outcome twice.
### Check import status [step] [#check-import-status-step]
The import runs in the background. Read it with [`GET /v1/imports/{id}`](/docs/api-reference/imports#retrieve-an-import) or `abusezero imports get` until `state` is `complete`. [Import states](/docs/api-reference/imports#import-states) lists every state.
```ts
const status = await abusezero.getImport("imp_2d4f6a8c-1b3e-4c5d-9e7f-0a1b2c3d4e5f");
```
## Import a CSV file [#import-a-csv-file]
Use this path for a one-time export from another system. The file can be up to 25 MiB and 100,000 rows, in the [file format](/docs/api-reference/imports#file-format) below.
| Column | Required | Value |
|---|---|---|
| `email` | Yes | The user's email address. |
| `benefit_key` | Yes | The benefit's `key`. |
| `outcome` | Yes | `granted`. |
| `occurred_at` | Yes | When the benefit was granted, in ISO 8601 with a timezone offset. |
| `external_user_id` | No | Your user ID. |
| `external_outcome_id` | No | Your record ID for the grant. |
```csv title="history.csv"
email,benefit_key,outcome,occurred_at,external_user_id,external_outcome_id
sam@example.com,pro_trial,granted,2025-06-14T09:30:00Z,user_1093,trial_1093
ari@example.com,pro_trial,granted,2025-07-02T17:05:00+02:00,user_1188,trial_1188
```
You can also upload the file in **Developers → Data import**.
### Create the upload [step] [#create-the-upload-step]
Create the upload with [`POST /v1/imports/csv`](/docs/api-reference/imports#create-an-upload), `abusezero imports create-csv` in the [AbuseZero CLI](/docs/tools/cli#cli-command-reference), or `abusezero_create_csv_import` on the [AbuseZero MCP server](/docs/tools/mcp#mcp-tools).
```ts tab="Node.js"
const upload = await abusezero.createCsvImport("import-csv-2026-10-29") as {
id: string;
url: string;
upload_headers: Record;
};
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/imports/csv \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: import-csv-2026-10-29" \
-d '{}'
```
The response is an `import_upload` with the import's `id`, an upload `url` and the `upload_headers` to send with the file. Retrying with the same `Idempotency-Key` returns the same import with a fresh `url`.
### Upload the file [step] [#upload-the-file-step]
Send the file with `PUT` to `url`, with exactly the headers in `upload_headers`. See [Upload the file](/docs/api-reference/imports#upload-the-file) for the errors an upload can return.
```ts
import { readFile } from "node:fs/promises";
const response = await fetch(upload.url, {
method: "PUT",
headers: upload.upload_headers,
body: await readFile("history.csv"),
});
if (!response.ok) throw new Error(`Upload failed with status ${response.status}`);
```
The upload address is temporary. If it expires before the file is sent, retry the create call with the same `Idempotency-Key` for a fresh address.
### Validate the file [step] [#validate-the-file-step]
Validation checks every row before anything is imported. Start it after the upload succeeds, then read the import until `state` is `preview`. The REST API call is [`POST /v1/imports/{id}/validate`](/docs/api-reference/imports#validate-the-import). Validating before the file arrives returns `IMPORT_NOT_UPLOADED` ([conflict](/docs/api-reference/errors#conflict)), and the import waits in `uploading`.
```ts tab="Node.js"
await abusezero.validateImport(upload.id, `validate:${upload.id}`);
const checked = await abusezero.getImport(upload.id);
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/imports/imp_2d4f6a8c-1b3e-4c5d-9e7f-0a1b2c3d4e5f/validate \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: validate:imp_2d4f6a8c-1b3e-4c5d-9e7f-0a1b2c3d4e5f" \
-d '{}'
```
If the import ends in `failed`, its `failure` field gives a `code` and a `message` that say what to fix ([failure reasons](/docs/api-reference/imports#failure-reasons)), such as `csv_columns` or `row_limit`. Fix the file and upload it to a new import.
In `preview`, `validated` is the number of rows read and `rejected` the number with a problem. If any row was rejected, download the error report: [`GET /v1/imports/{id}/errors`](/docs/api-reference/imports#download-the-error-report) returns a temporary `url` for a CSV with each rejected row number and what to fix. The report is available until 7 days after the import completes.
### Confirm the import [step] [#confirm-the-import-step]
When the preview looks right, confirm it with [`POST /v1/imports/{id}/confirm`](/docs/api-reference/imports#confirm-the-import) or `abusezero imports confirm`. Rejected rows are left out.
```ts tab="Node.js"
await abusezero.confirmImport(upload.id, `confirm:${upload.id}`);
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/imports/imp_2d4f6a8c-1b3e-4c5d-9e7f-0a1b2c3d4e5f/confirm \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: confirm:imp_2d4f6a8c-1b3e-4c5d-9e7f-0a1b2c3d4e5f" \
-d '{}'
```
The import moves to `queued`, then `processing`, then `complete`.
To stop an import before it completes, cancel it with [`POST /v1/imports/{id}/cancel`](/docs/api-reference/imports#cancel-an-import). Cancelling permanently deletes the uploaded file and every row not yet imported, and the import's `state` becomes `cancelled`. It can't be undone. A `complete` import can't be cancelled.
## Verify the result [#verify-the-result]
Read the import with [`GET /v1/imports/{id}`](/docs/api-reference/imports#retrieve-an-import) once it's `complete`.
```json title="Response (abridged)"
{
"object": "import",
"id": "imp_2d4f6a8c-1b3e-4c5d-9e7f-0a1b2c3d4e5f",
"state": "complete",
"total": 2,
"validated": 2,
"processed": 2,
"imported": 2,
"skipped": 0,
"rejected": 0
}
```
- `imported` is the number of outcomes added to your history.
- `skipped` is the number already in your history.
- `rejected` is the number with a problem.
Then preview a claim for an imported user with [a preview](/docs/api-reference/previews-simulations-replays#preview-an-evaluation), as in [Test changes safely](/docs/guides/test-changes-safely). The result reflects their earlier claim.
Keep your history current after the import.
Every import field, state and error.
# Monitor your integration (/docs/guides/monitor-your-integration)
AbuseZero records every evaluation, every evaluation request, successful or failed, and every change to your workspace, and keeps watch over your [browser protection](/docs/concepts/browser-protection) setup. This guide shows where each of those lives, and ends with a scheduled check that alerts your team when something needs attention. Most of it is also in the dashboard, on the **Analytics** page and under **Developers**.
## Before you begin [#before-you-begin]
- A server key with the [scopes](/docs/api-reference/authentication#scopes) `analytics:read`, `request_logs:read`, `audit_logs:read`, `browser_collection:read` and `outcomes:read`.
## Review evaluations over time [step] [#review-evaluations-over-time-step]
[`GET /v1/analytics`](/docs/api-reference/analytics#read-analytics) returns one [daily summary](/docs/api-reference/analytics#the-daily-summary-object) per day and result, for 1 to 90 days back. The default is 30. The [AbuseZero CLI](/docs/tools/cli#cli-command-reference) command is `abusezero analytics get`, and the [AbuseZero MCP server](/docs/tools/mcp#mcp-tools) tool is `abusezero_get_analytics`.
```ts tab="Node.js"
import { abusezero } from "./lib/abusezero";
const analytics = await abusezero.analytics(30);
```
```bash tab="cURL"
curl "https://api.abusezero.com/v1/analytics?days=30" \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
```json title="Response (abridged)"
{
"object": "list",
"data": [
{ "object": "daily_summary", "day": "2026-10-28", "result": "allow", "count": 1184 },
{ "object": "daily_summary", "day": "2026-10-28", "result": "deny", "count": 97 }
],
"next_cursor": null
}
```
When the share of `deny` results changes suddenly, check what changed on your side first: a new [policy](/docs/concepts/policies-and-rules), a new [benefit](/docs/concepts/benefits) setting, or the [identifiers](/docs/guides/send-identifiers) your code sends.
## Trace a request [step] [#trace-a-request-step]
Every response carries an [`X-Request-ID`](/docs/api-reference#request-ids) header, and every error repeats it as `request_id`. Log it with every call your server makes, together with the error code when there is one. [REST API errors](/docs/api-reference/errors) explains each code.
[Request logs](/docs/api-reference/request-logs) list your workspace's evaluation requests, including failed ones with their HTTP `status` and error `code`. Each log's ID is `req_` followed by the request's `X-Request-ID`, for REST and MCP requests alike, so you can match a log line in your system to its record in AbuseZero. Requests that never authenticated and rate-limited requests aren't logged. Read them with [`GET /v1/request-logs`](/docs/api-reference/request-logs#list-request-logs), `abusezero request-logs list` in the [AbuseZero CLI](/docs/tools/cli#cli-command-reference), or `abusezero_list_request_logs` on the [AbuseZero MCP server](/docs/tools/mcp#mcp-tools).
```ts tab="Node.js"
const logs = await abusezero.requestLogs({ limit: 50 });
```
```bash tab="cURL"
curl "https://api.abusezero.com/v1/request-logs?limit=50" \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
```json title="Response (abridged)"
{
"object": "list",
"data": [
{
"object": "request_log",
"id": "req_1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
"status": 200,
"code": null,
"created_at": "2026-10-29T14:58:31.000Z"
}
],
"next_cursor": null
}
```
## Check browser protection [step] [#check-browser-protection-step]
[Browser setup](/docs/api-reference/browser-collection#retrieve-browser-setup) reports the one issue that most needs fixing, if any, and what recent evaluations received from the [AbuseZero browser SDK](/docs/tools/browser-sdk) over the last seven days. The [AbuseZero CLI](/docs/tools/cli#cli-command-reference) command is `abusezero browser-collection get`.
```bash
curl https://api.abusezero.com/v1/browser-collection \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
| [`issue`](/docs/api-reference/browser-collection#issues) | What to do |
|---|---|
| `not_passed_on` | Your pages collect device tokens, but your server doesn't send them. Pass `deviceToken` through to `identifiers.device_token`. |
| `site_not_listed` | A page outside your site list is sending browser data. Add its origin in **Developers → Browser setup**. |
| `sdk_outdated` | A site runs an outdated browser SDK. Update the package or the standalone script address. |
| `script_missing` | AbuseZero hasn't seen the browser SDK on your sites. Check that it loads on every page where people claim a benefit. See [Troubleshooting](/docs/troubleshooting#browser-setup-shows-script_missing). |
`issue_since` shows when the issue started. `coverage` counts each [browser data status](/docs/api-reference/browser-collection#browser-data-status) over the same seven days.
## Review changes to your workspace [step] [#review-changes-to-your-workspace-step]
The [audit log](/docs/api-reference/audit-logs) records changes to your [workspace](/docs/keys-and-access) and who made them: a member or the system. Read it with [`GET /v1/audit-logs`](/docs/api-reference/audit-logs#list-audit-events), `abusezero audit-logs list` in the [AbuseZero CLI](/docs/tools/cli#cli-command-reference), or `abusezero_list_audit_logs` on the [AbuseZero MCP server](/docs/tools/mcp#mcp-tools). In the dashboard, it's the **Audit trail**.
```ts tab="Node.js"
const events = await abusezero.auditLogs({ limit: 50 });
```
```bash tab="cURL"
curl "https://api.abusezero.com/v1/audit-logs?limit=50" \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
```json title="Response (abridged)"
{
"object": "list",
"data": [
{
"object": "audit_event",
"id": "aud_48213",
"action": "evaluations.export_requested",
"actor": "member",
"created_at": "2026-10-29T15:22:09.000Z"
}
],
"next_cursor": null
}
```
## Check claims awaiting an outcome [step] [#check-claims-awaiting-an-outcome-step]
If a benefit waits for outcome reports, outcomes that stay in `awaiting_outcome` past their `due_at`, or that reach `outcome_unknown`, mean your reports aren't arriving. List them with [`GET /v1/outcomes`](/docs/api-reference/outcomes#list-outcomes); [Outcome states](/docs/api-reference/outcomes#outcome-states) explains each state. See [Report outcomes](/docs/guides/report-outcomes).
```ts
const outcomes = await abusezero.outcomes({ limit: 100 }) as { data: { state: string }[] };
const unreported = outcomes.data.filter(outcome => outcome.state === "outcome_unknown").length;
```
## Schedule automated checks [step] [#schedule-automated-checks-step]
Run one check on a schedule and alert your team when it finds something. It uses the SDK's [additional methods](/docs/tools/server-sdk#additional-methods).
```ts title="jobs/abusezero-health.ts"
import { abusezero } from "../lib/abusezero";
import { alertTeam } from "../lib/alerts";
export async function checkAbuseZeroHealth() {
const browser = await abusezero.browserCollection() as { issue: string | null; issue_since: string | null };
if (browser.issue) {
await alertTeam(`AbuseZero browser setup: ${browser.issue} since ${browser.issue_since}`);
}
const outcomes = await abusezero.outcomes({ limit: 100 }) as { data: { state: string }[] };
const unreported = outcomes.data.filter(outcome => outcome.state === "outcome_unknown").length;
if (unreported > 0) {
await alertTeam(`AbuseZero: ${unreported} recent outcomes were never reported`);
}
}
```
`alertTeam` stands for your own alerting.
## Verify your integration [#verify-your-integration]
Run `checkAbuseZeroHealth` once by hand. With a healthy integration, it sends no alerts. Then schedule it to run regularly, for example every hour, alongside your own alerts on failed AbuseZero calls.
Symptoms, causes and fixes.
Everything to check before real users arrive.
# Handle outages and retries (/docs/guides/outages-and-retries)
Every [evaluation](/docs/concepts/evaluations) returns one final result within the evaluation's [time limit](/docs/api-reference#time-limits), and every write can be retried safely with the same [`Idempotency-Key`](/docs/api-reference#idempotency). This guide covers the rare cases around that: AbuseZero unreachable from your server, a retryable error, and an evaluation that is skipped instead of decided. Plan for each case once, and every claim gets a defined answer.
## Before you begin [#before-you-begin]
- A server that evaluates claims. See [Protect a benefit end to end](/docs/guides/protect-a-benefit).
## Choose your outage behavior [step] [#choose-your-outage-behavior-step]
The [AbuseZero Node.js SDK](/docs/tools/server-sdk)'s [`outage` option](/docs/tools/server-sdk#the-outage-option) decides what [`evaluate()`](/docs/tools/server-sdk#evaluate-a-claim) returns when AbuseZero can't be reached or returns a retryable error after the SDK's own retries.
| `outage` | What `evaluate()` does |
|---|---|
| `"throw"` | Throws an `AbuseZeroError` with `retryable: true`. Your code decides. |
| `"allow"` | Returns `{ source: "local_outage", result: "allow", claim_id, error }`. Your users keep getting the benefit. |
| `"deny"` | Returns `{ source: "local_outage", result: "deny", claim_id, error }`. Nothing is granted until AbuseZero answers. |
```ts title="lib/abusezero.ts"
import { AbuseZero } from "abusezero/server";
export const abusezero = new AbuseZero({
key: process.env.ABUSEZERO_SECRET_KEY!,
outage: "allow",
});
```
Three cases never become a local decision, whatever you choose:
- **Rate limits.** A `429` always throws, so request volume can never switch protection off. See [Rate limits](/docs/api-reference#rate-limits).
- **Bot check errors.** Codes starting with `BOT_EVIDENCE_` always throw. See [Add a bot check](/docs/guides/bot-check).
- **Skipped evaluations.** These are answers from AbuseZero, covered below and in [Skipped evaluations are not errors](/docs/api-reference/errors#skipped-evaluations-are-not-errors).
## Reuse the idempotency key on every retry [step] [#reuse-the-idempotency-key-on-every-retry-step]
Every write needs an `Idempotency-Key` of 1 to 128 printable characters with no spaces. Store the key with the claim, and send the same key and the same request body on every retry. A retry then returns the original response instead of repeating the write: no second evaluation, outcome, import or delete.
```ts
const idempotencyKey = `evaluate:${claim.claimId}`;
await saveClaimAttempt(claim.claimId, idempotencyKey);
const evaluation = await abusezero.evaluate(input, idempotencyKey);
```
`saveClaimAttempt` stands for your own storage. A changed request body needs a new key: the same key with a different body returns [`IDEMPOTENCY_CONFLICT`](/docs/troubleshooting#idempotency_conflict-409).
## Retry retryable errors [step] [#retry-retryable-errors-step]
The Node.js SDK already [retries](/docs/tools/server-sdk#retries) briefly within its own request budget and honors `Retry-After`. If it still fails, check `retryable` before you retry.
```ts
import { AbuseZeroError } from "abusezero/server";
try {
return await abusezero.evaluate(input, idempotencyKey);
} catch (error) {
if (error instanceof AbuseZeroError && error.retryable) {
await scheduleRetry(claim.claimId, idempotencyKey, error.requestId);
return { status: "pending" as const };
}
throw error;
}
```
`scheduleRetry` stands for your own job queue. Retry with the same key.
| Code | Status | Meaning |
|---|---|---|
| `EVALUATION_PENDING` | 409 | The same evaluation is still processing. Retry with the same key. See [Troubleshooting](/docs/troubleshooting#temporarily_unavailable-503-deadline_exceeded-504-or-evaluation_pending-409). |
| `RATE_LIMITED` | 429 | Too many requests. Wait for `Retry-After` seconds. See [Troubleshooting](/docs/troubleshooting#rate_limited-429). |
| `TEMPORARILY_UNAVAILABLE` | 503 | AbuseZero couldn't finish the request. Retry with the same key. |
| `DEADLINE_EXCEEDED` | 504 | The request ran past its time limit. Retry with the same key. |
If you call the REST API directly, apply the same rules: retry `429` and `5xx` responses, and responses whose error has `"retryable": true`, with the same `Idempotency-Key`, after `Retry-After` when it is present. Bot check errors are the exception: they need a fresh device token and a new key. Log the [`X-Request-ID`](/docs/api-reference#request-ids) header of every failed response. [REST API errors](/docs/api-reference/errors) lists every code.
## Handle skipped evaluations [step] [#handle-skipped-evaluations-step]
A [skipped evaluation](/docs/api-reference/evaluations#skipped-evaluations) means the [workspace](/docs/keys-and-access) couldn't run it, so no allow or deny was decided. Your `outage` setting doesn't apply.
```json title="Response"
{
"object": "evaluation",
"status": "skipped",
"reason": "insufficient_credits"
}
```
| `reason` | What it means |
|---|---|
| `insufficient_credits` | The workspace doesn't have enough credits. |
| `billing_overdue` | The workspace doesn't have enough credits while a payment is overdue. |
| `service_inactive` | The workspace can't run evaluations, for example while it's being deleted. |
| `benefit_paused` | The benefit is paused because the workspace is over its plan's limits. |
Handle a skip as its own branch, and treat a reason you don't recognize like any other skip. New reasons can be added within API `v1` ([REST API versioning](/docs/api-reference#rest-api-versioning)). [Troubleshooting](/docs/troubleshooting#evaluations-return-status-skipped) covers each reason. The workspace owner resolves skips in **Workspace settings → Billing & Subscriptions**.
```ts
if ("status" in evaluation) {
await alertWorkspaceOwner(evaluation.reason);
return applyFallback(claim);
}
```
`alertWorkspaceOwner` and `applyFallback` stand for your own alerting and your chosen fallback.
## Reconcile local decisions [step] [#reconcile-local-decisions-step]
When `evaluate()` returns `source: "local_outage"`, AbuseZero didn't decide the claim. Keep the claim ID and its idempotency key. When AbuseZero is reachable again, evaluate the claim again with the same key and body. If the first request did reach AbuseZero, you get its original result. Otherwise, the claim is evaluated now.
```ts
const evaluation = await abusezero.evaluate(input, storedIdempotencyKey);
if (!("status" in evaluation) && evaluation.source === "abusezero" && evaluation.result === "deny") {
await reviewGrantedDuringOutage(input.claim_id, evaluation);
}
```
`reviewGrantedDuringOutage` stands for your own follow-up, such as a review queue.
## Verify your integration [step] [#verify-your-integration-step]
Simulate an outage with a client whose [`fetch`](/docs/tools/server-sdk#client-options) always fails, then check what your code receives.
```ts
import { AbuseZero } from "abusezero/server";
const offline = new AbuseZero({
key: process.env.ABUSEZERO_SECRET_KEY!,
outage: "allow",
fetch: async () => { throw new Error("offline"); },
});
const result = await offline.evaluate(input, "outage-check-0001");
console.log(result);
```
With `outage: "allow"`, `result.source` is `local_outage` and `result.result` is `allow`. With `outage: "throw"`, the call throws an `AbuseZeroError` whose `retryable` is `true`.
Every error code, its status and what to do.
Every client option, including `outage` and `fetch`.
# Protect a benefit end to end (/docs/guides/protect-a-benefit)
This guide protects one benefit, a free trial, from the first claim to the recorded outcome. Each step shows the [AbuseZero Node.js SDK](/docs/tools/server-sdk) first and the [AbuseZero REST API](/docs/api-reference) call second. For the ideas behind each step, see [Benefits](/docs/concepts/benefits) and [Evaluations](/docs/concepts/evaluations).
## Before you begin [#before-you-begin]
- A [secret key, or a restricted key](/docs/keys-and-access) with the [scopes](/docs/api-reference/authentication#scopes) `benefits:write`, `evaluations:create`, `evaluations:read` and `outcomes:write`. Create one in **Developers → API keys** in the [dashboard](https://app.abusezero.com).
- Node.js 22 or later for the SDK examples.
## Install the SDK [step] [#install-the-sdk-step]
```npm
npm install abusezero
```
[Create one client](/docs/tools/server-sdk#create-a-nodejs-sdk-client) and reuse it for every request. Keep it in server code only.
```ts title="lib/abusezero.ts"
import { AbuseZero } from "abusezero/server";
export const abusezero = new AbuseZero({
key: process.env.ABUSEZERO_SECRET_KEY!,
outage: "throw",
});
```
[`outage`](/docs/tools/server-sdk#the-outage-option) decides what your code receives if AbuseZero can't be reached. This guide uses `"throw"`. [Handle outages and retries](/docs/guides/outages-and-retries) explains the other choices.
## Create the benefit [step] [#create-the-benefit-step]
A [benefit](/docs/concepts/benefits) is the thing you protect. Its `key` is how every evaluation refers to it. This benefit uses the Balanced [protection level](/docs/concepts/protection-levels), can be claimed once per user, and waits for you to report each [outcome](/docs/concepts/outcomes).
```ts tab="Node.js"
import { abusezero } from "./lib/abusezero";
const benefit = await abusezero.createBenefit({
name: "Pro trial",
key: "pro_trial",
policy_key: "balanced",
protection_mode: "protect",
config: {
eligibility: { kind: "once" },
outcome_tracking: "report_outcome",
},
}, "create-benefit-pro-trial");
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/benefits \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: create-benefit-pro-trial" \
-d '{
"name": "Pro trial",
"key": "pro_trial",
"policy_key": "balanced",
"protection_mode": "protect",
"config": {
"eligibility": { "kind": "once" },
"outcome_tracking": "report_outcome"
}
}'
```
```json title="Response"
{
"object": "benefit",
"id": "ben_6f1d2c3b-4a5e-4f60-8b7c-9d0e1f2a3b4c",
"revision": 1
}
```
The REST API call is [`POST /v1/benefits`](/docs/api-reference/benefits#create-a-benefit), and the SDK method is [`createBenefit()`](/docs/tools/server-sdk#additional-methods). The [AbuseZero CLI](/docs/tools/cli#cli-command-reference) runs the same action as `abusezero benefits create`, and the [AbuseZero MCP server](/docs/tools/mcp#mcp-tools) as `abusezero_create_benefit`. You can also create benefits on the **Benefits** page of the dashboard. Every setting is described in the [Benefits reference](/docs/api-reference/benefits#benefit-configuration).
## Assign a stable claim ID [step] [#assign-a-stable-claim-id-step]
Every [claim](/docs/concepts/evaluations) needs a `claim_id`: 1 to 128 printable characters, no spaces. Use the ID your app already has for the request, such as a trial request ID. Keep the same `claim_id` for every retry of the same claim.
- If the same claim is evaluated again after it was allowed, AbuseZero returns `allow` with the [reason](/docs/reason-codes) `previously_authorized`. See [Re-evaluating a `claim_id`](/docs/api-reference/evaluations#re-evaluating-a-claim_id).
- A `claim_id` used for a different user returns [`CLAIM_CONFLICT`](/docs/troubleshooting#claim_conflict-409).
- If you use [browser protection](/docs/guides/browser-setup), the browser SDK must collect data for the same `claim_id`.
Every evaluation also needs an [`Idempotency-Key`](/docs/api-reference#idempotency). Use the same key only for an exact retry of the same request. A different request with the same key returns [`IDEMPOTENCY_CONFLICT`](/docs/troubleshooting#idempotency_conflict-409).
## Evaluate the claim [step] [#evaluate-the-claim-step]
Call AbuseZero before you grant anything, with the SDK's [`evaluate()`](/docs/tools/server-sdk#evaluate-a-claim) or [`POST /v1/evaluations`](/docs/api-reference/evaluations#create-an-evaluation). Send every identifier you have; [Send identifiers](/docs/guides/send-identifiers) covers each one. AbuseZero checks each one against your workspace's full claim history, and the response tells you which [inputs](/docs/concepts/risk-score-and-signals) it received.
```ts tab="Node.js" title="lib/evaluate-trial.ts"
import { abusezero } from "./abusezero";
export type TrialClaim = {
claimId: string;
userId: string;
email: string;
emailVerified: boolean;
ip?: string;
deviceToken?: string;
};
export function evaluateTrial(claim: TrialClaim) {
return abusezero.evaluate({
benefit: "pro_trial",
claim_id: claim.claimId,
subject: {
external_id: claim.userId,
email: claim.email,
email_verified: claim.emailVerified,
},
context: claim.ip ? { ip: claim.ip } : undefined,
identifiers: claim.deviceToken ? { device_token: claim.deviceToken } : undefined,
}, `evaluate:${claim.claimId}`);
}
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/evaluations \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: evaluate:trial_8f2d41" \
-d '{
"benefit": "pro_trial",
"claim_id": "trial_8f2d41",
"subject": {
"external_id": "user_4821",
"email": "jordan@example.com",
"email_verified": true
},
"context": { "ip": "203.0.113.42" }
}'
```
```json title="Response"
{
"object": "evaluation",
"id": "eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93",
"result": "allow",
"risk": { "score": 10, "band": "low" },
"reason": {
"code": "default_allow",
"message": "No rule matched, so the policy's default allowed this claim."
},
"outcome": { "state": "awaiting_outcome", "due_at": "2026-10-29T15:10:00.000Z" }
}
```
Send the visitor's real IP address as your trusted proxy chain reports it. AbuseZero rejects private, documentation and other non-routable addresses, such as the one in the example, with [`IP_NOT_ROUTABLE`](/docs/troubleshooting#ip_not_routable-422). In the SDK, `evaluate()` returns `id`, `result`, `risk`, `reason` and `outcome`, with `source: "abusezero"`. Every field is described in [The evaluation object](/docs/api-reference/evaluations#the-evaluation-object).
For manual checks, the [AbuseZero CLI](/docs/tools/cli#cli-command-reference) runs the same evaluation as `abusezero evaluations create`, and the [AbuseZero MCP server](/docs/tools/mcp#mcp-tools) as `abusezero_create_evaluation`.
## Act on the result [step] [#act-on-the-result-step]
Handle four cases: a skipped evaluation, an error, `deny` and `allow`. Grant the benefit only on `allow`.
```ts title="lib/start-trial.ts"
import { AbuseZeroError } from "abusezero/server";
import { abusezero } from "./abusezero";
import { evaluateTrial, type TrialClaim } from "./evaluate-trial";
import { grantTrial } from "./trials";
export async function startTrial(claim: TrialClaim) {
let evaluation;
try {
evaluation = await evaluateTrial(claim);
} catch (error) {
if (error instanceof AbuseZeroError && error.retryable) return { status: "try_again" as const };
throw error;
}
if ("status" in evaluation || evaluation.source === "local_outage") {
return { status: "not_evaluated" as const };
}
if (evaluation.result === "deny") {
return { status: "denied" as const, reason: evaluation.reason?.code };
}
await grantTrial(claim.userId);
await abusezero.outcome(evaluation.id, { outcome: "granted" }, `outcome:${evaluation.id}`);
return { status: "started" as const };
}
```
`grantTrial` stands for your own code that starts the trial.
- **Skipped:** `status: "skipped"` means no decision was made ([skipped evaluations](/docs/api-reference/evaluations#skipped-evaluations)), for example because the workspace is out of credits. Choose your own fallback. See [Handle outages and retries](/docs/guides/outages-and-retries).
- **Denied:** show the person a neutral message. Keep `reason.code` for your team. [Reason codes](/docs/reason-codes) lists every code.
- **Allowed:** grant the benefit, then report the outcome in the next step.
## Report the outcome [step] [#report-the-outcome-step]
This benefit waits for your report, so tell AbuseZero what you granted before `outcome.due_at`. The code in the previous step already does this after `grantTrial`, with the SDK's [`outcome()`](/docs/tools/server-sdk#report-an-outcome). The REST API call is [`POST /v1/evaluations/{id}/outcome`](/docs/api-reference/outcomes#report-an-outcome); the [AbuseZero CLI](/docs/tools/cli#cli-command-reference) command is `abusezero outcomes report`, and the [AbuseZero MCP server](/docs/tools/mcp#mcp-tools) tool is `abusezero_report_outcome`.
```ts tab="Node.js"
await abusezero.outcome(evaluation.id, { outcome: "granted" }, `outcome:${evaluation.id}`);
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/evaluations/eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93/outcome \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: outcome:eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93" \
-d '{ "outcome": "granted" }'
```
If you couldn't grant the benefit after all, report `not_granted` with a `reason` instead. [Report outcomes](/docs/guides/report-outcomes) covers every case.
## Verify your integration [step] [#verify-your-integration-step]
Run one claim through your flow, then read the evaluation back with [`GET /v1/evaluations/{id}`](/docs/api-reference/evaluations#retrieve-an-evaluation), or `abusezero evaluations get` in the [AbuseZero CLI](/docs/tools/cli#cli-command-reference).
```bash
curl https://api.abusezero.com/v1/evaluations/eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93 \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
Check three things in the response:
- `result` is `allow` and `outcome.state` is `granted`.
- [`inputs`](/docs/api-reference/evaluations#inputs) shows `received` for every identifier you meant to send.
- `policy.version` shows the exact [policy version](/docs/concepts/policies-and-rules) the claim ran under.
Then claim the same trial again for the same user with a new `claim_id`. The second evaluation is denied, with a reason such as [`prior_claim_user`](/docs/reason-codes#benefit-history).
Add device and automation evidence to every evaluation.
Send payment, phone and custom identifiers.
Preview claims and replay policy changes before they go live.
Everything to check before real users arrive.
# Report outcomes (/docs/guides/report-outcomes)
An outcome records what happened after an `allow`: whether you granted the [benefit](/docs/concepts/benefits), didn't, or took it back. Outcomes become the history that every later evaluation is checked against, so later decisions rest on what really happened. For the full model, see [Outcomes](/docs/concepts/outcomes).
## Before you begin [#before-you-begin]
- A benefit and a server that evaluates claims. See [Protect a benefit end to end](/docs/guides/protect-a-benefit).
- A server key with the [scopes](/docs/api-reference/authentication#scopes) `outcomes:write`, `outcomes:read` and `benefits:write`.
## Choose how the benefit tracks outcomes [step] [#choose-how-the-benefit-tracks-outcomes-step]
Two benefit settings decide how outcomes are recorded. Set them when you [create the benefit](/docs/api-reference/benefits#create-a-benefit), or change them on the **Benefits** page or with [`PATCH /v1/benefits/{id}`](/docs/api-reference/benefits#update-a-benefit). Both are part of the benefit's [configuration](/docs/api-reference/benefits#benefit-configuration).
| Setting | Value | Effect |
|---|---|---|
| `outcome_tracking` | `assume_granted` (default) | Every allowed claim counts as granted straight away. Its outcome starts as `assumed_granted` (see [outcome states](/docs/api-reference/outcomes#outcome-states)). You report only exceptions. |
| | `report_outcome` | Every allowed claim waits for your report. Its outcome starts as `awaiting_outcome`, with a `due_at` time. |
| `missing_outcome_behavior` | `hold_eligibility` (default) | If no report arrives by `due_at`, the outcome becomes `outcome_unknown` and still counts as claimed. |
| | `release_eligibility` | If no report arrives by `due_at`, the outcome becomes `outcome_unknown` and stops counting, so the user can claim again. |
Use `report_outcome` when granting can fail after the evaluation, for example when a trial needs a card check. Use `assume_granted` when an `allow` always leads to a grant.
## Report a granted benefit [step] [#report-a-granted-benefit-step]
After you grant the benefit, report `granted` before `outcome.due_at`, with the SDK's [`outcome()`](/docs/tools/server-sdk#report-an-outcome) or [`POST /v1/evaluations/{id}/outcome`](/docs/api-reference/outcomes#report-an-outcome). The [AbuseZero CLI](/docs/tools/cli#cli-command-reference) command is `abusezero outcomes report`, and the [AbuseZero MCP server](/docs/tools/mcp#mcp-tools) tool is `abusezero_report_outcome`. Add `external_outcome_id` if you want to link the outcome to your own record.
```ts tab="Node.js"
import { abusezero } from "./lib/abusezero";
const report = await abusezero.outcome(evaluationId, {
outcome: "granted",
external_outcome_id: "trial_8f2d41",
}, `outcome:${evaluationId}:granted`);
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/evaluations/eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93/outcome \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: outcome:eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93:granted" \
-d '{ "outcome": "granted", "external_outcome_id": "trial_8f2d41" }'
```
```json title="Response"
{
"object": "outcome_report",
"evaluation": "eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93",
"state": "granted",
"conflicts": []
}
```
Reporting the same outcome again changes nothing, so a retry with the same [`Idempotency-Key`](/docs/api-reference#idempotency) is always safe.
## Report a benefit you didn't grant [step] [#report-a-benefit-you-didnt-grant-step]
If the claim was allowed but you didn't grant the benefit, report `not_granted` with a `reason` of 3 to 500 characters. The claim stops counting, so the user can claim the benefit again.
```ts
await abusezero.outcome(evaluationId, {
outcome: "not_granted",
reason: "Card check failed before the trial started.",
}, `outcome:${evaluationId}:not_granted`);
```
## Report a reversal [step] [#report-a-reversal-step]
If you granted the benefit and later took it back, for example with a refund, report `reversed` with a `reason`. A reversed benefit still counts as received.
```ts
await abusezero.outcome(evaluationId, {
outcome: "reversed",
reason: "Refunded at the customer's request.",
}, `outcome:${evaluationId}:reversed`);
```
Each report moves the outcome from its current state:
| Report | Accepted when the outcome is |
|---|---|
| `granted` | `awaiting_outcome`, `outcome_unknown` or `assumed_granted` |
| `not_granted` | `awaiting_outcome`, `outcome_unknown` or `assumed_granted` |
| `reversed` | `granted` or `assumed_granted` |
Any other change returns [`OUTCOME_CONFLICT`](/docs/troubleshooting#reporting-an-outcome-fails-with-outcome_conflict-409). Only allowed evaluations have outcomes, so a report for a denied evaluation also returns `OUTCOME_CONFLICT`. [Allowed changes](/docs/api-reference/outcomes#allowed-changes) lists every transition.
## Check for conflicts [step] [#check-for-conflicts-step]
A `granted` report can arrive after `due_at` on a benefit set to `release_eligibility`. By then, the same user may have claimed again. When that happens, [`conflicts`](/docs/api-reference/outcomes#conflicts) lists the outcomes (`out_…`) recorded meanwhile for the same identifiers.
```ts
if (report.conflicts.length > 0) {
await flagForReview(evaluationId, report.conflicts);
}
```
`flagForReview` stands for your own review process. Review each listed outcome and report `reversed` or `not_granted` where the benefit shouldn't stand.
## Verify your integration [step] [#verify-your-integration-step]
Read the outcomes for your workspace with [`GET /v1/outcomes`](/docs/api-reference/outcomes#list-outcomes), `abusezero outcomes list` in the [AbuseZero CLI](/docs/tools/cli#cli-command-reference), or `abusezero_list_outcomes` on the [AbuseZero MCP server](/docs/tools/mcp#mcp-tools). Outcomes still in `awaiting_outcome` near their `due_at`, or already in `outcome_unknown`, mean reports aren't arriving.
```ts tab="Node.js"
const outcomes = await abusezero.outcomes({ limit: 20 });
```
```bash tab="cURL"
curl "https://api.abusezero.com/v1/outcomes?limit=20" \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
```json title="Response (abridged)"
{
"object": "list",
"data": [
{
"object": "outcome",
"id": "out_9a7b6c5d-4e3f-4a2b-8c1d-0e9f8a7b6c5d",
"evaluation": "eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93",
"state": "granted",
"source": "evaluation",
"occurred_at": "2026-10-29T15:01:12.000Z",
"due_at": "2026-10-29T15:10:00.000Z"
}
],
"next_cursor": null
}
```
Bring in benefits you granted before AbuseZero.
Every outcome field and state.
# Send identifiers (/docs/guides/send-identifiers)
Every identifier you send is checked against your [workspace](/docs/keys-and-access)'s full claim history on every evaluation. The more you send, the more connections AbuseZero can see. Phone numbers and custom identifiers travel as keyed hashes, so AbuseZero matches them without ever receiving the raw value. For how history and matching fit together, see [Evaluations](/docs/concepts/evaluations) and [Custom identifiers](/docs/concepts/custom-identifiers).
## What you can send [#what-you-can-send]
| Field | What it is |
|---|---|
| `subject.email` | Required. The email address on the account making the claim. |
| `subject.external_id` | Your user ID for the account. |
| `subject.email_verified` | `true` only when your app has verified that the person owns the email address. |
| `context.ip` | The visitor's IP address, IPv4 or IPv6. |
| `context.user_agent` | The visitor's user agent string. |
| `context.session_id` | Your session ID for the visit. |
| `identifiers.device_token` | The value the browser SDK returns. See [Set up browser protection](/docs/guides/browser-setup). |
| `identifiers.device_id` | Your own device identifier, such as a mobile app installation ID. |
| `identifiers.browser_cookie_id` | Your own first-party browser cookie ID. |
| `identifiers.payment` | A payment method fingerprint from your payment processor. |
| `identifiers.phone_hash` and `identifiers.phone_verified` | A keyed hash of the phone number, and whether your app verified it. |
| `identifiers.custom` | Up to 10 hashed custom identifiers that you registered first. |
Field limits are in the [Evaluations reference](/docs/api-reference/evaluations#create-an-evaluation). Every field goes into [`evaluate()`](/docs/tools/server-sdk#evaluate-a-claim) in the AbuseZero Node.js SDK, or the body of [`POST /v1/evaluations`](/docs/api-reference/evaluations#create-an-evaluation).
## Send the user and email [step] [#send-the-user-and-email-step]
Send the account's user ID and email with every claim. Set `email_verified` from your own records. AbuseZero doesn't verify ownership itself.
```ts
subject: {
external_id: "user_4821",
email: "jordan@example.com",
email_verified: true,
},
```
## Send the IP address [step] [#send-the-ip-address-step]
Send the visitor's IP address as your trusted proxy chain reports it, not a header the visitor could set. AbuseZero accepts IPv4 and IPv6. Private, loopback, documentation and other non-routable addresses return [`IP_NOT_ROUTABLE`](/docs/troubleshooting#ip_not_routable-422). If you don't have the address, leave `context.ip` out. The evaluation still runs.
```ts
context: {
ip: clientIp,
user_agent: request.headers.get("user-agent") ?? undefined,
},
```
`clientIp` stands for the address your proxy setup gives you.
## Send a payment fingerprint [step] [#send-a-payment-fingerprint-step]
If the claim involves a payment method, send the stable fingerprint your payment processor assigns to it, taken from your processor's server-side data. Never send card numbers or other card details.
| Field | Value |
|---|---|
| `provider` | A name for your payment processor. Keep it the same for every claim. |
| `scope` | The processor account that issued the fingerprint. |
| `fingerprint` | The processor's fingerprint for the payment method. |
```ts
identifiers: {
payment: {
provider: "card_processor",
scope: "merchant_2041",
fingerprint: "fp_7Qk2mV0aZ",
},
},
```
Fingerprints match only within the same `provider` and `scope`.
## Hash and send a phone number [step] [#hash-and-send-a-phone-number-step]
Normalize the number to E.164 format, then hash it on your server with a secret that only you hold, using the AbuseZero Node.js SDK's [hashing helpers](/docs/tools/server-sdk#hash-phone-numbers-and-custom-identifiers). Send `phone_verified` with every phone hash.
```ts
import { createPhoneHash } from "abusezero/server";
const phoneHash = createPhoneHash({
secret: process.env.IDENTIFIER_HASH_SECRET!,
keyVersion: "v1",
e164: "+14155550123",
});
const identifiers = {
phone_hash: phoneHash,
phone_verified: true,
};
```
`createPhoneHash` returns the object AbuseZero expects:
```json
{
"algorithm": "hmac-sha256",
"key_version": "v1",
"value": "55e75c4c6e0f0cbd0b10441559a3cb28f99c1c04837ccbf9ceea1d127448a3cc"
}
```
`keyVersion` is your own label for the secret. Set `phone_verified` to `true` only when your app has verified the number. A `phone_verified` without a `phone_hash`, or a hash without `phone_verified`, is rejected.
## Register and send a custom identifier [step] [#register-and-send-a-custom-identifier-step]
[Custom identifiers](/docs/concepts/custom-identifiers) are business identifiers of your own, such as a license or tax ID. Register each key and key version once, in **Developers → Custom identifiers**, with [`POST /v1/custom-identifiers`](/docs/api-reference/custom-identifiers#register-a-custom-identifier) in the AbuseZero REST API, with `abusezero custom-identifiers register` in the [AbuseZero CLI](/docs/tools/cli#cli-command-reference), or with `abusezero_register_custom_identifier` on the [AbuseZero MCP server](/docs/tools/mcp#mcp-tools).
```ts tab="Node.js"
await abusezero.registerCustomIdentifier({
key: "license_id",
key_version: "v1",
description: "Software license ID",
}, "register-license-id-v1");
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/custom-identifiers \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: register-license-id-v1" \
-d '{ "key": "license_id", "key_version": "v1", "description": "Software license ID" }'
```
Then hash each value and send up to 10 custom identifiers with an evaluation:
```ts
import { createCustomIdentifier } from "abusezero/server";
const identifiers = {
custom: [
createCustomIdentifier({
secret: process.env.IDENTIFIER_HASH_SECRET!,
keyVersion: "v1",
key: "license_id",
value: "LIC-20931",
}),
],
};
```
A key or key version that isn't registered returns [`UNSUPPORTED_FIELD`](/docs/troubleshooting#invalid_field-unsupported_field-or-malformed_json). Once registered, a custom identifier can also be used in your own [policy rules](/docs/concepts/policies-and-rules).
## Hash identifiers in other languages [#hash-identifiers-in-other-languages]
If your server doesn't run Node.js, compute the same values yourself: an HMAC-SHA256 of a fixed message, in lowercase hexadecimal.
| Identifier | Message to hash |
|---|---|
| Phone | `abusezero:phone:e164:v1:` followed by the E.164 number |
| Custom | `abusezero:custom:` followed by the key, then `:v1:`, then the value |
In Node.js without the SDK helpers, that is:
```ts
import { createHmac } from "node:crypto";
const secret = process.env.IDENTIFIER_HASH_SECRET!;
const hmac = (message: string) => createHmac("sha256", secret).update(message, "utf8").digest("hex");
const phoneHash = {
algorithm: "hmac-sha256",
key_version: "v1",
value: hmac("abusezero:phone:e164:v1:+14155550123"),
};
const license = {
key: "license_id",
value_hash: {
algorithm: "hmac-sha256",
key_version: "v1",
value: hmac("abusezero:custom:license_id:v1:LIC-20931"),
},
};
```
The `v1` inside each message is part of the format. It doesn't change when you change your own `key_version`.
Keep the same secret and key version for as long as you want new claims to match earlier ones. Values hashed with a new secret, or sent with a new key version, don't match earlier values, so either change starts a new history for that identifier.
## Verify your integration [step] [#verify-your-integration-step]
Run a claim with every identifier you send, then open the evaluation with [`GET /v1/evaluations/{id}`](/docs/api-reference/evaluations#retrieve-an-evaluation). Its [`inputs`](/docs/api-reference/evaluations#inputs) object reports each input as `received` or `missing`.
```json title="inputs"
{
"email": "received",
"ip": "received",
"user_id": "received",
"phone": "received",
"payment": "received",
"device": "received",
"browser": "received"
}
```
Any input you meant to send that shows `missing` didn't arrive. Check the field name and where your code sets it. [Risk Score and signals](/docs/concepts/risk-score-and-signals) explains each input.
How custom identifiers extend matching to your own business data.
Every request field, type and limit.
# Test changes safely (/docs/guides/test-changes-safely)
A preview runs the same evaluation as live traffic without recording a claim. Replays and simulations reuse the evidence your past evaluations retained, so a changed result comes from your policy change. This guide takes one policy change from draft to live: preview a claim, replay the change against recent traffic, simulate it in sequence, observe it on real claims, then switch it on. For how policies work, see [Policies and rules](/docs/concepts/policies-and-rules).
## Before you begin [#before-you-begin]
- A [benefit](/docs/concepts/benefits) with some evaluation history. See [Protect a benefit end to end](/docs/guides/protect-a-benefit).
- A server key with the [scopes](/docs/api-reference/authentication#scopes) `simulations:run`, `policies:read`, `policies:write`, `benefits:read`, `benefits:write` and `evaluations:read`.
## Preview a single claim [step] [#preview-a-single-claim-step]
A preview runs a claim through the full evaluation and returns the result, without creating a claim, an evaluation or an outcome. It takes the same input as an evaluation and adds the [inputs](/docs/concepts/risk-score-and-signals#inputs) it received to the response. Use the SDK's [`previewEvaluation()`](/docs/tools/server-sdk#preview-an-evaluation) or [`POST /v1/evaluation-previews`](/docs/api-reference/previews-simulations-replays#preview-an-evaluation). The [AbuseZero CLI](/docs/tools/cli#cli-command-reference) command is `abusezero evaluations preview`, and the [AbuseZero MCP server](/docs/tools/mcp#mcp-tools) tool is `abusezero_preview_evaluation`.
```ts tab="Node.js"
import { abusezero } from "./lib/abusezero";
const preview = await abusezero.previewEvaluation({
benefit: "pro_trial",
claim_id: "preview_0001",
subject: { external_id: "user_4821", email: "jordan@example.com", email_verified: true },
}, "preview_0001");
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/evaluation-previews \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: preview_0001" \
-d '{
"benefit": "pro_trial",
"claim_id": "preview_0001",
"subject": { "external_id": "user_4821", "email": "jordan@example.com", "email_verified": true }
}'
```
```json title="Response"
{
"object": "evaluation_preview",
"preview": true,
"result": "allow",
"risk": { "score": 20, "band": "low" },
"reason": {
"code": "default_allow",
"message": "No rule matched, so the policy's default allowed this claim."
},
"inputs": {
"email": "received",
"ip": "missing",
"user_id": "received",
"phone": "missing",
"payment": "missing",
"device": "missing",
"browser": "missing"
}
}
```
Previews use credits like evaluations, and so do runs in the **Test console** under **Developers**, which previews claims from the dashboard. Both have their own rate limit, separate from evaluations; over it, they return `429` `RATE_LIMITED` with a `Retry-After` header.
## Draft the policy change [step] [#draft-the-policy-change-step]
Create the change as a new [policy](/docs/concepts/policies-and-rules) with [`POST /v1/policies`](/docs/api-reference/policies#create-a-policy), `abusezero policies create` in the [AbuseZero CLI](/docs/tools/cli#cli-command-reference), or `abusezero_create_policy` on the [AbuseZero MCP server](/docs/tools/mcp#mcp-tools). A policy affects no claims until a benefit uses it, so you can test it freely. This one builds on the Balanced [protection level](/docs/concepts/protection-levels) and adds one [rule](/docs/api-reference/policies#rules) of your own.
```ts tab="Node.js"
await abusezero.createPolicy({
name: "Balanced with device limit",
key: "balanced_device_limit",
definition: {
base: "balanced",
default_result: "allow",
rules: [
{
id: "repeat_device",
name: "Repeat device",
result: "deny",
when: { signal: "device.prior_claims", operator: "gte", value: 2 },
},
],
},
}, "create-policy-balanced-device-limit");
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/policies \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: create-policy-balanced-device-limit" \
-d '{
"name": "Balanced with device limit",
"key": "balanced_device_limit",
"definition": {
"base": "balanced",
"default_result": "allow",
"rules": [{
"id": "repeat_device",
"name": "Repeat device",
"result": "deny",
"when": { "signal": "device.prior_claims", "operator": "gte", "value": 2 }
}]
}
}'
```
[`GET /v1/policy-signals`](/docs/api-reference/policies#list-conditions) lists every condition you can use in a rule. To start from a policy you already have, duplicate it instead with [`POST /v1/policies/{id}/duplicate`](/docs/api-reference/policies#duplicate-a-policy).
## Test against past traffic [step] [#test-against-past-traffic-step]
A replay runs your benefit's recent evaluations through the draft policy and shows which results would change. On the dashboard, it's **Test against past traffic** on the **Policies** page. The REST API call is [`POST /v1/replays`](/docs/api-reference/previews-simulations-replays#run-a-replay); the [AbuseZero CLI](/docs/tools/cli#cli-command-reference) command is `abusezero simulations replay`, and the [AbuseZero MCP server](/docs/tools/mcp#mcp-tools) tool is `abusezero_replay_evaluations`.
```ts tab="Node.js"
const replay = await abusezero.replay({
benefit_id: "ben_6f1d2c3b-4a5e-4f60-8b7c-9d0e1f2a3b4c",
policy_key: "balanced_device_limit",
});
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/replays \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "benefit_id": "ben_6f1d2c3b-4a5e-4f60-8b7c-9d0e1f2a3b4c", "policy_key": "balanced_device_limit" }'
```
```json title="Response (abridged)"
{
"object": "replay",
"limit": 500,
"data": [
{
"evaluation": "eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93",
"original": "allow",
"replayed": "deny",
"changed": true
}
]
}
```
`limit` is how many recent evaluations the replay covers. Count the entries where `changed` is `true`, then open a few of those evaluations to check that the new results are the ones you want.
## Simulate the change in sequence [step] [#simulate-the-change-in-sequence-step]
A replay checks each evaluation on its own. A simulation runs your recent claims in order, starting from an empty history, as if they had arrived under the draft policy. Each claim it allows counts toward the ones after it. The REST API call is [`POST /v1/simulations`](/docs/api-reference/previews-simulations-replays#run-a-simulation); the [AbuseZero CLI](/docs/tools/cli#cli-command-reference) command is `abusezero simulations run`, and the [AbuseZero MCP server](/docs/tools/mcp#mcp-tools) tool is `abusezero_simulate_sequence`.
```ts tab="Node.js"
const simulation = await abusezero.simulate({
benefit_id: "ben_6f1d2c3b-4a5e-4f60-8b7c-9d0e1f2a3b4c",
policy_key: "balanced_device_limit",
});
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/simulations \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "benefit_id": "ben_6f1d2c3b-4a5e-4f60-8b7c-9d0e1f2a3b4c", "policy_key": "balanced_device_limit" }'
```
```json title="Response (abridged)"
{
"object": "simulation",
"data": [
{
"step": 1,
"evaluation": "eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93",
"result": "allow",
"reason": {
"code": "default_allow",
"message": "No rule matched, so the policy's default allowed this claim."
}
}
]
}
```
Replays and simulations reuse evaluations you already have. They don't create new evaluations and don't use credits.
## Run in observe mode [step] [#run-in-observe-mode-step]
To see the change on live traffic before it decides anything, move the benefit to the draft policy in [observe mode](/docs/api-reference/benefits#protection-mode), with [`PATCH /v1/benefits/{id}`](/docs/api-reference/benefits#update-a-benefit). In observe mode, every evaluation returns `allow`, and each evaluation records what the policy would have decided as `observed_result`.
```ts title="lib/save-benefit.ts"
import { abusezero } from "./abusezero";
type Benefit = {
name: string;
policy: string;
mode: "protect" | "observe";
revision: number;
config: Record;
};
export async function saveBenefit(
benefitId: string,
changes: { policy_key?: string; protection_mode?: "protect" | "observe" },
) {
const benefit = await abusezero.getBenefit(benefitId) as Benefit;
const policy = await abusezero.getPolicy(benefit.policy) as { key: string };
const config = Object.fromEntries(Object.entries(benefit.config).filter(([, value]) => value !== null));
return abusezero.updateBenefit(benefitId, {
name: benefit.name,
config,
policy_key: changes.policy_key ?? policy.key,
protection_mode: changes.protection_mode ?? benefit.mode,
revision: benefit.revision,
}, `benefit:${benefitId}:revision:${benefit.revision}`);
}
```
```ts
await saveBenefit("ben_6f1d2c3b-4a5e-4f60-8b7c-9d0e1f2a3b4c", {
policy_key: "balanced_device_limit",
protection_mode: "observe",
});
```
In observe mode, every claim is allowed. Keep the benefit in observe mode only as long as you need to compare results. Evaluations in observe mode are real evaluations and use credits.
## Switch to protect mode [step] [#switch-to-protect-mode-step]
When the observed results match what you expect, move the benefit back to protect mode.
```ts
await saveBenefit("ben_6f1d2c3b-4a5e-4f60-8b7c-9d0e1f2a3b4c", {
protection_mode: "protect",
});
```
## Verify your integration [step] [#verify-your-integration-step]
Open an evaluation created after the switch with [`GET /v1/evaluations/{id}`](/docs/api-reference/evaluations#retrieve-an-evaluation).
```bash
curl https://api.abusezero.com/v1/evaluations/eval_7c2e9b4a-1d3f-4e5a-9b6c-8d7e6f5a4b3c \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
- `mode` is `protect`.
- `policy.id` is the draft policy's ID, and `policy.version` is its current version.
Every evaluation keeps the [policy version](/docs/api-reference/policies#list-policy-versions) it ran under, so evaluations from before the switch still show the earlier policy.
How custom rules combine with protection levels.
Every field in each request and response.
# Verify AI agents (/docs/guides/verify-ai-agents)
AI agents now sign up, start trials and claim credits on behalf of real people. AbuseZero recognizes agents that can prove who they are, so you can let the ones you trust through while all other automation stays under your [benefit](/docs/concepts/benefits)'s rules. Your server verifies the incoming request, and AbuseZero takes that result into account inside the evaluation. For the bigger picture, see [Bots and AI agents](/docs/concepts/bots-and-ai-agents).
## Before you begin [#before-you-begin]
- A server that already evaluates claims. See [Protect a benefit end to end](/docs/guides/protect-a-benefit).
- A server key with the [scopes](/docs/api-reference/authentication#scopes) `agents:read`, `benefits:read`, `benefits:write`, `policies:read` and `evaluations:create`.
- The [AbuseZero Node.js SDK](/docs/tools/server-sdk) in your server code. Verification runs in your backend, where the agent's request arrives.
## List recognized agents [step] [#list-recognized-agents-step]
Read the list with [`GET /v1/agents`](/docs/api-reference/agents#list-recognized-agents), `abusezero agents list` in the [AbuseZero CLI](/docs/tools/cli#cli-command-reference), or `abusezero_list_agents` on the [AbuseZero MCP server](/docs/tools/mcp#mcp-tools).
```ts tab="Node.js"
import { abusezero } from "./lib/abusezero";
const agents = await abusezero.agents();
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/agents \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
The response is a list of [`agent` objects](/docs/api-reference/agents#the-agent-object):
| Field | Type | Description |
|---|---|---|
| `object` | `"agent"` | The object type. |
| `id` | string | The value you use in a benefit's `allowed_agents`. |
| `name` | string | The agent's display name. |
AbuseZero maintains this list.
## Allow the agent on a benefit [step] [#allow-the-agent-on-a-benefit-step]
Add the agent's `id` to the benefit's [`allowed_agents`](/docs/api-reference/benefits#automation) setting, on the **Benefits** page of the dashboard, with [`PATCH /v1/benefits/{id}`](/docs/api-reference/benefits#update-a-benefit) in the AbuseZero REST API, with `abusezero benefits update` in the [AbuseZero CLI](/docs/tools/cli#cli-command-reference), or with `abusezero_update_benefit` on the [AbuseZero MCP server](/docs/tools/mcp#mcp-tools). The helper below reads the benefit and saves it with its current `revision`. It's the same helper as in [Add a bot check](/docs/guides/bot-check).
```ts title="lib/update-benefit-config.ts"
import { abusezero } from "./abusezero";
type Benefit = {
name: string;
policy: string;
mode: "protect" | "observe";
revision: number;
config: Record;
};
export async function updateBenefitConfig(benefitId: string, changes: Record) {
const benefit = await abusezero.getBenefit(benefitId) as Benefit;
const policy = await abusezero.getPolicy(benefit.policy) as { key: string };
const config = Object.fromEntries(Object.entries(benefit.config).filter(([, value]) => value !== null));
return abusezero.updateBenefit(benefitId, {
name: benefit.name,
policy_key: policy.key,
protection_mode: benefit.mode,
config: { ...config, ...changes },
revision: benefit.revision,
}, `benefit:${benefitId}:revision:${benefit.revision}`);
}
```
```ts
await updateBenefitConfig("ben_6f1d2c3b-4a5e-4f60-8b7c-9d0e1f2a3b4c", {
allowed_agents: [agentId],
});
```
`agentId` is the `id` of the agent you chose from the list.
On this benefit, an allowed agent isn't denied or scored as automation, and it doesn't need a [bot check](/docs/guides/bot-check). Recognized agents you don't allow are treated like any other automation, under the benefit's [`automation`](/docs/api-reference/benefits#automation) setting.
## Verify the incoming request [step] [#verify-the-incoming-request-step]
In the route that receives the claim, pass the incoming request to [`verifyAgentRequest`](/docs/tools/server-sdk#verify-ai-agents) with the claim's ID and your app's public origin. It returns an opaque `AgentEvidence` token for that request, which AbuseZero verifies during the evaluation.
```ts title="app/api/trial/route.ts"
import { verifyAgentRequest } from "abusezero/server";
const PUBLIC_ORIGIN = "https://app.example.com";
export async function POST(request: Request) {
const body = await request.clone().json() as { claimId: string; email: string };
const agentEvidence = await verifyAgentRequest(request, body.claimId, PUBLIC_ORIGIN);
}
```
The next step completes this route. Three rules keep verification accurate:
- **Pass the original request.** Call it before any proxy, middleware or framework code rewrites the request's headers or body.
- **Use a configured origin.** `PUBLIC_ORIGIN` is your app's public HTTPS origin, set in your configuration. Never build it from a forwarded header.
- **Use the evaluation's claim ID.** The evidence is bound to that `claim_id`.
`verifyAgentRequest` takes a standard Fetch API `Request`. Route handlers in many web frameworks receive one directly. If your framework gives you a Node.js request object instead, build a `Request` from the original method, full URL and headers.
## Send the evidence with the evaluation [step] [#send-the-evidence-with-the-evaluation-step]
Pass the result unchanged as `agent_evidence` in [`evaluate()`](/docs/tools/server-sdk#evaluate-a-claim) or [`POST /v1/evaluations`](/docs/api-reference/evaluations#create-an-evaluation), in the same request handler, right after verification.
```ts title="app/api/trial/route.ts"
import { verifyAgentRequest } from "abusezero/server";
import { abusezero } from "@/lib/abusezero";
const PUBLIC_ORIGIN = "https://app.example.com";
export async function POST(request: Request) {
const body = await request.clone().json() as { claimId: string; email: string };
const agentEvidence = await verifyAgentRequest(request, body.claimId, PUBLIC_ORIGIN);
const evaluation = await abusezero.evaluate({
benefit: "pro_trial",
claim_id: body.claimId,
subject: { email: body.email },
agent_evidence: agentEvidence,
}, `evaluate:${body.claimId}`);
return Response.json({ evaluation });
}
```
Send the evidence for every claim, whether or not an agent made the request. Your route keeps one code path. Evidence for a different `claim_id` returns `AGENT_EVIDENCE_SCOPE`, and evidence from a Node.js SDK before 0.11.0 returns `AGENT_EVIDENCE_OUTDATED` (both [invalid request](/docs/api-reference/errors#invalid-request)).
## Verify your integration [step] [#verify-your-integration-step]
Send a claim through the route from an ordinary browser. The evaluation runs exactly as before.
Then send a claim from an allowed agent. On a benefit with `automation` set to `deny` or `bot_evidence` set to `required`, the agent's claim is evaluated on your policy like a person's claim: it isn't denied with the [reason](/docs/reason-codes) `automation`, and it doesn't fail with [`BOT_EVIDENCE_REQUIRED`](/docs/troubleshooting#bot_evidence_required-422).
How AbuseZero tells legitimate agents from abusive automation.
`verifyAgentRequest` and every other server function.
# How AbuseZero works (/docs/how-it-works)
Every protected claim follows the same lifecycle. You define the benefit once. Your server evaluates each claim, acts on the result and, when you choose, reports what happened. Every outcome becomes history that later evaluations are checked against.
```mermaid
flowchart LR
A[Benefit settings] --> B[AbuseZero]
C[Your app] -->|Evaluate the claim| B
B -->|allow or deny| C
C -->|Report the outcome| B
```
## Benefits [#benefits]
A [benefit](/docs/concepts/benefits) is anything you give out on a limited basis: a free trial, a free plan, sign-up credits or a promotional offer. Create benefits in the dashboard, with [`POST /v1/benefits`](/docs/api-reference/benefits#create-a-benefit), with `abusezero benefits create` in the [AbuseZero CLI](/docs/tools/cli#cli-command-reference), or with `abusezero_create_benefit` on the [AbuseZero MCP server](/docs/tools/mcp#mcp-tools). Each benefit has its own settings:
- **[Eligibility](/docs/api-reference/benefits#eligibility):** once per user, once per calendar day, week or month, or once per rolling number of days.
- **Policy:** an AbuseZero [protection level](/docs/concepts/protection-levels), or your own [custom rules](/docs/concepts/policies-and-rules) built on one.
- **[Mode](/docs/concepts/benefits#protect-and-observe):** protect, where results are applied, or observe, where AbuseZero records what it would have decided while every claim is allowed.
- **Outcome tracking:** count allowed claims as granted automatically, or report each outcome yourself. See [tracking models](/docs/concepts/outcomes#tracking-models).
- **Automation and AI agents:** how automated traffic is treated, and which [recognized agents](/docs/concepts/bots-and-ai-agents) may claim the benefit.
See [Benefits](/docs/concepts/benefits).
## Evaluations [#evaluations]
When someone claims a benefit, your server calls [`POST /v1/evaluations`](/docs/api-reference/evaluations#create-an-evaluation), or [`evaluate()`](/docs/tools/server-sdk#evaluate-a-claim) in the AbuseZero Node.js SDK, with the benefit key, a claim ID, the user, and the [identifiers you have](/docs/guides/send-identifiers): IP address, [device token](/docs/concepts/browser-protection), phone, payment method and your own [custom identifiers](/docs/concepts/custom-identifiers).
Within that one call, AbuseZero checks every identifier against your workspace's full claim history, assesses five [signal families](/docs/concepts/risk-score-and-signals#signal-families) (identity, device, network, automation and benefit history), recognizes AI agents that can prove who they are, and applies your protection level, rules and lists. Evidence that is slow or unavailable doesn't hold the evaluation up. It finishes within the time limit with everything it has, and reports which [inputs](/docs/api-reference/evaluations#inputs) it received.
The response is one final result:
| Field | What it tells you |
|---|---|
| `result` | `allow` or `deny`. Your application acts on this. |
| `risk` | The [AbuseZero Risk Score](/docs/concepts/risk-score-and-signals#the-abusezero-risk-score) from 0 to 100, or `null` when no score was produced, and its band. |
| `reason` | A stable code and a readable message explaining the result. See [Reason codes](/docs/reason-codes). |
| `outcome` | The claim's [outcome state](/docs/api-reference/outcomes#outcome-states), when one applies. |
Every evaluation is stored with the exact [policy version](/docs/concepts/policies-and-rules#versions) it ran under. Open any past evaluation and you see the decision exactly as it was made, even after your rules have changed. See [Evaluations](/docs/concepts/evaluations).
## Outcomes and history [#outcomes-and-history]
A result is a decision. An [outcome](/docs/concepts/outcomes) is what actually happened: the benefit was granted, wasn't granted, or was granted and later reversed.
Outcomes are what later evaluations are checked against. A claim that was granted makes the same user, email, device, phone or payment method visible to every future evaluation of that benefit. Report outcomes with [`POST /v1/evaluations/{id}/outcome`](/docs/api-reference/outcomes#report-an-outcome) or [`outcome()`](/docs/tools/server-sdk#report-an-outcome) in the AbuseZero Node.js SDK. You can also [import your existing outcome history](/docs/guides/import-claim-history), so protection starts with everything you already know on day one. See [Report outcomes](/docs/guides/report-outcomes).
## What you control [#what-you-control]
| Control | What it does |
|---|---|
| [Protection levels](/docs/concepts/protection-levels) | Lenient, Balanced or Strict. AbuseZero defines and maintains each level. |
| [Custom rules](/docs/concepts/policies-and-rules) | Your own conditions, added to a protection level. |
| [Allow and block lists](/docs/concepts/allow-and-block-lists) | Always allow or always deny specific emails, domains, IP addresses, users or devices. |
| [Observe mode](/docs/concepts/benefits#protect-and-observe) | See what AbuseZero would decide, without affecting any user. |
| [Previews, simulations and replays](/docs/guides/test-changes-safely) | Test a change against a single request, a sequence of claims or your recent traffic before it goes live. |
[Previews](/docs/api-reference/previews-simulations-replays#preview-an-evaluation) run the full evaluation without recording a claim. [Simulations](/docs/api-reference/previews-simulations-replays#run-a-simulation) and [replays](/docs/api-reference/previews-simulations-replays#run-a-replay) apply a policy with the same evaluation engine as your live traffic, using what your past evaluations recorded.
## Integration options: REST API, Node.js SDK, CLI and MCP server [#integration-options-rest-api-nodejs-sdk-cli-and-mcp-server]
Every action is available through the [AbuseZero REST API](/docs/api-reference), the [AbuseZero Node.js SDK](/docs/tools/server-sdk), the [AbuseZero CLI](/docs/tools/cli) and the [AbuseZero MCP server](/docs/tools/mcp), with the same inputs and the same results. The [coverage table](/docs/tools/coverage) lists each action and where to find it. Browser data is the one exception: it comes only from the [AbuseZero browser SDK](/docs/tools/browser-sdk) on your pages.
## Next steps [#next-steps]
Protect your first benefit.
Choose the right credential for each part of your integration.
# Introduction (/docs)
AbuseZero is an abuse prevention API for SaaS. It stops people from creating extra accounts to claim your trials, free plans, credits and other [benefits](/docs/concepts/benefits) again and again.
Before you grant a benefit, your server sends AbuseZero the claim through the [AbuseZero REST API](/docs/api-reference/evaluations#create-an-evaluation) or the [AbuseZero Node.js SDK](/docs/tools/server-sdk#evaluate-a-claim). AbuseZero returns one final result, `allow` or `deny`, with the [AbuseZero Risk Score](/docs/concepts/risk-score-and-signals) and a [reason](/docs/reason-codes) you can show your team.
## How an evaluation works [#how-an-evaluation-works]
Your server makes one request. Behind it, on every evaluation, AbuseZero:
- checks every [identifier you send](/docs/guides/send-identifiers) against your workspace's full claim history;
- runs the claim through the [AbuseZero AI Risk Engine](/docs/ai-risk-engine);
- assesses five [signal families](/docs/concepts/risk-score-and-signals#signal-families): identity, device, network, automation and benefit history;
- tells legitimate AI agents apart from automation that hides what it is (see [Bots and AI agents](/docs/concepts/bots-and-ai-agents));
- applies your [protection level](/docs/concepts/protection-levels), [custom rules](/docs/concepts/policies-and-rules) and [allow and block lists](/docs/concepts/allow-and-block-lists);
- records the decision with the exact [policy version](/docs/concepts/policies-and-rules#versions) it ran under, so you can explain it later.
All of it finishes within the evaluation's time limit. When a source of evidence is slow, the evaluation doesn't wait for it. It returns on time and tells you which [inputs](/docs/concepts/risk-score-and-signals#inputs) it received.
```mermaid
flowchart LR
A[Your app] -->|Evaluate the claim| B[AbuseZero]
B -->|allow or deny| A
A -->|Report the outcome| B
```
## Benefits you can protect [#benefits-you-can-protect]
Free and discounted trials, including paid introductory offers. Free plans, where extra accounts
would multiply the free allowance. Sign-up credits, API credits, AI credits and free generations.
Promotional offers and any other limited benefit you choose to protect. Every benefit uses the same
evaluation, matching, eligibility and outcome model.
Your application keeps doing what it already does: authentication, payment, delivering the benefit and enforcing usage limits. After you grant a benefit, you can [report the outcome](/docs/concepts/outcomes). AbuseZero answers one question at the moment of the claim: should this claim receive the benefit?
## Get started [#get-started]
Protect your first benefit: evaluate a claim, add browser protection and report the outcome.
The lifecycle of a claim, from benefit to evaluation to outcome and history.
AI and a proprietary algorithm built on 140,000+ risk signal combinations.
What goes into an evaluation and everything that comes back.
A complete, production-ready integration, step by step.
The `abusezero` package: Node.js SDK, browser SDK, AbuseZero CLI and MCP server.
Every endpoint, field, sample and error.
## Markdown and llms.txt [#markdown-and-llmstxt]
Every page is available as Markdown. Add `.md` to any documentation address, or use the Markdown option at the top of each page. The complete documentation is also available at [`/llms.txt`](/llms.txt) and [`/llms-full.txt`](/llms-full.txt).
AI apps that support MCP can work with your workspace directly through the [AbuseZero MCP server](/docs/tools/mcp). For terminals and CI, use the [AbuseZero CLI](/docs/tools/cli). Common integration questions are answered in the [Developer FAQ](/docs/developer-faq).
# Keys and access (/docs/keys-and-access)
Every request to AbuseZero carries one credential, and every credential reaches only the workspaces it was granted. Choose the credential by where the code runs and what it needs to do.
## Workspaces [#workspaces]
Everything in AbuseZero belongs to a workspace: [benefits](/docs/concepts/benefits), [policies](/docs/concepts/policies-and-rules), [evaluations](/docs/concepts/evaluations), [outcomes](/docs/concepts/outcomes), [allow and block lists](/docs/concepts/allow-and-block-lists), keys and billing. Workspace IDs start with `ws_`. See [the workspace object](/docs/api-reference/workspaces#the-workspace-object).
Each workspace's data stays in that workspace. A credential can never be used to read or change another workspace, and no request parameter widens what a credential can reach.
Members, roles and permissions are managed in the dashboard under **Workspace settings → Members & access**.
## Credential types [#credential-types]
| Credential | Starts with | Runs in | Access |
|---|---|---|---|
| Secret key | `sk_` | Your server | Every server scope in one workspace. |
| Restricted key | `rk_` | Your server | Only the scopes you choose. You can change its scopes later. |
| Publishable key | `pk_` | Your pages | Browser collection only. |
| OAuth access token | | The [AbuseZero CLI](/docs/tools/cli), MCP clients and OAuth apps | The scopes the user approved, in the workspaces they can access. |
| [Machine client](#machine-clients) | | Scripts and CI | OAuth client credentials for one workspace. |
Keys are sent in the `Authorization` header:
```bash
Authorization: Bearer sk_…
```
The full list of [scopes](/docs/api-reference/authentication#scopes) and which endpoints need them is in [REST API authentication](/docs/api-reference/authentication#send-a-credential).
## Secret and restricted keys [#secret-and-restricted-keys]
Server keys are created in **Developers → API keys**.
- **Secret keys** have full access to the workspace. Use them where one service runs your whole integration.
- **Restricted keys** have only the scopes you choose. Use them to give each service exactly what it needs, such as `evaluations:create` and `outcomes:write` for the service that grants trials.
- A key's type and expiry are set when you create it and never change. Choose an expiry from 1 to 3,650 days, or none.
- When a key is revoked or expires, requests with it fail with `INVALID_API_KEY` or `API_KEY_EXPIRED` (see [REST API errors](/docs/api-reference/errors#authentication)). Create a new key and replace it in your environment.
Secret and restricted keys belong on your server only. Never put them in browser code, mobile
apps, URLs or logs.
### Key emails [#key-emails]
AbuseZero emails the workspace's owners and admins about every server key, so a key never stops working without warning.
| Email | Sent | Also sent to the key's creator |
|---|---|---|
| Key created | When a member creates a key. A secret key is marked as having full access. | No |
| Scopes changed | When a restricted key's scopes change. | No |
| Expires soon | 7 days before the key expires. A key used in the last 7 days also gets a reminder 14 days and 1 day before. | Yes |
| Expired | Within minutes of the key expiring. | Yes |
| Revoked | When the key is revoked. | Yes |
| Still in use | Once, when requests keep arriving with the key in the 7 days after it expired or was revoked. | Yes |
Each email names the key, shows its prefix and last four characters, and gives the exact time in UTC. The key's creator gets these emails while they're still a member of the workspace. The publishable key doesn't expire and can't be revoked, so it gets none of them.
## Publishable key [#publishable-key]
Your publishable key is for the [AbuseZero browser SDK](/docs/tools/browser-sdk) and the [standalone browser script](/docs/tools/browser-sdk#standalone-browser-script). It can only send browser data for your claims, so it is safe in your page source.
- Browser data is accepted only from HTTPS sites in your site list, under **Your sites** in **Developers → Browser setup**, or through [`PATCH /v1/browser-collection`](/docs/api-reference/browser-collection#update-the-site-list).
- The publishable key can't call server endpoints. A server request with it fails with `KEY_TYPE_NOT_ALLOWED` (see [Troubleshooting](/docs/troubleshooting#requests-fail-with-key_type_not_allowed-403)).
## OAuth [#oauth]
The [AbuseZero CLI](/docs/tools/cli), the [AbuseZero MCP server](/docs/tools/mcp) and other OAuth apps sign in with OAuth instead of a key. The user signs in to AbuseZero in the browser and approves the scopes the app asks for.
- An OAuth token acts as the user who approved it, with that user's role and permissions in each workspace.
- Requests made with an OAuth token choose the workspace with the `AbuseZero-Workspace` header (see [How requests choose a workspace](/docs/api-reference/workspaces#how-requests-choose-a-workspace)):
```bash
Authorization: Bearer
AbuseZero-Workspace: ws_1c9e7b42-5d3a-4f81-a6e2-0b7d94c3f518
```
- [`GET /v1/workspaces`](/docs/api-reference/workspaces#list-workspaces) lists the workspaces a token can use. A token granted for one workspace lists only that workspace.
- Apps you've authorized appear in **Developers → Connected apps**. Disconnecting an app revokes its access.
## Machine clients [#machine-clients]
Machine clients let the AbuseZero CLI run in scripts and CI without a person signing in (see [Use the CLI in scripts and CI](/docs/tools/cli#use-the-cli-in-scripts-and-ci)). Create one in **Developers → Connected apps**. It works only in the workspace it was created in.
```bash
export ABUSEZERO_CLIENT_ID="…"
export ABUSEZERO_CLIENT_SECRET="…"
abusezero benefits list
```
## Choosing a credential [#choosing-a-credential]
| You're building | Use |
|---|---|
| The server code that evaluates claims and reports outcomes, with the [AbuseZero Node.js SDK](/docs/tools/server-sdk) or the [AbuseZero REST API](/docs/api-reference) | A restricted key with `evaluations:create` and `outcomes:write`, or a secret key |
| [Browser protection](/docs/concepts/browser-protection) on your pages | Your publishable key |
| An internal tool that manages benefits and policies | A restricted key with the matching scopes |
| A script or CI job using the AbuseZero CLI | A machine client |
| An AI app working with your workspace | The [AbuseZero MCP server](/docs/tools/mcp#mcp-server-authentication), signed in with OAuth |
## Next steps [#next-steps]
Headers, scopes and authentication errors.
Use your first key to evaluate a claim.
# Quickstart (/docs/quickstart)
This guide takes you from an empty integration to a protected benefit. By the end, every claim on that benefit is evaluated by AbuseZero before your application grants it.
## Before you start [#before-you-start]
- An AbuseZero workspace. Sign in at [app.abusezero.com](https://app.abusezero.com).
- A benefit to protect. Create one in **Benefits**, with [`POST /v1/benefits`](/docs/api-reference/benefits#create-a-benefit) or with `abusezero benefits create` in the [AbuseZero CLI](/docs/tools/cli#cli-command-reference), and note its key, such as `pro_trial`. See [Benefits](/docs/concepts/benefits).
- Node.js 22 or later on your server, for the [AbuseZero Node.js SDK](/docs/tools/server-sdk). Any other language can call the [AbuseZero REST API](/docs/api-reference) directly.
Every evaluation is real and uses credits, including the ones you run while building. See [which requests use credits](/docs/developer-faq#credits). Credits and
plans are managed in **Workspace settings → Billing & Subscriptions**.
## Install the package [step] [#install-the-package-step]
```npm
npm install abusezero
```
The package has separate [entry points](/docs/tools#entry-points) for your server (`abusezero/server`) and your pages (`abusezero/browser`). Server code never loads in the browser.
## Create a secret key [step] [#create-a-secret-key-step]
In the dashboard, open **Developers → API keys** and create a secret key. Store it in your server's environment as `ABUSEZERO_SECRET_KEY`.
```bash title=".env"
ABUSEZERO_SECRET_KEY="sk_…"
```
Secret keys stay on your server. For keys limited to specific [scopes](/docs/api-reference/authentication#scopes), see [restricted keys](/docs/keys-and-access#secret-and-restricted-keys).
## Evaluate the claim [step] [#evaluate-the-claim-step]
Call AbuseZero from your server before you grant the benefit, with [`evaluate()`](/docs/tools/server-sdk#evaluate-a-claim) or [`POST /v1/evaluations`](/docs/api-reference/evaluations#create-an-evaluation). `claim_id` identifies this one award in your system, such as the trial's ID (see [Re-evaluating a `claim_id`](/docs/api-reference/evaluations#re-evaluating-a-claim_id)). The [idempotency key](/docs/api-reference#idempotency) makes the call safe to retry.
```ts tab="Node.js" title="server/start-trial.ts"
import { AbuseZero } from "abusezero/server";
const abusezero = new AbuseZero({
key: process.env.ABUSEZERO_SECRET_KEY!,
outage: "throw",
});
export async function startTrial(
user: { id: string; email: string; emailVerified: boolean },
trialId: string,
ip: string,
deviceToken: string,
) {
const evaluation = await abusezero.evaluate(
{
benefit: "pro_trial",
claim_id: trialId,
subject: { external_id: user.id, email: user.email, email_verified: user.emailVerified },
context: { ip },
identifiers: { device_token: deviceToken },
},
`evaluate-${trialId}`,
);
if ("status" in evaluation) {
// Skipped: no allow or deny was decided.
return { started: false };
}
if (evaluation.result === "deny") {
return { started: false };
}
// Grant the trial in your application here.
return { started: true };
}
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/evaluations \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: evaluate-trial_8f2d41" \
-d '{
"benefit": "pro_trial",
"claim_id": "trial_8f2d41",
"subject": { "external_id": "user_4821", "email": "jordan@example.com", "email_verified": true },
"context": { "ip": "203.0.113.42" },
"identifiers": { "device_token": "" }
}'
```
Replace the example IP address with your visitor's public address. `203.0.113.42` comes from a range reserved for documentation, and AbuseZero rejects documentation, private and other non-routable addresses with `IP_NOT_ROUTABLE`.
AbuseZero responds with the result:
```json
{
"object": "evaluation",
"id": "eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93",
"result": "allow",
"risk": { "score": 10, "band": "low" },
"reason": { "code": "default_allow", "message": "No rule matched, so the policy's default allowed this claim." },
"outcome": { "state": "assumed_granted", "due_at": null }
}
```
Branch on `result`, and keep the [`reason`](/docs/reason-codes) for your team. When the workspace can't run an evaluation, for example because it has no credits left, the response is `{ "object": "evaluation", "status": "skipped", "reason": "…" }` with no result. Handle it as its own branch. See [skipped evaluations](/docs/api-reference/evaluations#skipped-evaluations) and [Handle outages and retries](/docs/guides/outages-and-retries).
## Add browser protection [step] [#add-browser-protection-step]
[Browser protection](/docs/concepts/browser-protection) connects your pages to AbuseZero's device and automation intelligence. Load the [AbuseZero browser SDK](/docs/tools/browser-sdk) with your [publishable key](/docs/keys-and-access#publishable-key), collect a device token for the claim, and send it to your server with the claim.
```ts title="app/trial-form.ts"
import { init, observe } from "abusezero/browser";
init({ key: "pk_…" }); // once, when your app starts
const { deviceToken } = await observe({ key: "pk_…", claimId: trialId });
// Send deviceToken to your server with the claim, then pass it as identifiers.device_token.
```
- Use the same claim ID in the browser and on your server.
- Always send `deviceToken`, whatever its status. When there is no browser data, it carries the reason instead. See [unavailable reasons](/docs/tools/browser-sdk#unavailable-reasons).
- Add your site under **Your sites** in **Developers → Browser setup**, or with [`PATCH /v1/browser-collection`](/docs/api-reference/browser-collection#update-the-site-list). AbuseZero accepts browser data only from HTTPS sites in your site list. If your site sets a Content Security Policy, copy the sources shown there into it.
Your publishable key is in **Developers → API keys**. It only works for browser collection, so it is safe in your pages. For the complete setup, see [Set up browser protection](/docs/guides/browser-setup).
## Report the outcome [step] [#report-the-outcome-step]
By default, an allowed claim counts as granted automatically (`assumed_granted`). If your benefit is set to [report outcomes](/docs/concepts/outcomes#tracking-models), tell AbuseZero what happened after you acted on the result, with [`outcome()`](/docs/tools/server-sdk#report-an-outcome) or [`POST /v1/evaluations/{id}/outcome`](/docs/api-reference/outcomes#report-an-outcome):
```ts tab="Node.js"
await abusezero.outcome(
evaluation.id,
{ outcome: "granted" },
`outcome-${trialId}`,
);
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/evaluations/eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93/outcome \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: outcome-trial_8f2d41" \
-d '{ "outcome": "granted" }'
```
Reported outcomes become the claim history that later evaluations are checked against. See [Report outcomes](/docs/guides/report-outcomes).
## Verify your integration [step] [#verify-your-integration-step]
Open **Evaluations** in the dashboard, or [retrieve the evaluation](/docs/api-reference/evaluations#retrieve-an-evaluation) by its ID. Your evaluation appears with its result, reason, [AbuseZero Risk Score](/docs/concepts/risk-score-and-signals), policy version, the inputs it received and the [browser data status](/docs/concepts/browser-protection#browser-data-statuses).
- `inputs.device` shows `received` when the device token reached the evaluation.
- `browser_data` shows `collected` when the browser SDK is set up correctly.
If something is missing, see [Troubleshooting](/docs/troubleshooting#browser-data) and the [Developer FAQ](/docs/developer-faq).
## Next steps [#next-steps]
Production-ready handling for every result, skip and error.
Choose how strictly each benefit is protected.
Preview, simulate and replay before you change anything live.
Everything to confirm before you ship.
# Reason codes (/docs/reason-codes)
Every [evaluation](/docs/concepts/evaluations) explains its result with a [`reason`](/docs/api-reference/evaluations#reason). The reason has a stable `code` for your code and reports, and a readable `message` you can show your team.
```json
{
"result": "deny",
"reason": {
"code": "prior_claim_email",
"message": "An earlier claim used this email address."
}
}
```
When one of your [custom rules](/docs/concepts/policies-and-rules#rules) decided the result, the reason also carries `rule`, the ID of that rule.
Your application should act on `result`, which is always `allow` or `deny`. Use `reason.code` for
analytics, support tools and messages to your own team. Handle a code you don't recognize like any
other reason.
## Benefit history [#benefit-history]
These codes mean an earlier claim on the same [benefit](/docs/concepts/benefits) is linked to this one. They come from your workspace's claim history, which grows with every [outcome](/docs/concepts/outcomes) you [report](/docs/guides/report-outcomes) or [import](/docs/guides/import-claim-history).
| Code | Message | What it means |
|---|---|---|
| `prior_claim_user` | This user already received this benefit. | The same user, by your user ID (`subject.external_id`), already has a claim that counts toward eligibility. |
| `prior_claim_email` | An earlier claim used this email address. | An earlier claim used this email address. |
| `prior_claim_payment` | An earlier claim used this payment method. | An earlier claim used the same payment method. |
| `prior_claim_phone` | An earlier claim used this verified phone number. | An earlier claim used the same verified phone number. See [Hash and send a phone number](/docs/guides/send-identifiers#hash-and-send-a-phone-number). |
| `prior_claim_device` | This device is linked to earlier claims. | The device behind this claim is linked to earlier claims, through [browser protection](/docs/concepts/browser-protection) or the device ID you send. |
**Recommended action:** [open the evaluation](/docs/api-reference/evaluations#retrieve-an-evaluation) to see the policy version it ran under and the inputs it received, and its [association graph](/docs/concepts/association-graph) ([`GET /v1/evaluations/{id}/graph`](/docs/api-reference/evaluations#retrieve-an-evaluations-association-graph)) to see the earlier outcomes behind the denial. If this person should receive the benefit, add an [allow list](/docs/concepts/allow-and-block-lists) entry. If the earlier claim was never actually granted, [report its outcome](/docs/guides/report-outcomes#report-a-benefit-you-didnt-grant) as `not_granted` so it stops counting.
## Network Intelligence, Identity Intelligence and Automation Defense [#network-intelligence-identity-intelligence-and-automation-defense]
These codes mean AbuseZero's own assessment decided the result, under your [protection level](/docs/concepts/protection-levels), your policy's [risk threshold](/docs/concepts/risk-score-and-signals#the-abusezero-risk-score) or the benefit's [automation setting](/docs/api-reference/benefits#automation).
| Code | Message | What it means |
|---|---|---|
| `network_risk` | AbuseZero Network Intelligence flagged this connection. | The connection behind this claim was assessed as high risk. It shows in the `network` [signal family](/docs/concepts/risk-score-and-signals#signal-families). |
| `identity_risk` | AbuseZero Identity Intelligence flagged this email address. | The email address behind this claim was assessed as high risk. It shows in the `identity` signal family. |
| `repeated_attempts` | AbuseZero detected repeated claim attempts. | This claim is part of a pattern of repeated attempts. |
| `automation` | AbuseZero Automation Defense detected automated activity. | The claim came from automation that isn't allowed for this benefit. See [Bots and AI agents](/docs/concepts/bots-and-ai-agents). |
| `protection_level` | AbuseZero managed protection matched this claim. | Your protection level decided the result. |
| `risk_threshold` | The AbuseZero Risk Score reached this policy's threshold. | The claim's overall risk reached the threshold set for this policy. |
**Recommended action:** check the evaluation's [signal families](/docs/api-reference/evaluations#signals), in its evidence or the dashboard, to see which area was elevated. If your audience legitimately looks like this, for example a company that routes all traffic through one network, choose a [protection level](/docs/concepts/protection-levels) that fits it, add an [allow list](/docs/concepts/allow-and-block-lists) entry, or adjust your [custom rules](/docs/concepts/policies-and-rules). For `automation`, review the benefit's automation settings and [recognized agents](/docs/api-reference/agents#list-recognized-agents). [Test against past traffic](/docs/guides/test-changes-safely#test-against-past-traffic) before you change a live policy.
## Your lists and rules [#your-lists-and-rules]
| Code | Message | What it means |
|---|---|---|
| `allow_list` | An allow list entry matched this claim. | One of your [allow list](/docs/concepts/allow-and-block-lists) entries matched, so the claim was allowed. |
| `block_list` | A block list entry matched this claim. | One of your block list entries matched, so the claim was denied. |
| `custom_rule` | Your rule “…” matched this claim. | One of your [custom rules](/docs/concepts/policies-and-rules) decided the result. `rule` carries its ID. |
For `custom_rule`, the evaluation's details name the rule in the message. Responses from [`POST /v1/evaluations`](/docs/api-reference/evaluations#create-an-evaluation) and [previews](/docs/api-reference/previews-simulations-replays#preview-an-evaluation) carry the message "One of your rules matched this claim." with the same `rule` ID.
**Recommended action:** these results are exactly what you configured. Review the matching entry or rule under **Policies** in the dashboard, or through the [Allow and block lists](/docs/api-reference/access-lists) and [Policies](/docs/api-reference/policies) endpoints.
## Policy defaults [#policy-defaults]
| Code | Message | What it means |
|---|---|---|
| `default_allow` | No rule matched, so the policy's default allowed this claim. | Nothing in the policy matched, and the policy allows by default (`default_result`, see [Custom policy definition](/docs/api-reference/policies#custom-policy-definition)). |
| `default_deny` | No rule matched, so the policy's default denied this claim. | Nothing in the policy matched, and the policy denies by default. |
## Repeat requests [#repeat-requests]
| Code | Message | What it means |
|---|---|---|
| `previously_authorized` | This claim was already approved. | This claim ID was already allowed for this user. The original allowed result is returned instead of a new decision. See [Re-evaluating a `claim_id`](/docs/api-reference/evaluations#re-evaluating-a-claim_id). |
**Recommended action:** nothing, if you're retrying the same award. To evaluate a new award, use a new claim ID.
## Observe mode [#observe-mode]
When a benefit is in [observe mode](/docs/concepts/benefits#protect-and-observe), every claim is allowed, and `reason` explains what the policy decided. The evaluation records that decision as `observed_result`, so you can review it in the dashboard or with [`GET /v1/evaluations/{id}`](/docs/api-reference/evaluations#retrieve-an-evaluation) before you [switch the benefit to protect mode](/docs/guides/test-changes-safely#switch-to-protect-mode).
## Related [#related]
- [Evaluations](/docs/concepts/evaluations)
- [Troubleshooting](/docs/troubleshooting#unexpected-denials)
- [Developer FAQ](/docs/developer-faq)
- [REST API errors](/docs/api-reference/errors), for requests that fail instead of returning a result
# Troubleshooting (/docs/troubleshooting)
Start with the evaluation itself. Open it in **Evaluations** in the dashboard, or read it with [`GET /v1/evaluations/{id}`](/docs/api-reference/evaluations#retrieve-an-evaluation). The evaluation shows which inputs it received, the browser data status, the policy version it ran under and the reason for its result. Most integration problems are visible there.
Every response carries an [`X-Request-ID`](/docs/api-reference#request-ids) header, and every error repeats it as `request_id`. Record it with your own logs for every failed request. Evaluation requests appear in [request logs](/docs/api-reference/request-logs) ([`GET /v1/request-logs`](/docs/api-reference/request-logs#list-request-logs), or `abusezero request-logs list` with the [AbuseZero CLI](/docs/tools/cli#cli-command-reference)), including failed ones with their status and error `code`, with the `X-Request-ID` as the log ID after `req_`. Requests that never authenticated and rate-limited requests aren't logged.
## Authentication and access [#authentication-and-access]
### Requests fail with `INVALID_API_KEY` or `API_KEY_EXPIRED` (401) [#requests-fail-with-invalid_api_key-or-api_key_expired-401]
**Check:** the key in your server's environment is complete and current. Keys start with `sk_` or `rk_` and are sent as `Authorization: Bearer `.
**Fix:** if the key was revoked or has expired, create a new one in **Developers → API keys** and replace it. A key's expiry can't be extended. See [Secret and restricted keys](/docs/keys-and-access#secret-and-restricted-keys) and [authentication errors](/docs/api-reference/errors#authentication).
### Requests fail with `KEY_PERMISSION_DENIED` (403) [#requests-fail-with-key_permission_denied-403]
**Check:** the message names the scope the request needs. Restricted keys carry only the scopes chosen for them.
**Fix:** add the scope to the restricted key in **Developers → API keys**, or use a key that has it. See [Scopes](/docs/api-reference/authentication#scopes) for the scope each endpoint needs, and [permission errors](/docs/api-reference/errors#permission).
### Requests fail with `KEY_TYPE_NOT_ALLOWED` (403) [#requests-fail-with-key_type_not_allowed-403]
**Check:** a publishable key (`pk_`) was sent to a server endpoint.
**Fix:** publishable keys work only for browser collection. Use a secret or restricted key on your server. See [Choosing a credential](/docs/keys-and-access#choosing-a-credential).
### OAuth requests fail with `WORKSPACE_REQUIRED` or `WORKSPACE_ACCESS_DENIED` (403) [#oauth-requests-fail-with-workspace_required-or-workspace_access_denied-403]
**Check:** requests made with an OAuth token must name a workspace in the [`AbuseZero-Workspace`](/docs/api-reference/workspaces#how-requests-choose-a-workspace) header, using a `ws_` ID the token can access.
**Fix:** list the available workspaces with [`GET /v1/workspaces`](/docs/api-reference/workspaces#list-workspaces) and send one of them. With the AbuseZero CLI, run `abusezero workspaces use ws_…`.
### OAuth requests fail with `OAUTH_SCOPE_REQUIRED` or `PERMISSION_REQUIRED` (403) [#oauth-requests-fail-with-oauth_scope_required-or-permission_required-403]
**Check:** the app wasn't granted the scope the request needs, or the signed-in member's role doesn't allow the action.
**Fix:** sign in again and approve the scope, or ask the workspace owner for the permission. See [Member permissions for OAuth](/docs/api-reference/authentication#member-permissions-for-oauth).
### Requests fail with `ONBOARDING_REQUIRED` (403) [#requests-fail-with-onboarding_required-403]
**Fix:** the workspace owner needs to finish setting up the workspace in the dashboard.
## Evaluation requests [#evaluation-requests]
### `BENEFIT_NOT_FOUND` (404) [#benefit_not_found-404]
**Check:** `benefit` must be the key of a [benefit](/docs/concepts/benefits) in this workspace, such as `pro_trial`. It is not the benefit's `ben_` ID.
**Fix:** copy the key from **Benefits**, or list benefits with [`GET /v1/benefits`](/docs/api-reference/benefits#list-benefits). See [not-found errors](/docs/api-reference/errors#not-found).
### `INVALID_FIELD`, `UNSUPPORTED_FIELD` or `MALFORMED_JSON` [#invalid_field-unsupported_field-or-malformed_json]
**Check:** `field` in the error names the field. `UNSUPPORTED_FIELD` means the request contains a field the endpoint doesn't accept. `MALFORMED_JSON` means the body isn't valid JSON, or `Content-Type: application/json` is missing.
**Fix:** compare the request with [Create an evaluation](/docs/api-reference/evaluations#create-an-evaluation) and the [invalid request errors](/docs/api-reference/errors#invalid-request). When the field is a [custom identifier](/docs/concepts/custom-identifiers), register its key and key version first, in **Developers → Custom identifiers** or with [`POST /v1/custom-identifiers`](/docs/api-reference/custom-identifiers#register-a-custom-identifier).
### `IP_NOT_ROUTABLE` (422) [#ip_not_routable-422]
**Check:** `context.ip` is a private, loopback, documentation or other non-routable address, such as your own server's or a load balancer's.
**Fix:** send the visitor's public IP address from your proxy's forwarded headers, or leave `context.ip` out. See [Send the IP address](/docs/guides/send-identifiers#send-the-ip-address).
### `IDEMPOTENCY_KEY_REQUIRED` (422) [#idempotency_key_required-422]
**Fix:** every write needs an [`Idempotency-Key`](/docs/api-reference#idempotency) header of 1 to 128 printable characters with no spaces. Use a value tied to the attempt, such as `evaluate-`, so a retry sends the same key.
### `IDEMPOTENCY_CONFLICT` (409) [#idempotency_conflict-409]
**Check:** the same idempotency key was sent with a different request body.
**Fix:** a retry must send the original body with the original key. A new evaluation needs a new key. See [conflict errors](/docs/api-reference/errors#conflict).
### `CLAIM_CONFLICT` (409) [#claim_conflict-409]
**Check:** the claim ID was already used for a different user.
**Fix:** a claim ID identifies one award to one user. Use a new claim ID for each award, such as the ID of the trial or credit grant in your system. See [Re-evaluating a `claim_id`](/docs/api-reference/evaluations#re-evaluating-a-claim_id).
### `CLAIM_PERIOD_ENDED` (409) [#claim_period_ended-409]
**Check:** the benefit's [eligibility](/docs/api-reference/benefits#eligibility) resets by period, and this claim ID belongs to an earlier period.
**Fix:** use a new claim ID for the new period's award.
### `BOT_EVIDENCE_REQUIRED` (422) [#bot_evidence_required-422]
**Check:** the benefit requires a bot check, and the evaluation didn't carry a usable one in `identifiers.device_token`.
**Fix:** call [`prepareBotObservation`](/docs/tools/browser-sdk#preparebotobservation) when the person submits, and send its `deviceToken` in the evaluation within 2 minutes. See [Add a bot check](/docs/guides/bot-check).
### `AGENT_EVIDENCE_OUTDATED` (422) [#agent_evidence_outdated-422]
**Check:** `agent_evidence` came from a version of the [AbuseZero Node.js SDK](/docs/tools/server-sdk#verify-ai-agents) before 0.11.0.
**Fix:** update the `abusezero` package to 0.11.0 or later, and send the opaque token [`verifyAgentRequest()`](/docs/tools/server-sdk#verify-ai-agents) returns, unchanged. See [Verify AI agents](/docs/guides/verify-ai-agents).
### Evaluations return `"status": "skipped"` [#evaluations-return-status-skipped]
No result was decided, and your [`outage` setting](/docs/tools/server-sdk#the-outage-option) doesn't apply. See [Skipped evaluations](/docs/api-reference/evaluations#skipped-evaluations).
| `reason` | What to do |
|---|---|
| `insufficient_credits` | The workspace owner adds credits or changes plan in **Workspace settings → Billing & Subscriptions**. |
| `billing_overdue` | The workspace owner settles the overdue payment in **Workspace settings → Billing & Subscriptions**. |
| `benefit_paused` | The workspace is over its plan's limits. The owner reduces usage or changes plan. |
| `service_inactive` | The workspace can't run evaluations right now, for example while it's being deleted. |
Decide in advance what your application does with a skipped claim. See [Handle skipped evaluations](/docs/guides/outages-and-retries#handle-skipped-evaluations).
## Browser data [#browser-data]
### Evaluations show `inputs.device: missing` or `browser_data: not_sent` [#evaluations-show-inputsdevice-missing-or-browser_data-not_sent]
**Check:** the evaluation didn't include `identifiers.device_token`.
**Fix:** pass the `deviceToken` from [`observe`](/docs/tools/browser-sdk#observe) or `prepareBotObservation` to your server, then send it unchanged as `identifiers.device_token`. Send it whatever its status. When there's no browser data, it carries the reason. See [Send the device token with the evaluation](/docs/guides/browser-setup#send-the-device-token-with-the-evaluation).
### `browser_data` shows one of these statuses [#browser_data-shows-one-of-these-statuses]
| Status | What it means | Fix |
|---|---|---|
| `collected` | Browser data reached the evaluation. | Nothing to fix. |
| `not_passed_on` | The browser collected data for this claim, but your server didn't send its device token. | Pass `deviceToken` through to `identifiers.device_token`. |
| `site_not_listed` | The page isn't in your site list. | Add the site in **Developers → Browser setup**. Only HTTPS sites are accepted. |
| `sdk_outdated` | The page runs a browser SDK version that's no longer supported. | Update the `abusezero` package or the standalone script address. |
| `expired` | The device token reached the evaluation too late. | Send the token in the evaluation soon after the browser returns it. |
| `not_loaded` | Your consent tool kept the script from loading, and you reported it. | Nothing to fix. |
| `consent_declined` | The visitor declined consent. | Nothing to fix. |
| `blocked`, `timed_out`, `failed` | The browser couldn't complete collection, for example because of a blocker or a poor connection. | Nothing to fix for a single evaluation. If it happens often, check **Developers → Browser setup**. |
The evaluation still runs in every case. It uses everything else it received. See [Browser data statuses](/docs/concepts/browser-protection#browser-data-statuses).
### Browser setup shows `script_missing` [#browser-setup-shows-script_missing]
**Check:** AbuseZero hasn't heard from the [AbuseZero browser SDK](/docs/tools/browser-sdk) on any of your sites recently, while evaluations are still arriving.
**Fix:** confirm that [`init`](/docs/tools/browser-sdk#init) runs on every page that leads to a claim, with your publishable key, and that the page is in your site list. If your site sets a Content Security Policy, copy the sources shown in **Developers → Browser setup** into it. See [Browser setup issues](/docs/api-reference/browser-collection#issues).
## Results [#results]
### Unexpected denials [#unexpected-denials]
1. Read `reason` on the evaluation. [Reason codes](/docs/reason-codes) explains each one and what you can do.
2. Check the [policy version](/docs/concepts/policies-and-rules#versions) the evaluation ran under. A recent policy change may have decided it.
3. Check [`inputs`](/docs/concepts/risk-score-and-signals#inputs). A missing input changes what AbuseZero can see.
4. For a prior-claim reason, open the evaluation's [association graph](/docs/concepts/association-graph) to see the earlier outcomes behind it.
5. To let this person through, add an [allow list](/docs/concepts/allow-and-block-lists) entry.
6. To change how the benefit treats claims like this, adjust its [protection level](/docs/concepts/protection-levels) or [custom rules](/docs/concepts/policies-and-rules), and [test against past traffic](/docs/guides/test-changes-safely#test-against-past-traffic) before you save.
To review results without affecting any user, put the benefit in [observe mode](/docs/concepts/benefits#protect-and-observe).
### `previously_authorized` on a repeat claim [#previously_authorized-on-a-repeat-claim]
**Check:** the same claim ID was evaluated again for the same user. See [Repeat requests](/docs/reason-codes#repeat-requests).
**Fix:** nothing, if it's a retry of the same award. A new award needs a new claim ID.
## Outcomes [#outcomes]
### Reporting an outcome fails with `OUTCOME_CONFLICT` (409) [#reporting-an-outcome-fails-with-outcome_conflict-409]
**Check:** the message says why. A denied evaluation can't be confirmed as granted. A newer evaluation may have replaced this one. The change may not be one the outcome's current state accepts, such as reversing an outcome reported as `not_granted`. Or the `external_outcome_id` already belongs to another outcome.
**Fix:** report against the claim's most recent allowed evaluation, with a change its current state accepts. See [allowed changes](/docs/api-reference/outcomes#allowed-changes) for the accepted changes and [Report outcomes](/docs/guides/report-outcomes).
### An evaluation fails with `OUTCOME_CONFLICT` (409) [#an-evaluation-fails-with-outcome_conflict-409]
**Check:** the benefit [reports outcomes](/docs/concepts/outcomes#tracking-models), and an earlier evaluation of this claim ID passed its `due_at` without a report.
**Fix:** report that claim's outcome first, with [`POST /v1/evaluations/{id}/outcome`](/docs/api-reference/outcomes#report-an-outcome), then evaluate again.
### `REASON_REQUIRED` (422) [#reason_required-422]
**Fix:** reporting `not_granted` or `reversed` needs a `reason` of 3 to 500 characters. See [invalid request errors](/docs/api-reference/errors#invalid-request).
## Retries and availability [#retries-and-availability]
### `RATE_LIMITED` (429) [#rate_limited-429]
**Fix:** wait for the number of seconds in the `Retry-After` header, then retry with the same idempotency key. Limits depend on your plan. The [AbuseZero Node.js SDK](/docs/tools/server-sdk#the-outage-option) never turns a rate limit into a local allow or deny. See [Rate limits](/docs/api-reference#rate-limits) and [rate limit errors](/docs/api-reference/errors#rate-limit).
### `TEMPORARILY_UNAVAILABLE` (503), `DEADLINE_EXCEEDED` (504) or `EVALUATION_PENDING` (409) [#temporarily_unavailable-503-deadline_exceeded-504-or-evaluation_pending-409]
**Fix:** these errors are retryable (`retryable: true`). Retry with the same idempotency key. AbuseZero returns the original result if the first attempt finished. See [unavailable errors](/docs/api-reference/errors#unavailable) and [Retry retryable errors](/docs/guides/outages-and-retries#retry-retryable-errors).
### `CURSOR_INVALID` (422) [#cursor_invalid-422]
**Fix:** a page cursor was changed or belongs to another list. Pass `next_cursor` back exactly as you received it, or start again from the first page. See [Pagination](/docs/api-reference#pagination).
## Contacting support [#contacting-support]
Include the `X-Request-ID` of the request, the evaluation ID if there is one, and the time it happened. Never send secret keys or client secrets.
## Related [#related]
- [REST API errors](/docs/api-reference/errors)
- [Reason codes](/docs/reason-codes)
- [Developer FAQ](/docs/developer-faq)
- [Monitor your integration](/docs/guides/monitor-your-integration)
# Allow and block lists (/docs/api-reference/access-lists)
Allow and block lists decide the result for the specific people, addresses, networks and devices you name. An entry applies to one [benefit](/docs/concepts/benefits) or to every benefit in the [workspace](/docs/keys-and-access#workspaces), from the moment it's saved until it expires or you remove it. Every [evaluation](/docs/concepts/evaluations) checks every active entry. See [Allow and block lists](/docs/concepts/allow-and-block-lists) for how entries shape results.
Examples on this page use a server client:
```ts
import { AbuseZero } from "abusezero/server";
const abusezero = new AbuseZero({ key: process.env.ABUSEZERO_SECRET_KEY!, outage: "throw" });
```
## The access list entry object [#the-access-list-entry-object]
```json
{
"object": "access_list_entry",
"id": "lst_4c2e8a1f-7b3d-4e9a-8f12-6d0b5a3c9e71",
"kind": "block",
"type": "domain",
"benefit": null,
"reason": "Reseller domain confirmed by our support team",
"expires_at": "2027-01-27T09:14:22.000Z",
"created_at": "2026-10-29T09:14:22.000Z"
}
```
| Field | Type | Description |
|---|---|---|
| `object` | `"access_list_entry"` | The object type. |
| `id` | string | The entry ID, starting with `lst_`. |
| `kind` | `allow` or `block` | Whether a match allows or denies the [claim](/docs/concepts/evaluations). |
| `type` | `email`, `email_exact`, `domain`, `ip`, `user` or `device_id` | What the entry matches. See [Entry types](#entry-types). |
| `benefit` | string or `null` | The [benefit](/docs/api-reference/benefits#the-benefit-object) ID (`ben_…`) the entry applies to. `null` means every benefit in the workspace. |
| `reason` | string | Why the entry exists, as you wrote it. |
| `expires_at` | string or `null` | When the entry stops applying. `null` means it never expires. |
| `created_at` | string | When the entry was created. |
Reads never return the listed value itself. Keep your own record of what you listed and why, and use the entry ID to manage it.
### Entry types [#entry-types]
| Type | Matches | Value format |
|---|---|---|
| `email` | The mailbox behind an email address, as AbuseZero recognizes it. | An email address, such as `jordan@example.com`. |
| `email_exact` | Only this exact email address. | An email address. |
| `domain` | Every email address at this domain. | A domain, such as `example.com`. |
| `ip` | Claims from this IP address. | One IPv4 or IPv6 address. |
| `user` | Claims for this user. | The `subject.external_id` you send in [evaluations](/docs/api-reference/evaluations#create-an-evaluation). |
| `device_id` | Claims from this device. | The `identifiers.device_id` you send in evaluations. |
## Add an entry [#add-an-entry]
Adds one value, or up to 50 values in a single call, to an [allow or block list](/docs/concepts/allow-and-block-lists).
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `POST` | `/v1/access-lists` | [`access_lists:write`](/docs/api-reference/authentication#scopes) | Required |
Also available as [`abusezero access-lists add`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_add_access_list_entry`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`addAccessListEntry()`](/docs/tools/server-sdk#additional-methods) or [`addAccessListEntries()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
### Request body [#request-body]
| Field | Type | Required | Limits | Description |
|---|---|---|---|---|
| `kind` | `allow` or `block` | Yes | | Whether a match allows or denies the [claim](/docs/concepts/evaluations). |
| `type` | string | Yes | One of the [entry types](#entry-types) | What the entry matches. |
| `value` | string | One of `value` or `values` | 1 to 254 characters | One value to list. |
| `values` | array of strings | One of `value` or `values` | 1 to 50 values, each 1 to 254 characters | Several values of the same type to list in one call. |
| `reason` | string | Yes | 3 to 500 characters | Why you're adding the entry. It's returned with the entry. |
| `benefit_id` | string or `null` | No | A `ben_` ID from this workspace | The [benefit](/docs/api-reference/benefits#the-benefit-object) the entry applies to. Omit it or send `null` to apply the entry to every benefit. |
| `expires_at` | string or `null` | No | ISO 8601 date and time in UTC, ending in `Z`, such as `2027-01-27T09:00:00Z` | When the entry stops applying. Send this or `expires_in_days`, not both. |
| `expires_in_days` | integer or `null` | No | 1 to 3,650 | Whole days from now until the entry stops applying. Send this or `expires_at`, not both. |
Omit both `expires_at` and `expires_in_days` for an entry that never expires.
### Example: one value [#example-one-value]
```ts tab="Node.js"
const entry = await abusezero.addAccessListEntry(
{
kind: "block",
type: "domain",
value: "example.com",
reason: "Reseller domain confirmed by our support team",
expires_in_days: 90,
},
"block-example-com-2026-10-29",
);
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/access-lists \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: block-example-com-2026-10-29" \
-d '{
"kind": "block",
"type": "domain",
"value": "example.com",
"reason": "Reseller domain confirmed by our support team",
"expires_in_days": 90
}'
```
### Response: one value [#response-one-value]
Returns `201`.
```json
{
"object": "access_list_entry",
"id": "lst_4c2e8a1f-7b3d-4e9a-8f12-6d0b5a3c9e71"
}
```
If an active entry with the same kind, type, value and benefit already exists, the response returns that entry's ID instead of creating a second one.
### Example: several values [#example-several-values]
```ts tab="Node.js"
const results = await abusezero.addAccessListEntries(
{
kind: "allow",
type: "email_exact",
values: ["jordan@example.com", "sam@example.com", "not-an-email"],
reason: "Design partners approved for a second trial",
},
"allow-design-partners-2026-10-29",
);
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/access-lists \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: allow-design-partners-2026-10-29" \
-d '{
"kind": "allow",
"type": "email_exact",
"values": ["jordan@example.com", "sam@example.com", "not-an-email"],
"reason": "Design partners approved for a second trial"
}'
```
### Response: several values [#response-several-values]
Returns `201`. Every submitted value gets its own result, in the order you sent them. Valid values are saved even when others in the same call are invalid. Repeated values get one result, and blank values are ignored. A retry with the same [`Idempotency-Key`](/docs/api-reference#idempotency) and body returns these same results, so values the first call saved still read `created`.
```json
{
"object": "list",
"data": [
{
"object": "access_list_result",
"value": "jordan@example.com",
"status": "created",
"id": "lst_9a1d3f5e-2c4b-4d8e-a6f7-0b3e5c7d9a12"
},
{
"object": "access_list_result",
"value": "sam@example.com",
"status": "already_listed",
"id": "lst_1e7b9c3d-5f2a-4a6e-8d0c-4b2f6a8e0c34"
},
{
"object": "access_list_result",
"value": "not-an-email",
"status": "invalid",
"reason": "Use an email such as name@example.com. Quoted and international local parts are not supported."
}
]
}
```
| Field | Type | Description |
|---|---|---|
| `value` | string | The value as you sent it. |
| `status` | `created`, `already_listed` or `invalid` | `created` saved a new entry. `already_listed` found an active entry with the same kind, type, value and benefit. `invalid` saved nothing for this value. |
| `id` | string | The entry ID. Present for `created` and `already_listed`. |
| `reason` | string | Why the value is invalid. Present for `invalid` only. |
### Errors [#errors]
| Code | Status | Meaning |
|---|---|---|
| [`INVALID_FIELD`](/docs/api-reference/errors#invalid-request) | 422 | Both or neither of `value` and `values` were sent, both expiry fields were sent, `expires_at` isn't a UTC time ending in `Z`, or a field is outside its limits. |
| [`INVALID_EMAIL`](/docs/api-reference/errors#invalid-request) | 422 | A single `email` or `email_exact` value isn't a supported email address. `field` is `value`. |
| [`INVALID_DOMAIN`](/docs/api-reference/errors#invalid-request) | 422 | A single `domain` value isn't a domain such as `example.com`. `field` is `value`. |
| [`INVALID_IP`](/docs/api-reference/errors#invalid-request) | 422 | A single `ip` value isn't an IPv4 or IPv6 address. `field` is `value`. |
| [`BENEFIT_NOT_FOUND`](/docs/api-reference/errors#not-found) | 404 | `benefit_id` isn't a benefit in this workspace. `field` is `benefit_id`. |
In a call with `values`, the value problems appear as `invalid` results instead of errors. For every other code, see [REST API errors](/docs/api-reference/errors).
## List entries [#list-entries]
Returns entries newest first.
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `GET` | `/v1/access-lists` | [`access_lists:read`](/docs/api-reference/authentication#scopes) | Not used |
Also available as [`abusezero access-lists list`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_list_access_lists`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`accessLists()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
### Query parameters [#query-parameters]
| Parameter | Type | Required | Limits | Description |
|---|---|---|---|---|
| `limit` | integer | No | 1 to 100, default 50 | How many entries to return. |
| `cursor` | string | No | | The `next_cursor` from the previous page, unchanged. |
### Example [#example]
```ts tab="Node.js"
const page = await abusezero.accessLists({ limit: 100 });
```
```bash tab="cURL"
curl "https://api.abusezero.com/v1/access-lists?limit=100" \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
### Response [#response]
Returns `200`.
```json
{
"object": "list",
"data": [
{
"object": "access_list_entry",
"id": "lst_4c2e8a1f-7b3d-4e9a-8f12-6d0b5a3c9e71",
"kind": "block",
"type": "domain",
"benefit": null,
"reason": "Reseller domain confirmed by our support team",
"expires_at": "2027-01-27T09:14:22.000Z",
"created_at": "2026-10-29T09:14:22.000Z"
}
],
"next_cursor": null
}
```
When `next_cursor` is a string, pass it as `cursor` to read the next page. See [Pagination](/docs/api-reference#pagination) in the AbuseZero REST API overview.
## Retrieve an entry [#retrieve-an-entry]
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `GET` | `/v1/access-lists/{id}` | [`access_lists:read`](/docs/api-reference/authentication#scopes) | Not used |
Also available as [`abusezero access-lists get`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_get_access_list_entry`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`call()`](/docs/tools/server-sdk#call-for-any-endpoint) in the AbuseZero Node.js SDK.
### Path parameters [#path-parameters]
| Parameter | Type | Description |
|---|---|---|
| `id` | string | The entry ID, starting with `lst_`. |
### Example [#example-1]
```ts tab="Node.js"
const entry = await abusezero.call("/v1/access-lists/lst_4c2e8a1f-7b3d-4e9a-8f12-6d0b5a3c9e71");
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/access-lists/lst_4c2e8a1f-7b3d-4e9a-8f12-6d0b5a3c9e71 \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
### Response [#response-1]
Returns `200` with an [access list entry object](#the-access-list-entry-object).
### Errors [#errors-1]
| Code | Status | Meaning |
|---|---|---|
| [`RESOURCE_NOT_FOUND`](/docs/api-reference/errors#not-found) | 404 | No entry with this ID exists in the workspace. |
## Remove an entry [#remove-an-entry]
Removes an entry. It stops applying to [evaluations](/docs/concepts/evaluations) immediately.
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `DELETE` | `/v1/access-lists/{id}` | [`access_lists:write`](/docs/api-reference/authentication#scopes) | Required |
Also available as [`abusezero access-lists delete`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_delete_access_list_entry`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`deleteAccessListEntry()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
### Path parameters [#path-parameters-1]
| Parameter | Type | Description |
|---|---|---|
| `id` | string | The entry ID, starting with `lst_`. |
### Example [#example-2]
```ts tab="Node.js"
await abusezero.deleteAccessListEntry(
"lst_4c2e8a1f-7b3d-4e9a-8f12-6d0b5a3c9e71",
"remove-lst-4c2e8a1f",
);
```
```bash tab="cURL"
curl -X DELETE https://api.abusezero.com/v1/access-lists/lst_4c2e8a1f-7b3d-4e9a-8f12-6d0b5a3c9e71 \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: remove-lst-4c2e8a1f" \
-d '{}'
```
### Response [#response-2]
Returns `200`. Removing an entry that's already gone also succeeds.
```json
{
"object": "deleted",
"id": "lst_4c2e8a1f-7b3d-4e9a-8f12-6d0b5a3c9e71",
"deleted": true
}
```
## Related [#related]
How list entries shape evaluation results.
Find the benefit IDs that entries can apply to.
# Agents (/docs/api-reference/agents)
AI agents now sign up, start trials and claim credits for real people. AbuseZero recognizes agents that can prove who they are, by name, on every [evaluation](/docs/concepts/evaluations). This endpoint lists every agent AbuseZero recognizes. AbuseZero maintains the list.
You decide, benefit by benefit, which recognized agents may claim a benefit: list their IDs in the [benefit's `allowed_agents` setting](/docs/api-reference/benefits#automation). Read the IDs from this endpoint instead of hard-coding them. Automation that can't prove what it is gets no such pass. For the concept, see [Bots and AI agents](/docs/concepts/bots-and-ai-agents). To recognize agents on your own server, see [Verify AI agents](/docs/guides/verify-ai-agents).
## The agent object [#the-agent-object]
```json
{
"object": "agent",
"id": "",
"name": ""
}
```
| Field | Type | Description |
|---|---|---|
| `object` | `"agent"` | The object type. |
| `id` | string | The agent's ID. Use it in a [benefit's](/docs/api-reference/benefits#automation) `allowed_agents`. |
| `name` | string | The agent's display name. |
## List recognized agents [#list-recognized-agents]
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `GET` | `/v1/agents` | [`agents:read`](/docs/api-reference/authentication#scopes) | Not used |
Also available as [`abusezero agents list`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_list_agents`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`agents()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
### Example [#example]
```ts tab="Node.js"
import { AbuseZero } from "abusezero/server";
const abusezero = new AbuseZero({ key: process.env.ABUSEZERO_SECRET_KEY!, outage: "throw" });
const agents = await abusezero.agents();
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/agents \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
### Response [#response]
Returns `200`. Every recognized agent comes back in one response, so `next_cursor` is always `null`.
```json
{
"object": "list",
"data": [
{
"object": "agent",
"id": "",
"name": ""
}
],
"next_cursor": null
}
```
For error codes, see [REST API errors](/docs/api-reference/errors).
## Related [#related]
How AbuseZero tells legitimate agents from automation that hides what it is.
Set `allowed_agents` on a benefit.
Recognize agents on your server and send the evidence with the evaluation.
# Analytics (/docs/api-reference/analytics)
Analytics give you your [workspace's](/docs/keys-and-access#workspaces) [evaluation](/docs/concepts/evaluations) volume by day and result, across every [benefit](/docs/concepts/benefits). Use them to watch trends, size alerts in your own monitoring, and confirm that a [policy](/docs/concepts/policies-and-rules) change moved results the way you expected. For a broader monitoring setup, see [Monitor your integration](/docs/guides/monitor-your-integration).
## The daily summary object [#the-daily-summary-object]
```json
{
"object": "daily_summary",
"day": "2026-10-28",
"result": "deny",
"count": 37
}
```
| Field | Type | Description |
|---|---|---|
| `object` | `"daily_summary"` | The object type. |
| `day` | string | The UTC day, as `YYYY-MM-DD`. |
| `result` | `allow` or `deny` | The [evaluation](/docs/api-reference/evaluations#the-evaluation-object) result being counted. |
| `count` | integer | How many evaluations returned this result on this day, across every [benefit](/docs/api-reference/benefits#the-benefit-object). |
## Read analytics [#read-analytics]
Returns one summary for each day and result that had evaluations, newest day first. Days without evaluations don't appear. Today's counts are refreshed through the day and can briefly trail live traffic.
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `GET` | `/v1/analytics` | [`analytics:read`](/docs/api-reference/authentication#scopes) | Not used |
Also available as [`abusezero analytics get`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_get_analytics`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`analytics()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
### Query parameters [#query-parameters]
| Parameter | Type | Required | Limits | Description |
|---|---|---|---|---|
| `days` | integer | No | 1 to 90, default 30 | How many recent UTC days to include, counting today. |
### Example [#example]
```ts tab="Node.js"
import { AbuseZero } from "abusezero/server";
const abusezero = new AbuseZero({ key: process.env.ABUSEZERO_SECRET_KEY!, outage: "throw" });
const summaries = await abusezero.analytics(7);
```
```bash tab="cURL"
curl "https://api.abusezero.com/v1/analytics?days=7" \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
### Response [#response]
Returns `200`. The whole range comes back in one response, so `next_cursor` is always `null`.
```json
{
"object": "list",
"data": [
{ "object": "daily_summary", "day": "2026-10-29", "result": "allow", "count": 412 },
{ "object": "daily_summary", "day": "2026-10-29", "result": "deny", "count": 21 },
{ "object": "daily_summary", "day": "2026-10-28", "result": "allow", "count": 958 },
{ "object": "daily_summary", "day": "2026-10-28", "result": "deny", "count": 37 }
],
"next_cursor": null
}
```
### Errors [#errors]
| Code | Status | Meaning |
|---|---|---|
| [`INVALID_FIELD`](/docs/api-reference/errors#invalid-request) | 422 | `days` isn't a whole number from 1 to 90. |
For every other code, see [REST API errors](/docs/api-reference/errors).
## Related [#related]
Combine analytics, request logs and browser setup into one view of your integration.
List and filter individual evaluations.
# Audit logs (/docs/api-reference/audit-logs)
The audit log records changes in your [workspace](/docs/keys-and-access#workspaces) as they happen: configuration updates, [imports](/docs/api-reference/imports), [exports](/docs/api-reference/exports), [deletion requests](/docs/api-reference/data-deletions) and more, each with what happened, whether a member or AbuseZero made the change, and when. Use it to answer what changed during a review, or feed it into your own tooling.
With an OAuth token, the signed-in member also needs [permission](/docs/api-reference/authentication#member-permissions-for-oauth) to manage the workspace.
Examples on this page use a server client:
```ts
import { AbuseZero } from "abusezero/server";
const abusezero = new AbuseZero({ key: process.env.ABUSEZERO_SECRET_KEY!, outage: "throw" });
```
## The audit event object [#the-audit-event-object]
```json
{
"object": "audit_event",
"id": "aud_48213",
"action": "browser_collection.updated",
"actor": "member",
"created_at": "2026-10-29T15:11:46.000Z"
}
```
| Field | Type | Description |
|---|---|---|
| `object` | `"audit_event"` | The object type. |
| `id` | string | The event ID: `aud_` followed by a number. |
| `action` | string | What happened, such as `import.confirmed`, `evaluations.export_requested` or `browser_collection.updated`. New actions are added as AbuseZero grows, so treat unfamiliar values as valid. |
| `actor` | `member` or `system` | `member` when a person or one of the workspace's API keys made the change. `system` when AbuseZero's own background work did, such as a scheduled job or a [data deletion](/docs/api-reference/data-deletions). |
| `created_at` | string | When it happened. |
## List audit events [#list-audit-events]
Returns audit events newest first.
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `GET` | `/v1/audit-logs` | [`audit_logs:read`](/docs/api-reference/authentication#scopes) | Not used |
Also available as [`abusezero audit-logs list`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_list_audit_logs`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`auditLogs()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
### Query parameters [#query-parameters]
| Parameter | Type | Required | Limits | Description |
|---|---|---|---|---|
| `limit` | integer | No | 1 to 100, default 50 | How many events to return. |
| `cursor` | string | No | | The `next_cursor` from the previous page, unchanged. |
### Example [#example]
```ts tab="Node.js"
const page = await abusezero.auditLogs({ limit: 100 });
```
```bash tab="cURL"
curl "https://api.abusezero.com/v1/audit-logs?limit=100" \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
### Response [#response]
Returns `200`.
```json
{
"object": "list",
"data": [
{
"object": "audit_event",
"id": "aud_48213",
"action": "browser_collection.updated",
"actor": "member",
"created_at": "2026-10-29T15:11:46.000Z"
},
{
"object": "audit_event",
"id": "aud_48190",
"action": "import.confirmed",
"actor": "member",
"created_at": "2026-10-29T11:24:02.000Z"
}
],
"next_cursor": "…"
}
```
When `next_cursor` is a string, pass it as `cursor` to read the next page. See [Pagination](/docs/api-reference#pagination).
### Errors [#errors]
| Code | Status | Meaning |
|---|---|---|
| [`PERMISSION_REQUIRED`](/docs/api-reference/errors#permission) | 403 | With an OAuth token, the signed-in member doesn't have permission to manage the workspace. Ask the workspace owner. |
## Retrieve an audit event [#retrieve-an-audit-event]
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `GET` | `/v1/audit-logs/{id}` | [`audit_logs:read`](/docs/api-reference/authentication#scopes) | Not used |
Also available as [`abusezero audit-logs get`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_get_audit_log`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`call()`](/docs/tools/server-sdk#call-for-any-endpoint) in the AbuseZero Node.js SDK.
### Path parameters [#path-parameters]
| Parameter | Type | Description |
|---|---|---|
| `id` | string | The event ID, such as `aud_48213`. |
### Example [#example-1]
```ts tab="Node.js"
const event = await abusezero.call("/v1/audit-logs/aud_48213");
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/audit-logs/aud_48213 \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
### Response [#response-1]
Returns `200` with an [audit event object](#the-audit-event-object).
### Errors [#errors-1]
| Code | Status | Meaning |
|---|---|---|
| [`RESOURCE_NOT_FOUND`](/docs/api-reference/errors#not-found) | 404 | No audit event with this ID exists in the workspace. |
| [`PERMISSION_REQUIRED`](/docs/api-reference/errors#permission) | 403 | With an OAuth token, the signed-in member doesn't have permission to manage the workspace. Ask the workspace owner. |
For every other code, see [REST API errors](/docs/api-reference/errors).
## Related [#related]
The record of evaluation requests, including failed ones.
Credentials, scopes and member permissions.
# REST API authentication (/docs/api-reference/authentication)
Every request to the AbuseZero REST API carries one credential. The credential decides which [workspace](/docs/keys-and-access#workspaces) the request reaches and what it may do there. A credential can never reach a workspace it wasn't granted, and no request parameter can widen its access.
## Credential types [#credential-types]
| Credential | Format | Use it in | Access |
|---|---|---|---|
| Secret key (full access) | `sk_…` | Your server | Every server scope in one [workspace](/docs/keys-and-access#workspaces). |
| Restricted key | `rk_…` | Your server | Only the scopes chosen for it, in one workspace. Its scopes can be changed later. |
| Publishable key | `pk_…` | The browser SDK and standalone browser script | Browser data collection only. Safe to ship in browser code. |
| OAuth access token | Opaque token | The AbuseZero CLI, MCP clients and OAuth apps | The scopes the user approved, in the workspaces they can access. |
Create and manage keys in the dashboard under **Developers**. A key's type and expiry never change after it's created. An expired or revoked key stops working at once.
Keep secret and restricted keys on your server. Never put them in browser code, mobile apps, URLs or logs. Use a restricted key with only the scopes a service needs.
## Send a credential [#send-a-credential]
Send the credential as a bearer token in the `Authorization` header.
```bash tab="API key"
curl https://api.abusezero.com/v1/benefits \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
```bash tab="OAuth access token"
curl https://api.abusezero.com/v1/benefits \
-H "Authorization: Bearer $ABUSEZERO_ACCESS_TOKEN" \
-H "AbuseZero-Workspace: ws_6d2a8f14-3c9e-4b71-a5d0-8e2f6c4a9b13"
```
```ts tab="Node.js"
import { AbuseZero } from "abusezero/server";
const abusezero = new AbuseZero({
key: process.env.ABUSEZERO_SECRET_KEY!,
outage: "throw",
});
```
## Choose a workspace [#choose-a-workspace]
An API key belongs to exactly one [workspace](/docs/keys-and-access#workspaces), so requests made with it need no workspace header.
An OAuth access token reaches the workspaces it was granted: only that workspace when it was granted for one workspace, otherwise every workspace its user can access. Every request made with one chooses the workspace in the `AbuseZero-Workspace` header, using the workspace's `ws_` ID. List the available workspaces with [`GET /v1/workspaces`](/docs/api-reference/workspaces), the one endpoint that works without the header.
```ts
const abusezero = new AbuseZero({
accessToken: process.env.ABUSEZERO_ACCESS_TOKEN!,
workspace: "ws_6d2a8f14-3c9e-4b71-a5d0-8e2f6c4a9b13",
outage: "throw",
});
```
The AbuseZero CLI and the AbuseZero MCP server handle OAuth sign-in and workspace selection for you. See [AbuseZero CLI](/docs/tools/cli) and [AbuseZero MCP server](/docs/tools/mcp).
## Scopes [#scopes]
Each endpoint requires one scope. Secret keys hold every server scope. Restricted keys and OAuth access tokens hold only the scopes granted to them.
| Scope | Allows |
|---|---|
| `workspaces:read` | List the [workspaces](/docs/keys-and-access#workspaces) the credential can use. |
| `evaluations:create` | Evaluate [claims](/docs/concepts/evaluations) with `POST /v1/evaluations`. |
| `evaluations:read` | List and read evaluations, and read their [association graphs](/docs/concepts/association-graph). |
| `evaluation_evidence:read` | Read the retained request details of an evaluation. Also needed, together with the export scopes, to create or download exports that include them. |
| `outcomes:read` | List and read [outcomes](/docs/concepts/outcomes). |
| `outcomes:write` | Report outcomes. |
| `simulations:run` | Run previews, simulations and replays. |
| `benefits:read` | List and read [benefits](/docs/concepts/benefits). |
| `benefits:write` | Create, update and delete benefits. |
| `policies:read` | List and read [policies](/docs/concepts/policies-and-rules), policy versions and the conditions available to custom rules. |
| `policies:write` | Create, update, duplicate and delete custom policies. |
| `access_lists:read` | List and read [allow and block list](/docs/concepts/allow-and-block-lists) entries. |
| `access_lists:write` | Add and delete allow and block list entries. |
| `custom_identifiers:read` | List and read [custom identifiers](/docs/concepts/custom-identifiers). |
| `custom_identifiers:write` | Register and delete custom identifiers. |
| `imports:read` | List and read imports and their error reports. |
| `imports:write` | Create, validate, confirm and cancel imports. |
| `exports:read` | List and read exports, and download finished ones. |
| `exports:create` | Request exports. |
| `data_deletions:read` | List and read data deletions. |
| `data_deletions:write` | Preview and request data deletions. |
| `analytics:read` | Read daily evaluation totals. |
| `request_logs:read` | List and read request logs. |
| `audit_logs:read` | List and read audit events. |
| `browser_collection:read` | Read browser setup: your site list and recent browser data coverage. |
| `browser_collection:write` | Update your site list. |
| `agents:read` | List the AI agents AbuseZero recognizes. |
| `observations:create` | Send browser data. Publishable keys only; the browser SDK uses it for you. |
A request without the required scope returns `KEY_PERMISSION_DENIED` for API keys and `OAUTH_SCOPE_REQUIRED` for OAuth access tokens.
## Member permissions for OAuth [#member-permissions-for-oauth]
An OAuth access token acts for a person, so some scopes also need that person to hold a permission in the [workspace](/docs/keys-and-access#workspaces). Workspace owners hold every permission, and admins also hold **Manage integrations, lists, and imports**. Owners grant permissions to other members in the dashboard.
| Scope | Permission the member needs |
|---|---|
| `evaluation_evidence:read` | Reveal personal identifiers |
| `exports:create` | Export data |
| `data_deletions:read`, `data_deletions:write` | Delete matching history |
| `audit_logs:read` | Manage integrations, lists, and imports |
| `benefits:write`, `policies:write` | Publish [policies](/docs/concepts/policies-and-rules) and [benefits](/docs/concepts/benefits) |
Without the permission, the request returns `PERMISSION_REQUIRED`. API keys don't need member permissions; their scopes decide their access.
## Errors [#errors]
| Status | Code | Meaning | What to do |
|---|---|---|---|
| 401 | `AUTHENTICATION_REQUIRED` | The request has no credential. | Send `Authorization: Bearer` with a key or access token. |
| 401 | `INVALID_API_KEY` | The key is unknown or revoked. | Create a new key in Developers. |
| 401 | `API_KEY_EXPIRED` | The key passed its expiry. | Create a new key in Developers and replace the old one. |
| 401 | `INVALID_ACCESS_TOKEN` | The OAuth access token is invalid or expired. A value that isn't a complete API key is read as an access token. | Sign in again, then retry. If you meant to send an API key, check that it was copied in full. |
| 403 | `KEY_TYPE_NOT_ALLOWED` | A publishable key was used on a server endpoint, or a server key in the browser. | Use a secret or restricted key on your server, and the publishable key in the browser. |
| 403 | `KEY_PERMISSION_DENIED` | The API key lacks the endpoint's scope. | Use a key with the scope named in the message. |
| 403 | `OAUTH_SCOPE_REQUIRED` | The access token lacks the endpoint's scope. | Authorize the scope named in the message, then retry. |
| 403 | `WORKSPACE_REQUIRED` | An OAuth request has no valid `AbuseZero-Workspace` header. | Send a `ws_` ID from `GET /v1/workspaces`. |
| 403 | `WORKSPACE_ACCESS_DENIED` | The credential can't reach that workspace. | Choose a workspace the credential belongs to. |
| 403 | `PERMISSION_REQUIRED` | The signed-in member lacks a permission the scope needs. | Ask the workspace owner for the permission. |
| 403 | `EVIDENCE_SCOPE_REQUIRED` | An export with evidence needs `evaluation_evidence:read`. | Add the scope, or request the export without evidence. |
| 403 | `ONBOARDING_REQUIRED` | The workspace hasn't finished setup. | Ask the workspace owner to finish onboarding in the dashboard. |
See [REST API errors](/docs/api-reference/errors) for the full list.
# Benefits (/docs/api-reference/benefits)
A [benefit](/docs/concepts/benefits) is something you give users a limited number of times: a trial, a free plan, sign-up credits, a promotion. Each benefit has a key you send with every [evaluation](/docs/concepts/evaluations), a [policy](/docs/concepts/policies-and-rules) that decides its claims, and settings for eligibility, [outcomes](/docs/concepts/outcomes), automation and agents.
| Endpoint | Scope |
|---|---|
| [`POST /v1/benefits`](#create-a-benefit) | `benefits:write` |
| [`GET /v1/benefits`](#list-benefits) | `benefits:read` |
| [`GET /v1/benefits/{id}`](#retrieve-a-benefit) | `benefits:read` |
| [`PATCH /v1/benefits/{id}`](#update-a-benefit) | `benefits:write` |
| [`DELETE /v1/benefits/{id}`](#delete-a-benefit) | `benefits:write` |
With an OAuth access token, `benefits:write` also needs the member's **Publish policies and benefits** permission.
## The benefit object [#the-benefit-object]
| Field | Type | Description |
|---|---|---|
| `object` | string | Always `benefit`. |
| `id` | string | The [benefit](/docs/concepts/benefits)'s ID, starting with `ben_`. |
| `key` | string | The key you send as `benefit` in [evaluations](/docs/concepts/evaluations). Unique in the [workspace](/docs/keys-and-access#workspaces) and fixed after creation. |
| `name` | string | The benefit's display name. |
| `policy` | string | The ID of the [policy](/docs/api-reference/policies#the-policy-object) that decides its claims, starting with `pol_`. See [Policies and rules](/docs/concepts/policies-and-rules). |
| `mode` | string | `protect` or `observe`. See [Protection mode](#protection-mode). |
| `config` | object | The benefit's settings. See [Benefit configuration](#benefit-configuration). |
| `revision` | integer | Increases with every update. Send it back when you update the benefit. |
| `created_at` | string | When the benefit was created. |
### Protection mode [#protection-mode]
| Mode | Behavior |
|---|---|
| `protect` | Evaluations return the result your policy reaches. |
| `observe` | Evaluations always return `allow`. Each evaluation records the result your policy would have reached as `observed_result`, so you can measure a policy on real traffic before it affects anyone. |
## Benefit configuration [#benefit-configuration]
Every field is optional when you create a [benefit](/docs/concepts/benefits). A field you leave out takes its default.
| Field | Type | Default | Description |
|---|---|---|---|
| `eligibility` | object | `{ "kind": "once" }` | How often one user may receive the benefit. Fixed after creation. See [Eligibility](#eligibility). |
| `outcome_tracking` | string | `assume_granted` | `assume_granted`: an allowed [claim](/docs/concepts/evaluations) counts as granted at once. `report_outcome`: you [report the outcome](/docs/api-reference/outcomes#report-an-outcome) of each allowed claim by its `due_at`. |
| `missing_outcome_behavior` | string | `hold_eligibility` | For `report_outcome` benefits. What happens when no report arrives by `due_at`. `hold_eligibility`: the claim keeps counting. `release_eligibility`: the claim stops counting, and the user can claim again. |
| `automation` | string | `assess` | How automated activity is treated. See [Automation](#automation). |
| `bot_evidence` | string | `optional` | `optional` or `required`. With `required`, every evaluation must carry a bot check from the browser SDK, or it returns `BOT_EVIDENCE_REQUIRED`. Allowed [recognized agents](/docs/concepts/bots-and-ai-agents) are exempt. |
| `allowed_agents` | array of strings | `[]` | The recognized AI agents allowed to claim this benefit. Use IDs from [`GET /v1/agents`](/docs/api-reference/agents). At most 1 item. |
| `valuation` | object | `{ "currency": "USD", "face_value": 0, "delivery_cost": 0 }` | What one award is worth and costs you. See [Valuation](#valuation). |
### Eligibility [#eligibility]
| Value | Meaning |
|---|---|
| `{ "kind": "once" }` | One award per user, ever. |
| `{ "kind": "calendar", "period": "day", "timezone": "Europe/Berlin" }` | One award per calendar `day`, `week` or `month`, in the IANA time zone you set (up to 80 characters). Weeks start on Monday. |
| `{ "kind": "rolling", "days": 30 }` | One award per rolling window of 1 to 365 days. |
To change a benefit's eligibility, create a new benefit. The existing benefit keeps its protection and history.
### Automation [#automation]
| Value | Behavior |
|---|---|
| `assess` | AbuseZero Automation Defense's assessment counts toward the AbuseZero Risk Score. |
| `deny` | Detected automation denies the claim, with the reason `automation`. |
| `allow` | Detected automation doesn't count against the claim. |
Recognized agents in `allowed_agents` are never treated as unwanted automation for this benefit. See [Bots and AI agents](/docs/concepts/bots-and-ai-agents).
### Valuation [#valuation]
| Field | Type | Description |
|---|---|---|
| `currency` | string | A three-letter ISO 4217 code in capitals, such as `USD`. |
| `face_value` | number | What one award is worth to the user, from 0 to 10,000,000,000, with up to two decimals. |
| `delivery_cost` | number | What one award costs you to deliver, in the same range. |
Each [outcome](/docs/concepts/outcomes) keeps the valuation in force when it was recorded.
## Create a benefit [#create-a-benefit]
`POST /v1/benefits` · Scope [`benefits:write`](/docs/api-reference/authentication#scopes) · Requires [`Idempotency-Key`](/docs/api-reference#idempotency)
Also available as [`abusezero benefits create`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_create_benefit`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`createBenefit()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
### Request body [#request-body]
| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | Yes | Display name, 1 to 80 characters. |
| `key` | string | Yes | The key you'll send in [evaluations](/docs/concepts/evaluations). Lowercase letters, numbers and underscores, starting with a letter; up to 64 characters. Unique in the [workspace](/docs/keys-and-access#workspaces): a key that's already used returns `BENEFIT_KEY_TAKEN`. |
| `policy_key` | string | Yes | The [policy](/docs/concepts/policies-and-rules) that decides claims: a [protection level](/docs/concepts/protection-levels) (`lenient`, `balanced` or `strict`) or one of your custom policies. |
| `protection_mode` | string | Yes | `protect` or `observe`. |
| `config` | object | Yes | The [benefit configuration](#benefit-configuration). Send `{}` to use every default. |
### Example [#example]
```ts tab="Node.js"
import { randomUUID } from "node:crypto";
const benefit = await abusezero.createBenefit(
{
name: "Pro trial",
key: "pro_trial",
policy_key: "balanced",
protection_mode: "protect",
config: {
eligibility: { kind: "once" },
outcome_tracking: "report_outcome",
missing_outcome_behavior: "hold_eligibility",
valuation: { currency: "USD", face_value: 49, delivery_cost: 6.5 },
},
},
randomUUID(),
);
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/benefits \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 9c3e7a12-5b8d-4f60-a2c4-1e7f9b3d5a88" \
-d '{
"name": "Pro trial",
"key": "pro_trial",
"policy_key": "balanced",
"protection_mode": "protect",
"config": {
"eligibility": { "kind": "once" },
"outcome_tracking": "report_outcome",
"missing_outcome_behavior": "hold_eligibility",
"valuation": { "currency": "USD", "face_value": 49, "delivery_cost": 6.5 }
}
}'
```
### Response [#response]
Returns `201`.
```json
{
"object": "benefit",
"id": "ben_5b2e9c41-7d3a-4f68-a1c0-9e8d7b6a5f24",
"revision": 1
}
```
### Errors [#errors]
| Status | Code | When |
|---|---|---|
| 422 | `IDEMPOTENCY_KEY_REQUIRED` | The `Idempotency-Key` header is missing or invalid. |
| 422 | `INVALID_FIELD` | A field is missing or invalid. `error.field` names it. |
| 422 | `POLICY_NOT_FOUND` | No policy has this `policy_key`. |
| 422 | `INVALID_TIMEZONE` | The calendar eligibility's `timezone` isn't a recognized IANA time zone. `field` is `config.eligibility.timezone`. |
| 409 | `BENEFIT_KEY_TAKEN` | Another [benefit](/docs/concepts/benefits) already uses this `key`. `field` is `key`. |
| 409 | `IDEMPOTENCY_CONFLICT` | The key was already used with a different body. |
## List benefits [#list-benefits]
`GET /v1/benefits` · Scope [`benefits:read`](/docs/api-reference/authentication#scopes)
Also available as [`abusezero benefits list`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_list_benefits`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`benefits()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
Lists [benefits](/docs/concepts/benefits), newest first.
### Query parameters [#query-parameters]
| Parameter | Type | Required | Description |
|---|---|---|---|
| `limit` | integer | No | Items per page, 1 to 100. Defaults to 50. |
| `cursor` | string | No | The `next_cursor` from the previous page. |
### Example [#example-1]
```ts tab="Node.js"
const page = await abusezero.benefits({ limit: 50 });
```
```bash tab="cURL"
curl "https://api.abusezero.com/v1/benefits?limit=50" \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
### Response [#response-1]
A [list](/docs/api-reference#pagination) of [benefit objects](#the-benefit-object).
### Errors [#errors-1]
| Status | Code | When |
|---|---|---|
| 422 | `CURSOR_INVALID` | The cursor was changed or belongs to another list. |
## Retrieve a benefit [#retrieve-a-benefit]
`GET /v1/benefits/{id}` · Scope [`benefits:read`](/docs/api-reference/authentication#scopes)
Also available as [`abusezero benefits get`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_get_benefit`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`getBenefit()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
### Example [#example-2]
```ts tab="Node.js"
const benefit = await abusezero.getBenefit("ben_5b2e9c41-7d3a-4f68-a1c0-9e8d7b6a5f24");
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/benefits/ben_5b2e9c41-7d3a-4f68-a1c0-9e8d7b6a5f24 \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
### Response [#response-2]
```json
{
"object": "benefit",
"id": "ben_5b2e9c41-7d3a-4f68-a1c0-9e8d7b6a5f24",
"key": "pro_trial",
"name": "Pro trial",
"policy": "pol_7a1e3c95-4d2b-4e68-9f0a-5c8d2b6e4a17",
"mode": "protect",
"config": {
"eligibility": { "kind": "once" },
"outcome_tracking": "report_outcome",
"missing_outcome_behavior": "hold_eligibility",
"automation": "assess",
"bot_evidence": "optional",
"allowed_agents": [],
"valuation": { "currency": "USD", "face_value": 49, "delivery_cost": 6.5 }
},
"revision": 1,
"created_at": "2026-10-29T13:40:00.000Z"
}
```
### Errors [#errors-2]
| Status | Code | When |
|---|---|---|
| 404 | `RESOURCE_NOT_FOUND` | No [benefit](/docs/concepts/benefits) with this ID exists in the workspace. |
| 422 | `INVALID_FIELD` | The ID doesn't start with `ben_`. |
## Update a benefit [#update-a-benefit]
`PATCH /v1/benefits/{id}` · Scope [`benefits:write`](/docs/api-reference/authentication#scopes) · Requires [`Idempotency-Key`](/docs/api-reference#idempotency)
Also available as [`abusezero benefits update`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_update_benefit`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`updateBenefit()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
Replaces the [benefit](/docs/concepts/benefits)'s name, [policy](/docs/concepts/policies-and-rules), protection mode and configuration. Send every field, including the configuration fields you aren't changing: a configuration field you leave out returns to its default. The key and eligibility can't change.
The benefit object gives its policy as an ID. To keep the current policy, [retrieve the policy](/docs/api-reference/policies#retrieve-a-policy) and send its `key` as `policy_key`.
Changes apply to [evaluations](/docs/concepts/evaluations) from the moment the update succeeds. Earlier evaluations keep the settings they ran under.
### Request body [#request-body-1]
| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | Yes | Display name, 1 to 80 characters. |
| `policy_key` | string | Yes | The policy that decides claims. |
| `protection_mode` | string | Yes | `protect` or `observe`. |
| `config` | object | Yes | The complete [benefit configuration](#benefit-configuration). `eligibility` must match the current value. |
| `revision` | integer | Yes | The benefit's current `revision`. |
### Example [#example-3]
```ts tab="Node.js"
import { randomUUID } from "node:crypto";
const current = await abusezero.getBenefit("ben_5b2e9c41-7d3a-4f68-a1c0-9e8d7b6a5f24");
await abusezero.updateBenefit(
"ben_5b2e9c41-7d3a-4f68-a1c0-9e8d7b6a5f24",
{
name: current.name,
policy_key: "strict",
protection_mode: "observe",
config: current.config,
revision: current.revision,
},
randomUUID(),
);
```
```bash tab="cURL"
curl -X PATCH https://api.abusezero.com/v1/benefits/ben_5b2e9c41-7d3a-4f68-a1c0-9e8d7b6a5f24 \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 2d8f4b6a-1c3e-4a97-b5d0-7e9a2c4f6b31" \
-d '{
"name": "Pro trial",
"policy_key": "strict",
"protection_mode": "observe",
"config": {
"eligibility": { "kind": "once" },
"outcome_tracking": "report_outcome",
"missing_outcome_behavior": "hold_eligibility",
"automation": "assess",
"bot_evidence": "optional",
"allowed_agents": [],
"valuation": { "currency": "USD", "face_value": 49, "delivery_cost": 6.5 }
},
"revision": 1
}'
```
### Response [#response-3]
Returns `200`.
```json
{
"object": "benefit",
"id": "ben_5b2e9c41-7d3a-4f68-a1c0-9e8d7b6a5f24",
"revision": 2
}
```
### Errors [#errors-3]
| Status | Code | When |
|---|---|---|
| 404 | `BENEFIT_NOT_FOUND` | No benefit with this ID exists in the workspace. |
| 409 | `BENEFIT_CONFLICT` | `revision` isn't the current revision. Read the benefit again. |
| 409 | `BENEFIT_SCOPE_FIXED` | The update changes `eligibility`. Create a new benefit instead. |
| 409 | `IDEMPOTENCY_CONFLICT` | The key was already used with a different body. |
| 422 | `POLICY_NOT_FOUND` | No policy has this `policy_key`. |
| 422 | `INVALID_TIMEZONE` | The calendar eligibility's `timezone` isn't a recognized IANA time zone. `field` is `config.eligibility.timezone`. |
| 422 | `INVALID_FIELD` | A field is missing or invalid. `error.field` names it. |
| 422 | `IDEMPOTENCY_KEY_REQUIRED` | The `Idempotency-Key` header is missing or invalid. |
## Delete a benefit [#delete-a-benefit]
`DELETE /v1/benefits/{id}` · Scope [`benefits:write`](/docs/api-reference/authentication#scopes) · Requires [`Idempotency-Key`](/docs/api-reference#idempotency)
Also available as [`abusezero benefits delete`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_delete_benefit`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`deleteBenefit()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
Deleting a [benefit](/docs/concepts/benefits) is permanent. It also deletes the [allow and block list](/docs/concepts/allow-and-block-lists) entries that apply only to this benefit. [Evaluations](/docs/concepts/evaluations) that send its key fail with `BENEFIT_NOT_FOUND` from then on.
### Example [#example-4]
```ts tab="Node.js"
import { randomUUID } from "node:crypto";
await abusezero.deleteBenefit("ben_5b2e9c41-7d3a-4f68-a1c0-9e8d7b6a5f24", randomUUID());
```
```bash tab="cURL"
curl -X DELETE https://api.abusezero.com/v1/benefits/ben_5b2e9c41-7d3a-4f68-a1c0-9e8d7b6a5f24 \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7b1d5f39-8e2a-4c64-9a0f-3b6d8e1c5f42" \
-d '{}'
```
### Response [#response-4]
Returns `200`.
```json
{
"object": "deleted",
"id": "ben_5b2e9c41-7d3a-4f68-a1c0-9e8d7b6a5f24",
"deleted": true
}
```
### Errors [#errors-4]
| Status | Code | When |
|---|---|---|
| 404 | `BENEFIT_NOT_FOUND` | No benefit with this ID exists in the workspace. |
| 422 | `IDEMPOTENCY_KEY_REQUIRED` | The `Idempotency-Key` header is missing or invalid. |
# Browser setup (/docs/api-reference/browser-collection)
Browser setup is the API form of **Developers → Browser setup** in the dashboard. It holds your site list: the only HTTPS sites allowed to send [browser data](/docs/concepts/browser-protection#browser-data-statuses) to AbuseZero. It also reports where the [browser script](/docs/tools/browser-sdk) is running, how much of your recent traffic arrived with browser data, and the most pressing problem you can fix, if there is one.
Browser protection is always on. For how it works, see [Browser protection](/docs/concepts/browser-protection). To install it, see [Set up browser protection](/docs/guides/browser-setup). If your site sets a Content Security Policy, copy the sources shown in Developers → Browser setup into it.
Examples on this page use a server client:
```ts
import { AbuseZero } from "abusezero/server";
const abusezero = new AbuseZero({ key: process.env.ABUSEZERO_SECRET_KEY!, outage: "throw" });
```
## The browser collection object [#the-browser-collection-object]
```json
{
"object": "browser_collection",
"origins": ["https://app.example.com", "https://www.example.com"],
"revision": 3,
"sites": [
{
"origin": "https://app.example.com",
"listed": true,
"first_seen_at": "2026-10-01T09:00:00.000Z",
"last_seen_at": "2026-10-29T15:42:10.000Z",
"sdk_version": "0.11.0"
},
{
"origin": "https://www.example.com",
"listed": true,
"first_seen_at": null,
"last_seen_at": null,
"sdk_version": null
},
{
"origin": "https://staging.example.com",
"listed": false,
"first_seen_at": "2026-10-27T10:15:00.000Z",
"last_seen_at": "2026-10-29T08:03:51.000Z",
"sdk_version": "0.11.0"
}
],
"issue": null,
"issue_since": null,
"coverage": {
"days": 7,
"total": 6210,
"collected": 5874,
"consent_declined": 214,
"not_loaded": 0,
"blocked": 88,
"timed_out": 21,
"failed": 3,
"site_not_listed": 0,
"sdk_outdated": 0,
"expired": 10,
"not_passed_on": 0,
"not_sent": 0
},
"notices": []
}
```
| Field | Type | Description |
|---|---|---|
| `object` | `"browser_collection"` | The object type. |
| `origins` | array of strings | Your site list. Only these sites can send browser data. |
| `revision` | integer | The site list's revision. It goes up by one with every change. |
| `sites` | array of objects | Every listed site, followed by the sites where the browser script has run without being listed. |
| `sites[].origin` | string | The site. |
| `sites[].listed` | boolean | Whether the site is in your site list. |
| `sites[].first_seen_at` | string or `null` | When the browser script first reported from this site. `null` if it never has. |
| `sites[].last_seen_at` | string or `null` | When the browser script last reported from this site. |
| `sites[].sdk_version` | string or `null` | The [AbuseZero browser SDK](/docs/tools/browser-sdk) version last reported from this site. |
| `issue` | string or `null` | The most pressing problem you can fix, or `null`. See [Issues](#issues). |
| `issue_since` | string or `null` | When the current issue started. |
| `coverage` | object | How your [evaluations](/docs/concepts/evaluations) from the last 7 days arrived, counted by [browser data status](#browser-data-status). `total` is every evaluation counted. |
| `notices` | array of strings | Browser families affected by a known condition that reduces browser data on every site: `chrome`, `edge`, `firefox`, `safari`, `samsung`, `opera` or `other`. AbuseZero publishes and clears these notices. They need no action from you. |
### Browser data status [#browser-data-status]
The same statuses appear on each [evaluation](/docs/api-reference/evaluations#browser-data) as `browser_data`.
| Status | Meaning | Action |
|---|---|---|
| `collected` | Browser data arrived with the evaluation. | None. |
| `consent_declined` | The visitor declined consent, so nothing was collected. | None. |
| `not_loaded` | Your consent tool kept the browser script from loading, and your server sent `unavailable:not_loaded`. | None. |
| `blocked` | The browser couldn't reach AbuseZero, for example because an extension, network or Content Security Policy blocked it. | If it's frequent, check your Content Security Policy against Browser setup. |
| `timed_out` | Collection didn't finish in time. | None. |
| `failed` | Collection failed in the browser. | None. |
| `site_not_listed` | The page's site isn't in your site list. | Add the site. |
| `sdk_outdated` | The page uses a browser SDK version AbuseZero no longer accepts. | Update the browser SDK. |
| `expired` | The browser data reached the evaluation too late to use. | Send the device token sooner after the browser script returns it. |
| `not_passed_on` | The browser script collected data, but the evaluation didn't include it. | Send the script's [`deviceToken`](/docs/tools/browser-sdk#observe) as `identifiers.device_token`. |
| `not_sent` | The evaluation carried no device token. | Send `deviceToken` with every evaluation, whatever its status. |
### Issues [#issues]
`issue` reports a problem only when it has a cause you can fix and it persists across your recent traffic. Expected losses, such as declined consent, never count. AbuseZero also emails the workspace's owners and admins when a problem persists.
| Issue | Meaning | Fix |
|---|---|---|
| `not_passed_on` | The browser script collected data for your claims, but your server sent those evaluations without it. [Device and automation protection](/docs/concepts/browser-protection) can't run on them. | Send the value the browser script returns as `identifiers.device_token`. |
| `site_not_listed` | The browser script is running on a site that isn't in your site list, so it can't collect browser data there. | Add the site to your site list. |
| `sdk_outdated` | Your pages use a version of the browser SDK that AbuseZero no longer accepts, so evaluations arrive without browser data. | Update to the version shown in Developers → Browser setup. |
| `script_missing` | The browser script stopped reporting from all of your sites while evaluations keep arriving. A recent release may have removed it. | Check that the script still loads on your pages. |
## Retrieve browser setup [#retrieve-browser-setup]
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `GET` | `/v1/browser-collection` | [`browser_collection:read`](/docs/api-reference/authentication#scopes) | Not used |
Also available as [`abusezero browser-collection get`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_get_browser_collection`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`browserCollection()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
### Example [#example]
```ts tab="Node.js"
const setup = await abusezero.browserCollection();
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/browser-collection \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
### Response [#response]
Returns `200` with the [browser collection object](#the-browser-collection-object).
## Update the site list [#update-the-site-list]
Replaces your site list. Send every site you want to keep.
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `PATCH` | `/v1/browser-collection` | [`browser_collection:write`](/docs/api-reference/authentication#scopes) | Required |
Also available as [`abusezero browser-collection update`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_update_browser_collection`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`updateBrowserCollection()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
### Request body [#request-body]
| Field | Type | Required | Limits | Description |
|---|---|---|---|---|
| `origins` | array of strings | Yes | Up to 20 HTTPS URLs, each up to 256 characters | The sites allowed to send browser data. Each URL is saved as its origin: scheme, host and port. Paths are dropped and repeated sites are merged. Send an empty array to remove every site. |
| `revision` | integer | No | | The `revision` you last read. When it's sent and the list has changed since, the update is refused, so you never overwrite someone else's change. |
### Example [#example-1]
```ts tab="Node.js"
const setup = await abusezero.updateBrowserCollection(
{
origins: ["https://app.example.com", "https://www.example.com", "https://staging.example.com"],
revision: 3,
},
"browser-sites-rev-3",
);
```
```bash tab="cURL"
curl -X PATCH https://api.abusezero.com/v1/browser-collection \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: browser-sites-rev-3" \
-d '{
"origins": ["https://app.example.com", "https://www.example.com", "https://staging.example.com"],
"revision": 3
}'
```
### Response [#response-1]
Returns `200` with the updated [browser collection object](#the-browser-collection-object). Its `revision` is one higher.
### Errors [#errors]
| Code | Status | Meaning |
|---|---|---|
| [`ORIGIN_NOT_ALLOWED`](/docs/api-reference/errors#invalid-request) | 422 | A site doesn't use HTTPS. |
| [`INVALID_FIELD`](/docs/api-reference/errors#invalid-request) | 422 | A value isn't a URL, is longer than 256 characters, or there are more than 20. |
| [`REVISION_CONFLICT`](/docs/api-reference/errors#conflict) | 409 | The site list changed after you read it. Retrieve it again, reapply your change and retry. |
For every other code, see [REST API errors](/docs/api-reference/errors).
## Related [#related]
What browser data adds to every evaluation.
Install the browser SDK and pass its device token to your server.
Every browser SDK function and option.
# Custom identifiers (/docs/api-reference/custom-identifiers)
Custom identifiers bring your own signals into every [evaluation](/docs/concepts/evaluations): a license ID, an account number, a contract reference from your own records. You hash each value with a secret only you hold, and AbuseZero checks the hash against your [workspace's](/docs/keys-and-access#workspaces) full claim history without ever receiving the raw value. Register each identifier and the version of the secret you hash it with before you send it. See [Custom identifiers](/docs/concepts/custom-identifiers) for the concept and [Send identifiers](/docs/guides/send-identifiers#register-and-send-a-custom-identifier) for hashing.
Examples on this page use a server client:
```ts
import { AbuseZero } from "abusezero/server";
const abusezero = new AbuseZero({ key: process.env.ABUSEZERO_SECRET_KEY!, outage: "throw" });
```
## The custom identifier object [#the-custom-identifier-object]
```json
{
"object": "custom_identifier",
"id": "cid_7d3b1f9a-4e2c-4a8b-b5d6-2f0e8c4a6b19",
"key": "license_id",
"key_version": "2026-10",
"description": "License ID from our billing system",
"created_at": "2026-10-29T10:02:41.000Z",
"updated_at": "2026-10-29T10:02:41.000Z"
}
```
| Field | Type | Description |
|---|---|---|
| `object` | `"custom_identifier"` | The object type. |
| `id` | string | The registration ID, starting with `cid_`. |
| `key` | string | The identifier's name. [Evaluations](/docs/api-reference/evaluations#request-body) send it as `identifiers.custom[].key`. |
| `key_version` | string | The version label of the secret you hash this identifier's values with. Evaluations send it as `identifiers.custom[].value_hash.key_version`. |
| `description` | string | What the identifier is, as you wrote it. |
| `created_at` | string | When the registration was created. |
| `updated_at` | string | When the registration last changed. |
Each registration covers one `key` and `key_version` pair. To rotate the secret behind an identifier, register the same `key` with a new `key_version`, then start hashing with the new secret. A hash made with a different secret is a different value, so [claims](/docs/concepts/evaluations) recorded under the earlier version don't match values hashed under the new one.
## Register a custom identifier [#register-a-custom-identifier]
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `POST` | `/v1/custom-identifiers` | [`custom_identifiers:write`](/docs/api-reference/authentication#scopes) | Required |
Also available as [`abusezero custom-identifiers register`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_register_custom_identifier`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`registerCustomIdentifier()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
### Request body [#request-body]
| Field | Type | Required | Limits | Description |
|---|---|---|---|---|
| `key` | string | Yes | 1 to 64 characters: lowercase letters, digits and underscores, starting with a letter | The identifier's name, such as `license_id`. |
| `key_version` | string | Yes | 1 to 128 printable characters, no spaces | The version label of the secret you hash values with, such as `2026-10`. |
| `description` | string | Yes | 3 to 200 characters | What the identifier is, for your team. |
### Example [#example]
```ts tab="Node.js"
await abusezero.registerCustomIdentifier(
{
key: "license_id",
key_version: "2026-10",
description: "License ID from our billing system",
},
"register-license-id-2026-10",
);
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/custom-identifiers \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: register-license-id-2026-10" \
-d '{
"key": "license_id",
"key_version": "2026-10",
"description": "License ID from our billing system"
}'
```
### Response [#response]
Returns `201`.
```json
{
"object": "custom_identifier"
}
```
The response confirms the registration. To read its ID, [list custom identifiers](#list-custom-identifiers). A retry with the same [`Idempotency-Key`](/docs/api-reference#idempotency) and body returns the same confirmation.
Once registered, the identifier can be sent in [evaluations](/docs/api-reference/evaluations#create-an-evaluation):
```json
{
"identifiers": {
"custom": [
{
"key": "license_id",
"value_hash": {
"algorithm": "hmac-sha256",
"key_version": "2026-10",
"value": "<64 lowercase hex characters>"
}
}
]
}
}
```
The [AbuseZero Node.js SDK's](/docs/tools/server-sdk#hash-phone-numbers-and-custom-identifiers) `createCustomIdentifier` builds this object for you. See [Send identifiers](/docs/guides/send-identifiers).
### Errors [#errors]
| Code | Status | Meaning |
|---|---|---|
| [`INVALID_FIELD`](/docs/api-reference/errors#invalid-request) | 422 | A field is missing or outside its limits. |
| [`IDENTIFIER_ALREADY_REGISTERED`](/docs/api-reference/errors#conflict) | 409 | This `key` and `key_version` pair is already registered. `field` is `key_version`. Register a new key version instead. |
An evaluation that sends a `key` and `key_version` pair that isn't registered returns [`UNSUPPORTED_FIELD`](/docs/api-reference/errors#invalid-request), with the message "Register this custom identifier and key version in Developers → Custom identifiers first." For every other code, see [REST API errors](/docs/api-reference/errors).
## List custom identifiers [#list-custom-identifiers]
Returns registrations, most recently changed first.
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `GET` | `/v1/custom-identifiers` | [`custom_identifiers:read`](/docs/api-reference/authentication#scopes) | Not used |
Also available as [`abusezero custom-identifiers list`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_list_custom_identifiers`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`customIdentifiers()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
### Query parameters [#query-parameters]
| Parameter | Type | Required | Limits | Description |
|---|---|---|---|---|
| `limit` | integer | No | 1 to 100, default 50 | How many registrations to return. |
| `cursor` | string | No | | The `next_cursor` from the previous page, unchanged. |
### Example [#example-1]
```ts tab="Node.js"
const page = await abusezero.customIdentifiers();
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/custom-identifiers \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
### Response [#response-1]
Returns `200`.
```json
{
"object": "list",
"data": [
{
"object": "custom_identifier",
"id": "cid_7d3b1f9a-4e2c-4a8b-b5d6-2f0e8c4a6b19",
"key": "license_id",
"key_version": "2026-10",
"description": "License ID from our billing system",
"created_at": "2026-10-29T10:02:41.000Z",
"updated_at": "2026-10-29T10:02:41.000Z"
}
],
"next_cursor": null
}
```
## Retrieve a custom identifier [#retrieve-a-custom-identifier]
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `GET` | `/v1/custom-identifiers/{id}` | [`custom_identifiers:read`](/docs/api-reference/authentication#scopes) | Not used |
Also available as [`abusezero custom-identifiers get`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_get_custom_identifier`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`call()`](/docs/tools/server-sdk#call-for-any-endpoint) in the AbuseZero Node.js SDK.
### Path parameters [#path-parameters]
| Parameter | Type | Description |
|---|---|---|
| `id` | string | The registration ID, starting with `cid_`. |
### Example [#example-2]
```ts tab="Node.js"
const registration = await abusezero.call("/v1/custom-identifiers/cid_7d3b1f9a-4e2c-4a8b-b5d6-2f0e8c4a6b19");
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/custom-identifiers/cid_7d3b1f9a-4e2c-4a8b-b5d6-2f0e8c4a6b19 \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
### Response [#response-2]
Returns `200` with a [custom identifier object](#the-custom-identifier-object).
### Errors [#errors-1]
| Code | Status | Meaning |
|---|---|---|
| [`RESOURCE_NOT_FOUND`](/docs/api-reference/errors#not-found) | 404 | No registration with this ID exists in the workspace. |
## Remove a custom identifier [#remove-a-custom-identifier]
Removes one `key` and `key_version` registration. [Evaluations](/docs/concepts/evaluations) that send that pair afterwards return [`UNSUPPORTED_FIELD`](/docs/api-reference/errors#invalid-request), so stop sending it first.
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `DELETE` | `/v1/custom-identifiers/{id}` | [`custom_identifiers:write`](/docs/api-reference/authentication#scopes) | Required |
Also available as [`abusezero custom-identifiers delete`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_delete_custom_identifier`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`deleteCustomIdentifier()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
### Path parameters [#path-parameters-1]
| Parameter | Type | Description |
|---|---|---|
| `id` | string | The registration ID, starting with `cid_`. |
### Example [#example-3]
```ts tab="Node.js"
await abusezero.deleteCustomIdentifier(
"cid_7d3b1f9a-4e2c-4a8b-b5d6-2f0e8c4a6b19",
"remove-cid-7d3b1f9a",
);
```
```bash tab="cURL"
curl -X DELETE https://api.abusezero.com/v1/custom-identifiers/cid_7d3b1f9a-4e2c-4a8b-b5d6-2f0e8c4a6b19 \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: remove-cid-7d3b1f9a" \
-d '{}'
```
### Response [#response-3]
Returns `200`. A retry with the same [`Idempotency-Key`](/docs/api-reference#idempotency) returns this same response.
```json
{
"object": "deleted",
"id": "cid_7d3b1f9a-4e2c-4a8b-b5d6-2f0e8c4a6b19",
"deleted": true
}
```
### Errors [#errors-2]
| Code | Status | Meaning |
|---|---|---|
| [`IDENTIFIER_NOT_FOUND`](/docs/api-reference/errors#not-found) | 404 | No registration with this ID exists in the workspace. |
| [`IDENTIFIER_IN_USE`](/docs/api-reference/errors#conflict) | 409 | A rule in a [policy's](/docs/api-reference/policies#the-policy-object) active version uses this identifier. Update those policies first, then remove it. |
## Related [#related]
What custom identifiers add to every evaluation.
Hash values and send them with evaluations.
Use custom identifiers in your own rules.
# Data deletions (/docs/api-reference/data-deletions)
Data deletions remove one user's claim history from your [workspace](/docs/keys-and-access#workspaces): their [claims](/docs/concepts/evaluations), the [evaluations](/docs/api-reference/evaluations#the-evaluation-object) of those claims and their [outcomes](/docs/api-reference/outcomes#the-outcome-object). Because [import](/docs/api-reference/imports) and [export](/docs/api-reference/exports) files can hold that user's data, a deletion also removes every export and every uploaded import file in the workspace. Preview a deletion first to see exactly what it covers, then request it. The deletion runs in the background. While it runs, AbuseZero refuses new evaluations for that user and new imports in the workspace. Every other user's evaluations continue as normal. For a walkthrough, see [Export and delete data](/docs/guides/export-and-delete-data).
Deletion is permanent and can't be undone. Once a user's history is gone, their earlier claims no longer count against them, so they can qualify for your [benefits](/docs/concepts/benefits) again.
Only deletions of one user's history are available through the REST API. To delete a whole workspace, the workspace owner uses the dashboard.
With an OAuth token, every endpoint on this page also needs the signed-in member to have [permission](/docs/api-reference/authentication#member-permissions-for-oauth) to delete data.
Examples on this page use a server client:
```ts
import { AbuseZero } from "abusezero/server";
const abusezero = new AbuseZero({ key: process.env.ABUSEZERO_SECRET_KEY!, outage: "throw" });
```
## The deletion object [#the-deletion-object]
```json
{
"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"
}
```
| Field | Type | Description |
|---|---|---|
| `object` | `"deletion"` | The object type. |
| `id` | string | The deletion ID, starting with `del_`. |
| `kind` | `subject` or `workspace` | `subject` deletes one user's history. `workspace` deletions are requested in the dashboard. |
| `state` | `pending` or `complete` | `pending` is still running. `complete` has finished. |
| `created_at` | string | When the deletion was requested. |
| `completed_at` | string or `null` | When the deletion finished. |
## The selector [#the-selector]
Every request names the user with a `selector`.
| Field | Type | Required | Limits | Description |
|---|---|---|---|---|
| `selector.kind` | `"subject"` | Yes | | Delete one user's history. |
| `selector.user` | string | Yes | 1 to 254 characters | The user to delete. |
| `selector.email` | boolean | Yes | | `true` when `user` is an email address. `false` when it's your own user ID. |
Name the user the way your [evaluations](/docs/api-reference/evaluations#request-body) did. If they sent `subject.external_id`, use that ID with `email: false`. If they sent only an email address, use the address with `email: true`. With `email: true`, a `user` that isn't a supported email address returns `INVALID_EMAIL`.
## Preview a deletion [#preview-a-deletion]
Shows what a deletion would cover. A preview changes nothing.
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `POST` | `/v1/data-deletions/preview` | [`data_deletions:write`](/docs/api-reference/authentication#scopes) | Not used |
Also available as [`abusezero data-deletions preview`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_preview_data_deletion`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`previewDataDeletion()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
### Request body [#request-body]
| Field | Type | Required | Description |
|---|---|---|---|
| `selector` | object | Yes | The [selector](#the-selector) for the user. |
### Example [#example]
```ts tab="Node.js"
const preview = await abusezero.previewDataDeletion({
selector: { kind: "subject", user: "user_4821", email: false },
});
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/data-deletions/preview \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "selector": { "kind": "subject", "user": "user_4821", "email": false } }'
```
### Response [#response]
Returns `201`.
```json
{
"object": "deletion_preview",
"counts": {
"claims": 3,
"outcomes": 2,
"files": 4
},
"confirmation": "5d41402abc4b2a76b9719d911017c592ae5f0c1e7d3b8a6f4c2e9d0b1a3f5e7c"
}
```
| Field | Type | Description |
|---|---|---|
| `counts.claims` | integer | The user's [claims](/docs/concepts/evaluations) that match. |
| `counts.outcomes` | integer | The user's [outcomes](/docs/concepts/outcomes) that match. |
| `counts.files` | integer | Finished [export](/docs/api-reference/exports) files and uploaded [import](/docs/api-reference/imports) files stored in the workspace. The deletion removes all of them. |
| `confirmation` | string | Identifies this preview. It changes whenever the selector or the counts change, so two previews with the same value describe the same deletion. |
## Request a deletion [#request-a-deletion]
Starts deleting the user's history.
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `POST` | `/v1/data-deletions` | [`data_deletions:write`](/docs/api-reference/authentication#scopes) | Required |
Also available as [`abusezero data-deletions create`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_request_data_deletion`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`requestDataDeletion()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
### Request body [#request-body-1]
| Field | Type | Required | Description |
|---|---|---|---|
| `selector` | object | Yes | The [selector](#the-selector) for the user. |
### Example [#example-1]
```ts tab="Node.js"
const deletion = await abusezero.requestDataDeletion(
{ selector: { kind: "subject", user: "user_4821", email: false } },
"delete-user-4821",
);
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/data-deletions \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: delete-user-4821" \
-d '{ "selector": { "kind": "subject", "user": "user_4821", "email": false } }'
```
### Response [#response-1]
Returns `202`. Retrieve the deletion at `status_url` until its `state` is `complete`.
```json
{
"object": "deletion",
"id": "del_8b4f0d2a-6c1e-4a7b-9d3f-5e2a8c0b4d16",
"status": "pending",
"status_url": "/v1/data-deletions/del_8b4f0d2a-6c1e-4a7b-9d3f-5e2a8c0b4d16"
}
```
### Errors [#errors]
| Code | Status | Meaning |
|---|---|---|
| [`WORKSPACE_DELETION_EXCLUDED`](/docs/api-reference/errors#permission) | 403 | The selector asks to delete the whole workspace. Use the dashboard. |
| [`PERMISSION_REQUIRED`](/docs/api-reference/errors#permission) | 403 | With an OAuth token, the signed-in member doesn't have permission to delete data. Ask the workspace owner. |
| [`INVALID_EMAIL`](/docs/api-reference/errors#invalid-request) | 422 | `email` is `true`, but `user` isn't a supported email address. |
| [`IMPORTS_IN_PROGRESS`](/docs/api-reference/errors#conflict) | 409 | An [import](/docs/api-reference/imports#import-states) in the workspace isn't `complete` or `cancelled`. Let it finish, or [cancel it](/docs/api-reference/imports#cancel-an-import), then retry. |
| [`DELETION_IN_PROGRESS`](/docs/api-reference/errors#conflict) | 409 | This user's history, or the workspace, is already being deleted. Retry after that deletion completes. |
For every other code, see [REST API errors](/docs/api-reference/errors).
## Retrieve a deletion [#retrieve-a-deletion]
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `GET` | `/v1/data-deletions/{id}` | [`data_deletions:read`](/docs/api-reference/authentication#scopes) | Not used |
Also available as [`abusezero data-deletions get`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_get_data_deletion`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`call()`](/docs/tools/server-sdk#call-for-any-endpoint) in the AbuseZero Node.js SDK.
### Path parameters [#path-parameters]
| Parameter | Type | Description |
|---|---|---|
| `id` | string | The deletion ID, starting with `del_`. |
### Example [#example-2]
```ts tab="Node.js"
const deletion = await abusezero.call("/v1/data-deletions/del_8b4f0d2a-6c1e-4a7b-9d3f-5e2a8c0b4d16");
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/data-deletions/del_8b4f0d2a-6c1e-4a7b-9d3f-5e2a8c0b4d16 \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
### Response [#response-2]
Returns `200` with a [deletion object](#the-deletion-object).
### Errors [#errors-1]
| Code | Status | Meaning |
|---|---|---|
| [`RESOURCE_NOT_FOUND`](/docs/api-reference/errors#not-found) | 404 | No deletion with this ID exists in the workspace. |
## List deletions [#list-deletions]
Returns deletions newest first.
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `GET` | `/v1/data-deletions` | [`data_deletions:read`](/docs/api-reference/authentication#scopes) | Not used |
Also available as [`abusezero data-deletions list`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_list_data_deletions`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`dataDeletions()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
### Query parameters [#query-parameters]
| Parameter | Type | Required | Limits | Description |
|---|---|---|---|---|
| `limit` | integer | No | 1 to 100, default 50 | How many deletions to return. |
| `cursor` | string | No | | The `next_cursor` from the previous page, unchanged. |
### Example [#example-3]
```ts tab="Node.js"
const page = await abusezero.dataDeletions();
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/data-deletions \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
### Response [#response-3]
Returns `200`.
```json
{
"object": "list",
"data": [
{
"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"
}
],
"next_cursor": null
}
```
## Related [#related]
Export what you need before you delete it.
Finish or cancel open imports before deleting a user's history.
# REST API errors (/docs/api-reference/errors)
Every error from the AbuseZero REST API uses the same envelope, on every endpoint. Your code can branch on `type`, act on `code`, and show `message` to your team.
## Error envelope [#error-envelope]
```json
{
"error": {
"type": "conflict",
"code": "IDEMPOTENCY_CONFLICT",
"message": "Use the original payload with this key, or a new key for a new evaluation.",
"retryable": false
},
"request_id": "7d0e5c2a-1f4b-4e8a-b6c3-9a2d5e0f1b74"
}
```
| Field | Type | Description |
|---|---|---|
| `error.type` | string | The error's category. See [Error types](#error-types). |
| `error.code` | string | A stable, machine-readable code. Codes never change meaning within `v1`. |
| `error.message` | string | A readable explanation with the next step. Messages can be reworded at any time, so don't parse them. |
| `error.field` | string | The request field that caused the error, as a dotted path such as `identifiers.device_token`. Present only when one field caused it. |
| `error.retryable` | boolean | `true` when the same request can succeed if you retry it unchanged. |
| `request_id` | string | The request's ID. It matches the `X-Request-ID` response header. |
## Error types [#error-types]
| `type` | HTTP status | Meaning |
|---|---|---|
| `invalid_request` | 400, 413, 422 | The request can't be accepted as sent. Fix it before retrying. |
| `authentication` | 401 | The credential is missing, invalid or expired. |
| `permission` | 403 | The credential is valid but can't do this. |
| `not_found` | 404, 410 | The endpoint or object doesn't exist in this [workspace](/docs/keys-and-access#workspaces), or is no longer available. |
| `conflict` | 409 | The request conflicts with the current state of an object. |
| `rate_limit` | 429 | The request went over a traffic limit. |
| `unavailable` | 503, 504 | AbuseZero couldn't finish the request. Retry it. |
New codes can be added within `v1`. Handle an unknown code by its `type`.
## Handling errors [#handling-errors]
- **`retryable: true`:** retry the same request with the same `Idempotency-Key`. Back off between attempts. When the response has a `Retry-After` header, wait that many seconds first.
- **`invalid_request`:** fix the request. Retrying it unchanged returns the same error.
- **`authentication` and `permission`:** fix the credential, its scopes or the member's permissions.
- **`conflict`:** read the object's current state, then decide what to send.
- **[Evaluations](/docs/concepts/evaluations):** never treat an error as an allow. Use the Node.js SDK's `outage` setting to decide what your code receives when AbuseZero can't be reached. See [Handle outages and retries](/docs/guides/outages-and-retries).
```ts
import { AbuseZero, AbuseZeroError, type EvaluationRequest } from "abusezero/server";
const abusezero = new AbuseZero({
key: process.env.ABUSEZERO_SECRET_KEY!,
outage: "throw",
});
export async function evaluateClaim(request: EvaluationRequest, idempotencyKey: string) {
try {
return await abusezero.evaluate(request, idempotencyKey);
} catch (error) {
if (error instanceof AbuseZeroError) {
console.error(error.code, error.message, error.requestId);
if (error.retryable) {
// Retry later with the same idempotency key.
}
}
throw error;
}
}
```
## Error codes [#error-codes]
### Invalid request [#invalid-request]
| Status | Code | Meaning | What to do |
|---|---|---|---|
| 400 | [`MALFORMED_JSON`](/docs/troubleshooting#invalid_field-unsupported_field-or-malformed_json) | The body isn't valid JSON, or isn't sent as JSON. | Send a JSON object with `Content-Type: application/json`. |
| 413 | `PAYLOAD_TOO_LARGE` | The body is over the size limit. | Keep the body under the limit given in the message. |
| 422 | [`INVALID_FIELD`](/docs/troubleshooting#invalid_field-unsupported_field-or-malformed_json) | A field is missing or has an invalid value. | Fix the field named in `error.field`. |
| 422 | [`UNSUPPORTED_FIELD`](/docs/troubleshooting#invalid_field-unsupported_field-or-malformed_json) | The request includes a field the endpoint doesn't define, a rule uses an unsupported condition, or a [custom identifier](/docs/concepts/custom-identifiers) isn't registered. | Remove the field, choose a supported condition, or register the custom identifier and key version in Developers → Custom identifiers first. |
| 422 | [`IDEMPOTENCY_KEY_REQUIRED`](/docs/troubleshooting#idempotency_key_required-422) | A write was sent without a valid `Idempotency-Key` header. | Send 1 to 128 printable characters with no spaces. |
| 422 | [`CURSOR_INVALID`](/docs/troubleshooting#cursor_invalid-422) | The page cursor was changed, or belongs to another list or workspace. | Clear the cursor and request the first page again. |
| 422 | `INVALID_EMAIL` | An email address isn't in a supported form. Quoted and international local parts aren't supported. `field` names where it was sent: `subject.email` on an [evaluation](/docs/concepts/evaluations), `value` on an [allow or block list](/docs/concepts/allow-and-block-lists). | Send an address such as `name@example.com`. |
| 422 | `INVALID_IP` | An IP address isn't a valid IPv4 or IPv6 address. `field` names where it was sent: `context.ip` on an evaluation, `value` on an allow or block list. | Send a valid address, or leave the IP out. |
| 422 | [`IP_NOT_ROUTABLE`](/docs/troubleshooting#ip_not_routable-422) | `context.ip` is a private, loopback, documentation or other non-routable address. | Send the user's public IP address, or leave the IP out. |
| 422 | `DEVICE_TOKEN_INVALID` | `identifiers.device_token` isn't a value the browser SDK returned. | Send the browser SDK's `deviceToken` unchanged, or `unavailable:not_loaded` when your consent tool kept the script from loading. |
| 422 | `DEVICE_TOKEN_SCOPE` | The [device token](/docs/concepts/browser-protection) was collected for a different `claim_id`. | Collect browser data with the same claim ID you evaluate. |
| 422 | `BROWSER_SDK_UPGRADE_REQUIRED` | The device token came from an outdated browser SDK. | Update the browser SDK and collect new browser data. |
| 422 | [`BOT_EVIDENCE_REQUIRED`](/docs/troubleshooting#bot_evidence_required-422) | The [benefit](/docs/concepts/benefits) requires a bot check, and the evaluation has none that can be used. | Collect browser data with `prepareBotObservation` when the person submits, and evaluate within 2 minutes. |
| 422 | `AGENT_EVIDENCE_SCOPE` | `agent_evidence` was produced for a different claim. | Verify the agent's request with this evaluation's claim ID. |
| 422 | [`AGENT_EVIDENCE_OUTDATED`](/docs/troubleshooting#agent_evidence_outdated-422) | `agent_evidence` came from a Node.js SDK before 0.11.0. | Update the Node.js SDK to 0.11.0 or later and send the token `verifyAgentRequest()` returns, unchanged. |
| 422 | [`REASON_REQUIRED`](/docs/troubleshooting#reason_required-422) | A `not_granted` or `reversed` [outcome](/docs/concepts/outcomes) report has no `reason`. | Add a reason of 3 to 500 characters. |
| 422 | `POLICY_NOT_FOUND` | A benefit names a [policy](/docs/concepts/policies-and-rules) key that doesn't exist, or a policy builds on an unavailable [protection level](/docs/concepts/protection-levels). | Choose an existing policy key or protection level. |
| 422 | `ORIGIN_NOT_ALLOWED` | A site in your site list isn't HTTPS. | List only HTTPS sites. |
| 422 | `INVALID_DOMAIN` | A list entry's domain isn't valid. `field` is `value`. | Use a domain such as `example.com`. |
| 422 | `INVALID_TIMEZONE` | A benefit's calendar eligibility names a time zone AbuseZero doesn't recognize. `field` is `config.eligibility.timezone`. | Use an IANA time zone such as `America/New_York`. |
### Authentication [#authentication]
| Status | Code | Meaning | What to do |
|---|---|---|---|
| 401 | `AUTHENTICATION_REQUIRED` | The request has no credential. | Send `Authorization: Bearer` with a key or access token. |
| 401 | [`INVALID_API_KEY`](/docs/troubleshooting#requests-fail-with-invalid_api_key-or-api_key_expired-401) | The key is unknown or revoked. | Create a new key in Developers. |
| 401 | [`API_KEY_EXPIRED`](/docs/troubleshooting#requests-fail-with-invalid_api_key-or-api_key_expired-401) | The key passed its expiry. | Create a new key in Developers and replace the old one. |
| 401 | `INVALID_ACCESS_TOKEN` | The OAuth access token is invalid or expired. A value that isn't a complete API key is read as an access token. | Sign in again, then retry. If you meant to send an API key, check that it was copied in full. |
### Permission [#permission]
| Status | Code | Meaning | What to do |
|---|---|---|---|
| 403 | [`KEY_TYPE_NOT_ALLOWED`](/docs/troubleshooting#requests-fail-with-key_type_not_allowed-403) | The wrong kind of key was used: a publishable key on a server endpoint, or a server key in the browser. | Use a secret or restricted key on your server, and the publishable key in the browser. |
| 403 | [`KEY_PERMISSION_DENIED`](/docs/troubleshooting#requests-fail-with-key_permission_denied-403) | The API key lacks the endpoint's scope. | Use a key with the scope named in the message. |
| 403 | [`OAUTH_SCOPE_REQUIRED`](/docs/troubleshooting#oauth-requests-fail-with-oauth_scope_required-or-permission_required-403) | The access token lacks the endpoint's scope. | Authorize the scope named in the message. |
| 403 | [`WORKSPACE_REQUIRED`](/docs/troubleshooting#oauth-requests-fail-with-workspace_required-or-workspace_access_denied-403) | An OAuth request has no valid `AbuseZero-Workspace` header. | Send a `ws_` ID from `GET /v1/workspaces`. |
| 403 | [`WORKSPACE_ACCESS_DENIED`](/docs/troubleshooting#oauth-requests-fail-with-workspace_required-or-workspace_access_denied-403) | The credential can't reach that workspace. | Choose a workspace the credential belongs to. |
| 403 | [`PERMISSION_REQUIRED`](/docs/troubleshooting#oauth-requests-fail-with-oauth_scope_required-or-permission_required-403) | The signed-in member lacks a permission the scope needs. | Ask the workspace owner for the permission. |
| 403 | `EVIDENCE_SCOPE_REQUIRED` | An export with evidence needs the `evaluation_evidence:read` scope. | Add the scope, or export without evidence. |
| 403 | [`ONBOARDING_REQUIRED`](/docs/troubleshooting#requests-fail-with-onboarding_required-403) | The workspace hasn't finished setup. | Ask the workspace owner to finish onboarding in the dashboard. |
| 403 | `ORIGIN_NOT_REGISTERED` | The browser SDK ran on a site that isn't in your site list. | Add the site in Developers → Browser setup. Only HTTPS sites can send browser data. |
| 403 | `WORKSPACE_DELETION_EXCLUDED` | A data deletion tried to delete the whole workspace. | Delete workspaces in the dashboard. |
### Not found [#not-found]
| Status | Code | Meaning | What to do |
|---|---|---|---|
| 404 | `ENDPOINT_NOT_FOUND` | No endpoint matches the method and path. | Check the method and path. |
| 404 | `RESOURCE_NOT_FOUND` | No object with this ID exists in the workspace. | Check the ID and the workspace. |
| 404 | `EVALUATION_NOT_FOUND` | No evaluation with this ID exists in the workspace. | Check the evaluation ID. |
| 404 | [`BENEFIT_NOT_FOUND`](/docs/troubleshooting#benefit_not_found-404) | No benefit with this key or ID exists in the workspace. | Check the benefit key or ID, or create the benefit. |
| 404 | `POLICY_NOT_FOUND` | No policy with this ID or key exists in the workspace. | Check the policy ID or key. |
| 404 | `POLICY_VERSION_NOT_FOUND` | The policy has no such version. | List the policy's versions and choose one. |
| 404 | `IDENTIFIER_NOT_FOUND` | No custom identifier with this ID exists. | Check the custom identifier's ID. |
| 404 | `IMPORT_NOT_FOUND` | No import with this ID exists in the workspace. | Check the import ID. |
| 404 | `EXPORT_NOT_FOUND` | The export doesn't exist or has expired. | Request a new export. |
| 410 | `IMPORT_REPORT_EXPIRED` | The import's error report is no longer available. | Upload the corrected CSV again. |
### Conflict [#conflict]
| Status | Code | Meaning | What to do |
|---|---|---|---|
| 409 | [`IDEMPOTENCY_CONFLICT`](/docs/troubleshooting#idempotency_conflict-409) | The `Idempotency-Key` was already used with a different body. | Send the original body again with this key, or use a new key for a new request. |
| 409 | [`EVALUATION_PENDING`](/docs/troubleshooting#temporarily_unavailable-503-deadline_exceeded-504-or-evaluation_pending-409) | An evaluation with this key is still running. | Retry with the same `Idempotency-Key`. |
| 409 | [`CLAIM_CONFLICT`](/docs/troubleshooting#claim_conflict-409) | The `claim_id` already belongs to a different user. | Use a new claim ID for a different award. |
| 409 | [`CLAIM_PERIOD_ENDED`](/docs/troubleshooting#claim_period_ended-409) | The `claim_id` belongs to an earlier eligibility period. | Use a new claim ID for the new period's award. |
| 409 | `OUTCOME_ALREADY_RECORDED` | An imported outcome already exists for this claim. | Reconcile the claim in your application. |
| 409 | [`OUTCOME_CONFLICT`](/docs/troubleshooting#reporting-an-outcome-fails-with-outcome_conflict-409) | The outcome can't move to the requested state, the evaluation was denied or replaced, the claim's earlier approval expired, or the external outcome ID belongs elsewhere. | Read the evaluation and outcome, then report the correct state. See [Outcomes](/docs/api-reference/outcomes). |
| 409 | `DEVICE_TOKEN_USED` | The device token was already used for another claim. | Collect new browser data for this claim. |
| 409 | `DELETION_IN_PROGRESS` | The user's history, or the workspace, is being deleted. | Retry after the deletion finishes. |
| 409 | `POLICY_UNAVAILABLE` | The benefit's policy isn't available. | Assign an available policy to the benefit. |
| 409 | `BENEFIT_CONFLICT` | The benefit changed since you read it. | Read the benefit again and send its current `revision`. |
| 409 | `BENEFIT_SCOPE_FIXED` | The update changes the benefit's eligibility. | Create a separate benefit for a different eligibility period. |
| 409 | `POLICY_CONFLICT` | The policy changed since you read it. | Read the policy again and send its current `revision`. |
| 409 | `BENEFIT_KEY_TAKEN` | Another benefit already uses this key. `field` is `key`. | Choose a different benefit key. |
| 409 | `POLICY_KEY_TAKEN` | Another policy already uses this key. | Choose a different key. |
| 409 | `POLICY_IN_USE` | Benefits still use the policy. | Move those benefits to another policy first. |
| 409 | `PROTECTION_LEVEL_READ_ONLY` | Protection levels can't be edited or deleted. | Duplicate the level and change the copy. |
| 409 | `IDENTIFIER_ALREADY_REGISTERED` | This custom identifier key and key version are already registered. `field` is `key_version`. | Register a new key version. |
| 409 | `IDENTIFIER_IN_USE` | A policy still uses the custom identifier. | Remove it from your policies first. |
| 409 | `REVISION_CONFLICT` | Your site list changed since you read it. | Read browser setup again and send its current `revision`. |
| 409 | `EXPORT_NOT_READY` | The export hasn't finished. | Wait until its state is complete, then download it. |
| 409 | `IMPORT_STATE` | The import isn't in a state that allows this step. | Read the import and follow its current state. |
| 409 | `IMPORT_NOT_UPLOADED` | Validation was requested before the CSV file was uploaded. | Upload the file to the import's upload URL, then validate. |
| 409 | `IMPORT_COMPLETE` | The import already finished, so it can't be cancelled. | To remove its history, delete the affected users' data. |
| 409 | `IMPORTS_IN_PROGRESS` | Active imports block a data deletion. | Finish or cancel them first. |
### Rate limit [#rate-limit]
| Status | Code | Meaning | What to do |
|---|---|---|---|
| 429 | [`RATE_LIMITED`](/docs/troubleshooting#rate_limited-429) | The request went over a traffic limit. | Wait for the number of seconds in `Retry-After`, then retry with the same `Idempotency-Key`. |
### Unavailable [#unavailable]
| Status | Code | Meaning | What to do |
|---|---|---|---|
| 503 | [`TEMPORARILY_UNAVAILABLE`](/docs/troubleshooting#temporarily_unavailable-503-deadline_exceeded-504-or-evaluation_pending-409) | AbuseZero couldn't finish the request. | Retry with the same `Idempotency-Key`. |
| 503 | `LIMIT_CONFIGURATION_UNAVAILABLE` | AbuseZero couldn't confirm the workspace's traffic limits. | Retry shortly. |
| 503 | `BOT_EVIDENCE_UNAVAILABLE` | The bot check couldn't be completed for this evaluation. | Collect new browser data with `prepareBotObservation`, then retry. |
| 504 | [`DEADLINE_EXCEEDED`](/docs/troubleshooting#temporarily_unavailable-503-deadline_exceeded-504-or-evaluation_pending-409) | The request reached its time limit. | Retry with the same `Idempotency-Key`. If the first attempt finished, you get its result. |
## Skipped evaluations are not errors [#skipped-evaluations-are-not-errors]
When a [workspace](/docs/keys-and-access#workspaces) can't run an [evaluation](/docs/concepts/evaluations), for example because it has run out of credits, `POST /v1/evaluations` returns `200` with `status: "skipped"` instead of an error. No allow or deny was decided. See [Evaluations](/docs/api-reference/evaluations#skipped-evaluations).
# Evaluations (/docs/api-reference/evaluations)
An [evaluation](/docs/concepts/evaluations) decides one [benefit](/docs/concepts/benefits) claim. You send what you know about the user and the claim. AbuseZero checks it against your [policy](/docs/concepts/policies-and-rules), your [workspace](/docs/keys-and-access#workspaces)'s full claim history and every signal family, and returns one final result, `allow` or `deny`, within the evaluation's time limit.
Every evaluation is saved before its response is sent. It keeps the exact policy version it ran under, so you can always explain a past decision, even after your rules change.
| Endpoint | Scope |
|---|---|
| [`POST /v1/evaluations`](#create-an-evaluation) | `evaluations:create` |
| [`GET /v1/evaluations`](#list-evaluations) | `evaluations:read` |
| [`GET /v1/evaluations/{id}`](#retrieve-an-evaluation) | `evaluations:read` |
| [`GET /v1/evaluations/{id}/evidence`](#retrieve-an-evaluations-evidence) | `evaluation_evidence:read` |
| [`GET /v1/evaluations/{id}/graph`](#retrieve-an-evaluations-association-graph) | `evaluations:read` |
To report what happened after an evaluation, see [Outcomes](/docs/api-reference/outcomes).
## The evaluation object [#the-evaluation-object]
`GET /v1/evaluations/{id}` returns the full [evaluation](/docs/concepts/evaluations) object.
| Field | Type | Description |
|---|---|---|
| `object` | string | Always `evaluation`. |
| `id` | string | The evaluation's ID, starting with `eval_`. |
| `benefit` | string or `null` | The [benefit](/docs/api-reference/benefits#the-benefit-object)'s key. See [Benefits](/docs/concepts/benefits). |
| `claim_id` | string or `null` | The claim ID you sent. |
| `result` | string | `allow` or `deny`. In observe mode this is always `allow`. |
| `reason` | object or `null` | Why the result was reached. See [Reason](#reason). |
| `outcome` | object | The claim's [outcome](/docs/concepts/outcomes): `state` and `due_at`. See [Outcomes](/docs/api-reference/outcomes). |
| `policy_version` | integer or `null` | The [version](/docs/api-reference/policies#retrieve-a-policy-version) of the [policy](/docs/concepts/policies-and-rules) the evaluation ran under. |
| `created_at` | string | When the evaluation ran. |
| `mode` | string | `protect` or `observe`: the benefit's protection mode when the evaluation ran. |
| `observed_result` | string | Observe mode only. The result your policy reached: `allow` or `deny`. |
| `risk` | object | The AbuseZero Risk Score. See [Risk](#risk). |
| `signals` | array | Evidence only. The assessment of each signal family, returned by [Retrieve an evaluation's evidence](#retrieve-an-evaluations-evidence). See [Signals](#signals). |
| `inputs` | object | Which inputs the evaluation received. See [Inputs](#inputs). |
| `browser_data` | string or `null` | What the evaluation received from the browser SDK. See [Browser data](#browser-data). |
| `policy` | object | The policy the evaluation ran under: `id` (starting with `pol_`) and `version`. |
### Reason [#reason]
| Field | Type | Description |
|---|---|---|
| `code` | string | A stable reason code, such as `prior_claim_email` or `custom_rule`. See [Reason codes](/docs/reason-codes). |
| `message` | string | A readable explanation you can show your team. |
| `rule` | string | Present when one of your own rules decided the result. The ID of that rule. |
### Risk [#risk]
| Field | Type | Description |
|---|---|---|
| `score` | number or `null` | The AbuseZero Risk Score, from 0 to 100. `null` when the result was decided without a score, for example by one of your rules, a list entry or a [protection level](/docs/concepts/protection-levels). |
| `band` | string | `low`, `elevated`, `high` or `not_assessed`. `not_assessed` means no score was produced. The result still stands. |
### Signals [#signals]
An array with one entry for each signal family. It appears only in [an evaluation's evidence](#retrieve-an-evaluations-evidence), which needs the [`evaluation_evidence:read`](/docs/api-reference/authentication#scopes) scope, and on the evaluation in the dashboard. Evaluation and preview responses don't include it.
| Field | Type | Description |
|---|---|---|
| `family` | string | `identity`, `device`, `network`, `automation` or `benefit_history`. |
| `assessment` | string | `clear`, `elevated`, `high` or `unavailable`. `unavailable` means the evaluation had no evidence for this family. |
See [Risk Score and signals](/docs/concepts/risk-score-and-signals).
### Inputs [#inputs]
Each key is `received` or `missing`.
| Key | Received when you send |
|---|---|
| `email` | `subject.email` |
| `ip` | `context.ip` |
| `user_id` | `subject.external_id`. `missing` when the evaluation sent none. Without it, the email address still identifies the user. Evaluations recorded before 29 October 2026 read `received`. |
| `phone` | `identifiers.phone_hash` |
| `payment` | `identifiers.payment` |
| `device` | `identifiers.device_id`, or a [device token](/docs/concepts/browser-protection) with browser data |
| `browser` | `identifiers.browser_cookie_id`, or a device token with browser data |
`missing` inputs are the first thing to check when an integration behaves unexpectedly.
### Browser data [#browser-data]
| Value | Meaning |
|---|---|
| `collected` | Browser data arrived with the evaluation. |
| `consent_declined` | The visitor declined consent, so nothing was collected. |
| `not_loaded` | Your consent tool kept the browser script from loading. |
| `blocked` | The visitor's browser or an extension blocked collection. |
| `timed_out` | Collection didn't finish in time. |
| `failed` | Collection failed in the browser. |
| `site_not_listed` | The page's site isn't in your site list. Add it in Developers → Browser setup. |
| `sdk_outdated` | The page uses an outdated browser SDK. Update it. |
| `expired` | The device token reached AbuseZero after it expired. Send it sooner. |
| `not_passed_on` | Browser data was collected for this claim, but the evaluation didn't include its device token. Pass `deviceToken` to your server and send it. |
| `not_sent` | The evaluation carried no device token. |
You can fix `site_not_listed`, `sdk_outdated` and `not_passed_on`. The others are expected for some visitors, and evaluations still run without browser data.
## Create an evaluation [#create-an-evaluation]
`POST /v1/evaluations` · Scope [`evaluations:create`](/docs/api-reference/authentication#scopes) · Requires [`Idempotency-Key`](/docs/api-reference#idempotency)
Also available as [`abusezero evaluations create`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_create_evaluation`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`evaluate()`](/docs/tools/server-sdk#evaluate-a-claim) in the AbuseZero Node.js SDK.
Evaluates one [benefit](/docs/concepts/benefits) claim and returns `allow` or `deny`. Call it from your server before you grant the benefit. Each [evaluation](/docs/concepts/evaluations) uses [workspace](/docs/keys-and-access#workspaces) credits.
### Request body [#request-body]
| Field | Type | Required | Description |
|---|---|---|---|
| `benefit` | string | Yes | The benefit's key. Lowercase letters, numbers and underscores, starting with a letter; up to 64 characters. |
| `claim_id` | string | Yes | Your ID for this claim, 1 to 128 printable characters with no spaces. Use one claim ID per award. See [Evaluating the same claim again](#re-evaluating-a-claim_id). |
| `subject` | object | Yes | The user making the claim. |
| `subject.email` | string | Yes | The user's email address, up to 254 characters. |
| `subject.external_id` | string | No | Your stable ID for the user, 1 to 128 characters. Recommended. Without it, AbuseZero identifies the user by email. |
| `subject.email_verified` | boolean | No | `true` when your app has verified the email address. |
| `context` | object or `null` | No | Details of the request your user made. |
| `context.ip` | string or `null` | No | The user's public IP address, IPv4 or IPv6. Send the user's address, not your server's. Private, loopback and documentation addresses return `IP_NOT_ROUTABLE`. |
| `context.user_agent` | string | No | The user's `User-Agent`, up to 512 characters. |
| `context.session_id` | string | No | Your session ID for the user, 1 to 128 characters. |
| `identifiers` | object | No | Further identifiers for the user and device. |
| `identifiers.device_token` | string | No | The `deviceToken` the browser SDK returned, up to 4,096 characters. Send it unchanged, whatever its status. Send `unavailable:not_loaded` when your consent tool kept the script from loading. |
| `identifiers.device_id` | string | No | Your own device ID, such as a mobile installation ID, 1 to 128 characters. |
| `identifiers.browser_cookie_id` | string | No | Your own first-party browser cookie ID, 1 to 128 characters. |
| `identifiers.payment` | object | No | The user's payment method. All three fields are required when you send it. |
| `identifiers.payment.provider` | string | Yes, in `payment` | The name of your payment processor, 1 to 128 printable characters. |
| `identifiers.payment.scope` | string | Yes, in `payment` | The account at that processor in which the fingerprint is unique, 1 to 128 printable characters. |
| `identifiers.payment.fingerprint` | string | Yes, in `payment` | The processor's fingerprint for the payment method, 1 to 128 characters. |
| `identifiers.phone_hash` | object | No | The user's phone number as a keyed hash. Create it with `createPhoneHash` from `abusezero/server`. Requires `phone_verified`. |
| `identifiers.phone_verified` | boolean | With `phone_hash` | `true` when your app has verified the phone number. Send it together with `phone_hash`. |
| `identifiers.custom` | array | No | Up to 10 [custom identifiers](/docs/concepts/custom-identifiers), each `{ "key", "value_hash" }`. Create each with `createCustomIdentifier` from `abusezero/server`. The key and its key version must be [registered](/docs/api-reference/custom-identifiers) first. |
| `agent_evidence` | string | No | The opaque `AgentEvidence` token `verifyAgentRequest` returned for this claim. Send it unchanged. See [Verify AI agents](/docs/guides/verify-ai-agents). |
Phone numbers and custom identifiers travel as keyed hashes, so AbuseZero matches them without ever receiving the raw value. See [Send identifiers](/docs/guides/send-identifiers).
### Example [#example]
```ts tab="Node.js"
import { randomUUID } from "node:crypto";
import { AbuseZero } from "abusezero/server";
const abusezero = new AbuseZero({
key: process.env.ABUSEZERO_SECRET_KEY!,
outage: "throw",
});
// deviceToken is the value the browser SDK returned on your page.
export async function evaluateTrial(deviceToken: string) {
// Reuse this key if you retry this attempt.
const idempotencyKey = randomUUID();
const evaluation = await abusezero.evaluate(
{
benefit: "pro_trial",
claim_id: "trial_user_4821",
subject: {
external_id: "user_4821",
email: "jordan@example.com",
email_verified: true,
},
context: { ip: "203.0.113.42" },
identifiers: { device_token: deviceToken },
},
idempotencyKey,
);
if ("status" in evaluation) {
// Skipped: no allow or deny was decided.
} else if (evaluation.result === "allow") {
// Grant the trial.
}
return evaluation;
}
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/evaluations \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 0b8f5c1e-4d2a-4c7b-9e3f-6a1d8b2c5e70" \
-d '{
"benefit": "pro_trial",
"claim_id": "trial_user_4821",
"subject": {
"external_id": "user_4821",
"email": "jordan@example.com",
"email_verified": true
},
"context": { "ip": "203.0.113.42" },
"identifiers": { "device_token": "" }
}'
```
Replace the example IP address with the user's public address. `203.0.113.42` comes from a range reserved for documentation, and AbuseZero rejects documentation, private and other non-routable addresses with `IP_NOT_ROUTABLE`.
### Response [#response]
Returns `200` with the evaluation result.
```json title="Allowed"
{
"object": "evaluation",
"id": "eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93",
"result": "allow",
"risk": { "score": 10, "band": "low" },
"reason": {
"code": "default_allow",
"message": "No rule matched, so the policy's default allowed this claim."
},
"outcome": { "state": "assumed_granted", "due_at": null }
}
```
```json title="Denied"
{
"object": "evaluation",
"id": "eval_8a1d4f20-6c3b-4e57-b9d2-0f7e6a5c3b18",
"result": "deny",
"risk": { "score": null, "band": "not_assessed" },
"reason": {
"code": "prior_claim_email",
"message": "An earlier claim used this email address."
},
"outcome": null
}
```
| Field | Type | Description |
|---|---|---|
| `object` | string | Always `evaluation`. |
| `id` | string | The evaluation's ID. Store it with the claim: you need it to report the [outcome](/docs/concepts/outcomes). |
| `result` | string | `allow` or `deny`. Grant the benefit only on `allow`. |
| `risk` | object | The AbuseZero Risk Score. See [Risk](#risk). |
| `reason` | object or `null` | Why the result was reached. See [Reason](#reason). |
| `outcome` | object or `null` | For an allowed claim, its outcome `state` and `due_at`. `null` for a denied claim. |
When the claim's outcome is `awaiting_outcome`, report it by `outcome.due_at`. See [Outcomes](/docs/api-reference/outcomes).
An allowed claim's response also carries its outcome in two headers, `AbuseZero-Outcome-State` and, when there is a due time, `AbuseZero-Outcome-Due-At`.
### Re-evaluating a `claim_id` [#re-evaluating-a-claim_id]
A claim ID stands for one award of a benefit. AbuseZero keeps every claim consistent, however often you evaluate it:
- **The same `Idempotency-Key` and body** returns the original response. Nothing is evaluated twice.
- **A new key for a claim that was allowed** returns `allow` with the reason `previously_authorized` and the original evaluation's `id`.
- **A claim whose outcome you reported as `not_granted`** is evaluated again from the start.
- **A claim ID already used by a different user** returns `CLAIM_CONFLICT`. Use a new claim ID for each award.
- **A claim ID from an earlier period** of a calendar-based benefit returns `CLAIM_PERIOD_ENDED`. Use a new claim ID for each period.
- **A claim whose outcome report is overdue** returns `OUTCOME_CONFLICT`. Report its outcome before evaluating it again.
### Skipped evaluations [#skipped-evaluations]
When the workspace can't run an evaluation, the response is `200` with `status: "skipped"` and no `result`. No allow or deny was decided, so handle skips as their own branch.
```json
{
"object": "evaluation",
"status": "skipped",
"reason": "insufficient_credits"
}
```
| `reason` | Meaning |
|---|---|
| `insufficient_credits` | The workspace doesn't have enough credits for the evaluation. |
| `billing_overdue` | The workspace doesn't have enough credits while a payment is overdue. |
| `service_inactive` | The workspace can't run evaluations, for example while it's being deleted. |
| `benefit_paused` | The benefit is paused because the workspace is over its plan's limits. |
New skip reasons can be added within `v1`. Handle an unknown reason like any other skip. The workspace owner manages credits, payments and plan limits in **Workspace settings → Billing & Subscriptions**.
### Errors [#errors]
| Status | Code | When |
|---|---|---|
| 422 | `IDEMPOTENCY_KEY_REQUIRED` | The `Idempotency-Key` header is missing or invalid. |
| 422 | `INVALID_FIELD` | A field is missing or invalid. `error.field` names it. |
| 422 | `INVALID_EMAIL` | `subject.email` isn't in a supported form. Quoted and international local parts aren't supported. |
| 422 | `INVALID_IP` | `context.ip` isn't a valid IPv4 or IPv6 address. |
| 422 | `IP_NOT_ROUTABLE` | `context.ip` is a private, loopback, documentation or other non-routable address. Send the user's public address, or leave it out. |
| 422 | `UNSUPPORTED_FIELD` | The body has an unknown field, or a custom identifier and key version aren't registered in Developers → Custom identifiers. |
| 422 | `DEVICE_TOKEN_INVALID` | `identifiers.device_token` isn't a value the browser SDK returned. |
| 422 | `DEVICE_TOKEN_SCOPE` | The [device token](/docs/concepts/browser-protection) was collected for a different claim ID. |
| 422 | `BROWSER_SDK_UPGRADE_REQUIRED` | The device token came from an outdated browser SDK. |
| 422 | `BOT_EVIDENCE_REQUIRED` | The benefit requires a bot check and the evaluation has none that can be used. |
| 422 | `AGENT_EVIDENCE_SCOPE` | `agent_evidence` was produced for a different claim. |
| 422 | `AGENT_EVIDENCE_OUTDATED` | `agent_evidence` came from a Node.js SDK before 0.11.0. Update the SDK. |
| 404 | `BENEFIT_NOT_FOUND` | No benefit has this key. |
| 409 | `IDEMPOTENCY_CONFLICT` | The key was already used with a different body. |
| 409 | `EVALUATION_PENDING` | An evaluation with this key is still running. Retry with the same key. |
| 409 | `CLAIM_CONFLICT` | The claim ID belongs to a different user. |
| 409 | `CLAIM_PERIOD_ENDED` | The claim ID belongs to an earlier eligibility period. |
| 409 | `OUTCOME_CONFLICT` | The claim's earlier approval expired before its outcome was reported. |
| 409 | `OUTCOME_ALREADY_RECORDED` | An imported outcome already exists for this claim. |
| 409 | `DEVICE_TOKEN_USED` | The device token was already used for another claim. |
| 409 | `DELETION_IN_PROGRESS` | The user's history or the workspace is being deleted. |
| 409 | `POLICY_UNAVAILABLE` | The benefit's [policy](/docs/concepts/policies-and-rules) isn't available. |
| 413 | `PAYLOAD_TOO_LARGE` | The body is over 16 KiB. |
| 429 | `RATE_LIMITED` | The workspace is over a traffic limit. Wait for `Retry-After`. |
| 503 | `BOT_EVIDENCE_UNAVAILABLE` | The bot check couldn't be completed. Collect new browser data and retry. |
| 503 | `TEMPORARILY_UNAVAILABLE` | AbuseZero couldn't finish the request. Retry with the same key. |
| 504 | `DEADLINE_EXCEEDED` | The evaluation reached its time limit. Retry with the same key. |
## List evaluations [#list-evaluations]
`GET /v1/evaluations` · Scope [`evaluations:read`](/docs/api-reference/authentication#scopes)
Also available as [`abusezero evaluations list`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_list_evaluations`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`evaluations()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
Lists [evaluations](/docs/concepts/evaluations), newest first.
### Query parameters [#query-parameters]
| Parameter | Type | Required | Description |
|---|---|---|---|
| `limit` | integer | No | Items per page, 1 to 100. Defaults to 50. |
| `cursor` | string | No | The `next_cursor` from the previous page. |
| `result` | string | No | Only `allow` or only `deny` evaluations. |
| `benefit` | string | No | Only evaluations for this [benefit](/docs/concepts/benefits) key. |
| `query` | string | No | Only evaluations with this claim ID, user ID (`subject.external_id`) or email address. Up to 254 characters. |
| `days` | integer | No | How many days back to look, 1 to 90. Defaults to 7. |
### Example [#example-1]
```ts tab="Node.js"
const page = await abusezero.evaluations({ result: "deny", days: 30, limit: 20 });
```
```bash tab="cURL"
curl "https://api.abusezero.com/v1/evaluations?result=deny&days=30&limit=20" \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
### Response [#response-1]
```json
{
"object": "list",
"data": [
{
"object": "evaluation",
"id": "eval_8a1d4f20-6c3b-4e57-b9d2-0f7e6a5c3b18",
"benefit": "pro_trial",
"claim_id": "trial_user_5307",
"result": "deny",
"reason": {
"code": "prior_claim_email",
"message": "An earlier claim used this email address."
},
"outcome": { "state": null },
"policy_version": 3,
"created_at": "2026-10-29T14:05:12.000Z"
}
],
"next_cursor": null
}
```
Each item is a summary. Retrieve an evaluation for its full detail.
### Errors [#errors-1]
| Status | Code | When |
|---|---|---|
| 422 | `CURSOR_INVALID` | The cursor was changed or belongs to another list. |
| 422 | `INVALID_FIELD` | A query parameter is invalid. |
## Retrieve an evaluation [#retrieve-an-evaluation]
`GET /v1/evaluations/{id}` · Scope [`evaluations:read`](/docs/api-reference/authentication#scopes)
Also available as [`abusezero evaluations get`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_get_evaluation`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`getEvaluation()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
Returns the full [evaluation object](#the-evaluation-object).
### Example [#example-2]
```ts tab="Node.js"
const evaluation = await abusezero.getEvaluation("eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93");
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/evaluations/eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93 \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
### Response [#response-2]
```json
{
"object": "evaluation",
"id": "eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93",
"benefit": "pro_trial",
"claim_id": "trial_user_4821",
"result": "allow",
"reason": {
"code": "default_allow",
"message": "No rule matched, so the policy's default allowed this claim."
},
"outcome": { "state": "granted", "due_at": null },
"policy_version": 3,
"created_at": "2026-10-29T14:02:47.000Z",
"mode": "protect",
"risk": { "score": 10, "band": "low" },
"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
}
}
```
### Errors [#errors-2]
| Status | Code | When |
|---|---|---|
| 404 | `EVALUATION_NOT_FOUND` | No [evaluation](/docs/concepts/evaluations) with this ID exists in the workspace. |
| 422 | `INVALID_FIELD` | The ID doesn't start with `eval_`. |
## Retrieve an evaluation's evidence [#retrieve-an-evaluations-evidence]
`GET /v1/evaluations/{id}/evidence` · Scope [`evaluation_evidence:read`](/docs/api-reference/authentication#scopes)
Also available as [`abusezero evaluations evidence`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_get_evaluation_evidence`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`evaluationEvidence()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
Returns the full [evaluation](/docs/concepts/evaluations) object with its [signal family assessments](#signals), plus the request details AbuseZero retained for it. With an OAuth access token, the signed-in member also needs permission to reveal evidence.
### Example [#example-3]
```ts tab="Node.js"
const evaluation = await abusezero.evaluationEvidence("eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93");
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/evaluations/eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93/evidence \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
### Response [#response-3]
The [evaluation object](#the-evaluation-object), with `signals` and one more field:
```json
{
"object": "evaluation",
"id": "eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93",
"result": "allow",
"signals": [
{ "family": "identity", "assessment": "clear" },
{ "family": "device", "assessment": "clear" },
{ "family": "network", "assessment": "clear" },
{ "family": "automation", "assessment": "clear" },
{ "family": "benefit_history", "assessment": "clear" }
],
"evidence": {
"request": {
"benefit": "pro_trial",
"claim_id": "trial_user_4821",
"subject": {
"external_id": "user_4821",
"email": "jordan@example.com",
"email_verified": true
},
"context": { "ip": "203.0.113.42" }
},
"device_profile": {
"status": "captured",
"expires_at": "2026-11-28T14:02:47.000Z"
}
}
}
```
The example shows only some fields of the evaluation object.
| Field | Type | Description |
|---|---|---|
| `evidence.request` | object or `null` | The request body AbuseZero received. `null` once your [workspace](/docs/keys-and-access#workspaces)'s evidence retention has removed it. |
| `evidence.device_profile.status` | string | `captured` when browser data from the evaluation is still retained, `not_retained` when it isn't. |
| `evidence.device_profile.expires_at` | string or `null` | When the retained browser data will be removed. |
### Errors [#errors-3]
| Status | Code | When |
|---|---|---|
| 403 | `PERMISSION_REQUIRED` | The signed-in member can't reveal evidence. |
| 404 | `EVALUATION_NOT_FOUND` | No evaluation with this ID exists in the workspace. |
## Retrieve an evaluation's association graph [#retrieve-an-evaluations-association-graph]
`GET /v1/evaluations/{id}/graph` · Scope [`evaluations:read`](/docs/api-reference/authentication#scopes)
Also available as [`abusezero evaluations graph`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_get_evaluation_graph`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`evaluationGraph()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
Returns the other [outcomes](/docs/concepts/outcomes) for the same [benefit](/docs/concepts/benefits) that are linked to this [evaluation](/docs/concepts/evaluations)'s claim through shared identifiers. See [Association graph](/docs/concepts/association-graph).
An allowed claim's graph starts from its outcome. A claim with no outcome, such as a denied claim, starts from the identifiers its evaluation recorded, so a prior-claim denial shows the earlier outcomes behind it.
### Example [#example-4]
```ts tab="Node.js"
const graph = await abusezero.evaluationGraph("eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93");
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/evaluations/eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93/graph \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
### Response [#response-4]
```json
{
"object": "evaluation_links",
"evaluation": "eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93",
"links": [
{ "outcome": "out_9e7c5a31-2d4b-4f86-b0a9-6c1e8d3f5a72", "family": "identity" },
{ "outcome": "out_9e7c5a31-2d4b-4f86-b0a9-6c1e8d3f5a72", "family": "device" },
{ "outcome": "out_1f3a5c7e-9b2d-4e6f-8a0c-3d5e7f9a1b24", "family": "network" }
],
"linked_outcomes": 2,
"complete": true
}
```
| Field | Type | Description |
|---|---|---|
| `object` | string | Always `evaluation_links`. |
| `evaluation` | string | The evaluation's ID. |
| `links` | array | One entry for each link: the linked `outcome` ID and the signal `family` that links them, `identity`, `device`, `network` or `benefit_history`. Device and browser identifiers both report `device`. One outcome can be linked in more than one way. |
| `linked_outcomes` | integer | How many distinct outcomes are linked. |
| `complete` | boolean | `false` when the graph is too large to return in full. |
A link shows an observed association. A shared device or network doesn't by itself mean two claims came from one person.
### Errors [#errors-4]
| Status | Code | When |
|---|---|---|
| 404 | `EVALUATION_NOT_FOUND` | No evaluation with this ID exists in the workspace. |
# Exports (/docs/api-reference/exports)
Exports package your [workspace's](/docs/keys-and-access#workspaces) [evaluations](/docs/api-reference/evaluations#the-evaluation-object) into a CSV file for your own analysis, warehouse or review. Each export covers up to the last 90 days. It's produced in the background and stays available for 24 hours after you request it. A [data deletion](/docs/api-reference/data-deletions) removes every export in the workspace, so download what you need first. For a walkthrough, see [Export and delete data](/docs/guides/export-and-delete-data).
Examples on this page use a server client:
```ts
import { AbuseZero } from "abusezero/server";
const abusezero = new AbuseZero({ key: process.env.ABUSEZERO_SECRET_KEY!, outage: "throw" });
```
## The export object [#the-export-object]
```json
{
"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"
}
```
| Field | Type | Description |
|---|---|---|
| `object` | `"export"` | The object type. |
| `id` | string | The export ID, starting with `exp_`. |
| `state` | `pending`, `complete` or `failed` | `pending` is still being produced. `complete` is ready to download. `failed` couldn't be produced. |
| `failure` | object or `null` | Why the export failed, with a `code` and a `message`. Present when `state` is `failed`, otherwise `null`. |
| `rows` | integer or `null` | How many [evaluations](/docs/concepts/evaluations) the file holds. `null` until the export completes. |
| `include_evidence` | boolean | Whether the file includes [signal assessments](/docs/concepts/risk-score-and-signals#signal-families) and device profile status. |
| `completed_at` | string or `null` | When the export completed or failed. |
| `expires_at` | string | When the export stops being available, 24 hours after it was requested. |
An export holds at most 10,000 evaluations. When the chosen range has more, the export fails with `failure.code` `too_many_rows`. Request a shorter range.
| `failure.code` | `failure.message` |
|---|---|
| `too_many_rows` | This export has over 10,000 evaluations. Request a shorter date range. |
| `export_failed` | AbuseZero couldn't finish this export. Request a new export. |
New codes may be added within `v1`. Treat a code you don't recognize like `export_failed`.
### File columns [#file-columns]
| Column | Description |
|---|---|
| `evaluation_id` | The [evaluation](/docs/api-reference/evaluations#the-evaluation-object) ID, starting with `eval_`. |
| `created_at` | When the evaluation ran. |
| `benefit` | The [benefit](/docs/api-reference/benefits#the-benefit-object) key. |
| `result` | `allow` or `deny`. |
| `reason_code` | The [reason code](/docs/reason-codes). |
| `reason` | The reason message. |
| `risk_score` | The [AbuseZero Risk Score](/docs/concepts/risk-score-and-signals#the-abusezero-risk-score), from 0 to 100, or empty. |
| `risk_band` | `low`, `elevated`, `high` or `not_assessed`. |
| `policy_version` | The [policy](/docs/api-reference/policies#the-policy-object) version the evaluation ran under. |
| `claim_id` | The `claim_id` you sent. |
| `outcome_state` | The claim's [outcome state](/docs/api-reference/outcomes#outcome-states). |
With `include_evidence`, the file adds:
| Column | Description |
|---|---|
| `identity`, `device`, `network`, `automation`, `benefit_history` | The assessment for each signal family: `clear`, `elevated`, `high` or `unavailable`. |
| `device_profile_status` | `captured` when [browser data](/docs/concepts/browser-protection#browser-data-statuses) was retained with the evaluation, or `not_retained`. |
| `device_profile_expires_at` | When the retained browser data expires, or empty. |
## Request an export [#request-an-export]
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `POST` | `/v1/exports` | [`exports:create`](/docs/api-reference/authentication#scopes) | Required |
Also available as [`abusezero exports create`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_request_export`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`requestExport()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
An export with `include_evidence` also needs the [`evaluation_evidence:read`](/docs/api-reference/authentication#scopes) scope. With an OAuth token, the signed-in member also needs [permission](/docs/api-reference/authentication#member-permissions-for-oauth) to export data, plus permission to reveal evidence when `include_evidence` is `true`.
### Request body [#request-body]
| Field | Type | Required | Limits | Description |
|---|---|---|---|---|
| `days` | integer | No | 1 to 90, default 30 | Export evaluations from this many recent days. |
| `include_evidence` | boolean | No | Default `false` | Add the signal assessment and device profile columns. |
### Example [#example]
```ts tab="Node.js"
const job = await abusezero.requestExport({ days: 30 }, "export-2026-10-29");
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/exports \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: export-2026-10-29" \
-d '{ "days": 30 }'
```
### Response [#response]
Returns `202`. Retrieve the export at `status_url` until its `state` is `complete`.
```json
{
"object": "export",
"id": "exp_6e0a2c4b-8d1f-4b3e-9a5c-7f2d4b6e8a01",
"status": "pending",
"status_url": "/v1/exports/exp_6e0a2c4b-8d1f-4b3e-9a5c-7f2d4b6e8a01"
}
```
### Errors [#errors]
| Code | Status | Meaning |
|---|---|---|
| [`EVIDENCE_SCOPE_REQUIRED`](/docs/api-reference/errors#permission) | 403 | `include_evidence` is `true`, but the credential doesn't have the `evaluation_evidence:read` scope. |
| [`PERMISSION_REQUIRED`](/docs/api-reference/errors#permission) | 403 | With an OAuth token, the signed-in member doesn't have the permission this export needs. Ask the workspace owner. |
For every other code, see [REST API errors](/docs/api-reference/errors).
## Retrieve an export [#retrieve-an-export]
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `GET` | `/v1/exports/{id}` | [`exports:read`](/docs/api-reference/authentication#scopes) | Not used |
Also available as [`abusezero exports get`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_get_export`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`call()`](/docs/tools/server-sdk#call-for-any-endpoint) in the AbuseZero Node.js SDK.
### Path parameters [#path-parameters]
| Parameter | Type | Description |
|---|---|---|
| `id` | string | The export ID, starting with `exp_`. |
### Example [#example-1]
```ts tab="Node.js"
const job = await abusezero.call("/v1/exports/exp_6e0a2c4b-8d1f-4b3e-9a5c-7f2d4b6e8a01");
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/exports/exp_6e0a2c4b-8d1f-4b3e-9a5c-7f2d4b6e8a01 \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
### Response [#response-1]
Returns `200` with an [export object](#the-export-object).
### Errors [#errors-1]
| Code | Status | Meaning |
|---|---|---|
| [`RESOURCE_NOT_FOUND`](/docs/api-reference/errors#not-found) | 404 | No export with this ID exists in the workspace. |
## Download an export [#download-an-export]
Returns a short-lived link to the export's CSV file.
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `GET` | `/v1/exports/{id}/download` | [`exports:read`](/docs/api-reference/authentication#scopes) | Not used |
Also available as [`abusezero exports download`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_get_export_download`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`call()`](/docs/tools/server-sdk#call-for-any-endpoint) in the AbuseZero Node.js SDK.
Downloading an export with evidence also needs the [`evaluation_evidence:read`](/docs/api-reference/authentication#scopes) scope and, with an OAuth token, the signed-in member's [permission](/docs/api-reference/authentication#member-permissions-for-oauth) to reveal evidence. Every download is recorded in your [audit log](/docs/api-reference/audit-logs).
### Path parameters [#path-parameters-1]
| Parameter | Type | Description |
|---|---|---|
| `id` | string | The export ID, starting with `exp_`. |
### Example [#example-2]
```ts tab="Node.js"
const download = await abusezero.call(
"/v1/exports/exp_6e0a2c4b-8d1f-4b3e-9a5c-7f2d4b6e8a01/download",
) as { url: string; expires_at: string };
const csv = await fetch(download.url).then((response) => response.text());
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/exports/exp_6e0a2c4b-8d1f-4b3e-9a5c-7f2d4b6e8a01/download \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
### Response [#response-2]
Returns `200`.
```json
{
"object": "export_download",
"url": "https://api.abusezero.com/…",
"expires_at": "2026-10-30T12:03:58.000Z"
}
```
| Field | Type | Description |
|---|---|---|
| `url` | string | The file link. It works for 5 minutes. Request the download again for a new link. A link that fails returns the standard [error envelope](/docs/api-reference/errors), for example [`TRANSFER_EXPIRED`](/docs/api-reference/errors#permission) once the 5 minutes have passed. |
| `expires_at` | string | When the export itself stops being available. |
### Errors [#errors-2]
| Code | Status | Meaning |
|---|---|---|
| [`EXPORT_NOT_FOUND`](/docs/api-reference/errors#not-found) | 404 | The export doesn't exist or has expired. Request a new export. |
| [`EXPORT_NOT_READY`](/docs/api-reference/errors#conflict) | 409 | The export isn't `complete`. If it's `pending`, wait for it to finish, then download it. If it's `failed`, request a new export. |
| [`EVIDENCE_SCOPE_REQUIRED`](/docs/api-reference/errors#permission) | 403 | The export includes evidence, but the credential doesn't have the `evaluation_evidence:read` scope. |
| [`PERMISSION_REQUIRED`](/docs/api-reference/errors#permission) | 403 | With an OAuth token, the export includes evidence and the signed-in member doesn't have permission to reveal it. Ask the workspace owner. |
## List exports [#list-exports]
Returns exports, most recent first.
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `GET` | `/v1/exports` | [`exports:read`](/docs/api-reference/authentication#scopes) | Not used |
Also available as [`abusezero exports list`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_list_exports`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`exports()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
### Query parameters [#query-parameters]
| Parameter | Type | Required | Limits | Description |
|---|---|---|---|---|
| `limit` | integer | No | 1 to 100, default 50 | How many exports to return. |
| `cursor` | string | No | | The `next_cursor` from the previous page, unchanged. |
### Example [#example-3]
```ts tab="Node.js"
const page = await abusezero.exports();
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/exports \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
### Response [#response-3]
Returns `200`.
```json
{
"object": "list",
"data": [
{
"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"
}
],
"next_cursor": null
}
```
## Related [#related]
Move data out of AbuseZero and remove it when you need to.
Read single evaluations in full.
# Imports (/docs/api-reference/imports)
Imports load the [benefits](/docs/concepts/benefits) you granted before AbuseZero into your [workspace's](/docs/keys-and-access#workspaces) claim history, so protection starts with your full history on day one. Every imported [outcome](/docs/concepts/outcomes) becomes history that later [evaluations](/docs/concepts/evaluations) are checked against.
There are two ways in:
- **JSON:** send up to 1,000 granted outcomes in one request. They're queued and applied in the background, with no review step.
- **CSV:** upload up to 100,000 rows. AbuseZero validates every row and holds the import for your review. Nothing is applied until you confirm it.
For a walkthrough, see [Import outcome history](/docs/guides/import-claim-history).
Examples on this page use a server client:
```ts
import { AbuseZero } from "abusezero/server";
const abusezero = new AbuseZero({ key: process.env.ABUSEZERO_SECRET_KEY!, outage: "throw" });
```
## The import object [#the-import-object]
```json
{
"object": "import",
"id": "imp_2b8e4d6f-1a3c-4f5e-9b7d-8c0a2e4f6b13",
"state": "preview",
"failure": null,
"total": 1240,
"validated": 1240,
"processed": 0,
"imported": 0,
"skipped": 0,
"rejected": 2,
"created_at": "2026-10-29T11:20:05.000Z",
"completed_at": null,
"errors": [
{ "row": 18, "error": "Check this email address." },
{ "row": 907, "error": "Use the time the benefit was actually granted, not a future time." }
]
}
```
| Field | Type | Description |
|---|---|---|
| `object` | `"import"` | The object type. |
| `id` | string | The import ID, starting with `imp_`. |
| `state` | string | Where the import is. See [Import states](#import-states). |
| `failure` | object or `null` | Why the import failed, with a `code` and a `message`. Present when `state` is `failed`, otherwise `null`. See [Failure reasons](#failure-reasons). |
| `total` | integer | Rows or outcomes in the import. For a CSV, this is known once validation finishes. |
| `validated` | integer | CSV rows checked so far. |
| `processed` | integer | Rows finished so far: imported, skipped and rejected together. |
| `imported` | integer | Rows added to your claim history. |
| `skipped` | integer | Rows that matched history your workspace already had. |
| `rejected` | integer | Rows that couldn't be imported. |
| `created_at` | string | When the import was created. |
| `completed_at` | string or `null` | When the import finished, failed or was cancelled. |
| `errors` | array | Up to the first 100 row problems, each with `row` (the row number) and `error` (what to fix). Returned when you retrieve or validate one import, not in lists. The [error report](#download-the-error-report) has every row. |
### Import states [#import-states]
| State | Meaning |
|---|---|
| `uploading` | The CSV import is waiting for its file and the validate call. |
| `validating` | AbuseZero is checking every row. |
| `preview` | Validation finished. Review the counts and errors, then confirm or cancel. |
| `queued` | The import was accepted and is waiting to be applied. |
| `processing` | Rows are being applied. |
| `complete` | Every row was applied. The counts are final. |
| `failed` | The import couldn't finish. `failure` says why. Fix the file, then upload it to a new import. |
| `cancelled` | The import was cancelled. Its uploaded file and unfinished rows were deleted. |
### Failure reasons [#failure-reasons]
| `failure.code` | `failure.message` |
|---|---|
| `file_not_uploaded` | The CSV file wasn't uploaded. Create a new import and upload the file again. |
| `file_too_large` | The CSV is over the 25 MiB limit. Split it into smaller files and import each one. |
| `row_limit` | The CSV needs 1 to 100,000 rows. Check the file, then upload it to a new import. |
| `csv_columns` | Use unique CSV columns and include email, benefit_key, outcome, and occurred_at. |
| `csv_unreadable` | The CSV could not be parsed. Check quoting, columns, and row size, then upload a new file. |
| `file_changed` | The uploaded file changed during validation. Create a new import. |
| `import_failed` | AbuseZero couldn't finish this import. Create a new import and upload the file again. |
New codes may be added within `v1`. Treat a code you don't recognize like `import_failed`.
## Import outcomes as JSON [#import-outcomes-as-json]
Adds up to 1,000 granted [outcomes](/docs/api-reference/outcomes#the-outcome-object) in one request. The import is queued and applied in the background. Retrieve it to follow its progress.
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `POST` | `/v1/imports` | [`imports:write`](/docs/api-reference/authentication#scopes) | Required |
Also available as [`abusezero imports create`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_create_outcome_import`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`importOutcomes()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
### Request body [#request-body]
The body is at most 1 MiB.
| Field | Type | Required | Limits | Description |
|---|---|---|---|---|
| `outcomes` | array of objects | Yes | 1 to 1,000 | The outcomes to import. |
| `outcomes[].outcome` | `"granted"` | Yes | | The benefit was granted. |
| `outcomes[].benefit` | string | Yes | A benefit key in this workspace | The [benefit](/docs/api-reference/benefits#the-benefit-object) that was granted, such as `pro_trial`. Create the benefit first. |
| `outcomes[].subject.email` | string | Yes | Up to 254 characters | The user's email address. |
| `outcomes[].subject.external_id` | string | No | 1 to 128 characters | Your ID for the user. Send it whenever you have it. |
| `outcomes[].subject.email_verified` | boolean | No | | Whether your app verified the email address. |
| `outcomes[].occurred_at` | string | Yes | ISO 8601 with a timezone, not in the future | When the benefit was granted. |
| `outcomes[].external_outcome_id` | string | No | 1 to 128 printable characters, no spaces | Your ID for this grant. Importing the same ID again doesn't create a second record. |
| `outcomes[].claim_id` | string | No | 1 to 128 printable characters, no spaces | The `claim_id` you used for this grant in an [evaluation](/docs/api-reference/evaluations#create-an-evaluation), if there was one. |
### Example [#example]
```ts tab="Node.js"
const job = await abusezero.importOutcomes(
[
{
outcome: "granted",
benefit: "pro_trial",
subject: { external_id: "user_4821", email: "jordan@example.com", email_verified: true },
occurred_at: "2026-03-14T09:30:00Z",
external_outcome_id: "grant_10293",
},
],
"history-batch-0001",
);
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/imports \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: history-batch-0001" \
-d '{
"outcomes": [
{
"outcome": "granted",
"benefit": "pro_trial",
"subject": { "external_id": "user_4821", "email": "jordan@example.com", "email_verified": true },
"occurred_at": "2026-03-14T09:30:00Z",
"external_outcome_id": "grant_10293"
}
]
}'
```
### Response [#response]
Returns `202`.
```json
{
"object": "import",
"id": "imp_5f1c7a3e-9d2b-4c6a-8e4f-1b3d5a7c9e20",
"state": "queued"
}
```
Rows that can't be applied, for example because their benefit doesn't exist, are counted in `rejected` with a message in `errors`. The rest of the import still goes through.
### Errors [#errors]
| Code | Status | Meaning |
|---|---|---|
| [`INVALID_FIELD`](/docs/api-reference/errors#invalid-request) | 422 | A field is missing or outside its limits. `field` names it. |
| [`UNSUPPORTED_FIELD`](/docs/api-reference/errors#invalid-request) | 422 | The body has a field the import doesn't define. `field` names it. |
| [`PAYLOAD_TOO_LARGE`](/docs/api-reference/errors#invalid-request) | 413 | The body is over 1 MiB. Split the outcomes into smaller requests. |
| [`DELETION_IN_PROGRESS`](/docs/api-reference/errors#conflict) | 409 | A [data deletion](/docs/api-reference/data-deletions) is running in this workspace. Wait for it to finish, then import. |
| [`RATE_LIMITED`](/docs/api-reference/errors#rate-limit) | 429 | Too many import requests. Wait for the number of seconds in `Retry-After`. |
For every other code, see [REST API errors](/docs/api-reference/errors).
## Import a CSV file [#import-a-csv-file]
A CSV import takes five steps:
1. [Create an upload](#create-an-upload) to get a one-time upload URL.
2. [Upload the file](#upload-the-file) to that URL.
3. [Validate the import](#validate-the-import). AbuseZero checks every row.
4. [Retrieve the import](#retrieve-an-import) until it reaches `preview`, then review its counts and errors.
5. [Confirm the import](#confirm-the-import) to apply it, or [cancel it](#cancel-an-import).
### File format [#file-format]
The first row names the columns. Each column may appear once, and no other columns are accepted.
| Column | Required | Description |
|---|---|---|
| `email` | Yes | The user's email address. |
| `benefit_key` | Yes | The [benefit](/docs/api-reference/benefits#the-benefit-object) that was granted. Create the benefit first. |
| `outcome` | Yes | Always `granted`. |
| `occurred_at` | Yes | When the benefit was granted, as ISO 8601 with a timezone. |
| `external_user_id` | No | Your ID for the user, 1 to 128 characters. |
| `external_outcome_id` | No | Your ID for this grant, 1 to 128 printable characters with no spaces. |
```csv
email,external_user_id,benefit_key,outcome,occurred_at,external_outcome_id
jordan@example.com,user_4821,pro_trial,granted,2026-03-14T09:30:00Z,grant_10293
sam@example.com,user_5307,pro_trial,granted,2026-04-02T16:05:00+02:00,grant_10377
```
A file holds 1 to 100,000 rows and is at most 25 MiB.
## Create an upload [#create-an-upload]
Creates a CSV import and returns the address to upload its file to.
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `POST` | `/v1/imports/csv` | [`imports:write`](/docs/api-reference/authentication#scopes) | Required |
Also available as [`abusezero imports create-csv`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_create_csv_import`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`createCsvImport()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
The request has no fields. Send an empty JSON object.
### Example [#example-1]
```ts tab="Node.js"
const upload = await abusezero.createCsvImport("history-csv-2026-10-29") as {
id: string;
url: string;
upload_headers: Record;
};
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/imports/csv \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: history-csv-2026-10-29" \
-d '{}'
```
### Response [#response-1]
Returns `202`.
```json
{
"object": "import_upload",
"id": "imp_2b8e4d6f-1a3c-4f5e-9b7d-8c0a2e4f6b13",
"url": "https://api.abusezero.com/…",
"upload_headers": {
"Content-Type": "text/csv",
"If-None-Match": "*"
}
}
```
| Field | Type | Description |
|---|---|---|
| `id` | string | The new import's ID. |
| `url` | string | Where to upload the file. It works for 5 minutes and accepts one upload. |
| `upload_headers` | object | Headers to send with the upload, exactly as given. |
A retry with the same [`Idempotency-Key`](/docs/api-reference#idempotency) returns the same import with a fresh upload `url`, so a lost response never leaves an extra import behind.
### Errors [#errors-1]
| Code | Status | Meaning |
|---|---|---|
| [`DELETION_IN_PROGRESS`](/docs/api-reference/errors#conflict) | 409 | A [data deletion](/docs/api-reference/data-deletions) is running in this workspace. Wait for it to finish, then import. |
## Upload the file [#upload-the-file]
Send the file to `url` with a `PUT` request and every header in `upload_headers`. No API key is needed: the URL carries its own authorization. Send the whole file as one request body, as both samples do.
```ts tab="Node.js"
import { readFile } from "node:fs/promises";
const response = await fetch(upload.url, {
method: "PUT",
headers: upload.upload_headers,
body: await readFile("history.csv"),
});
if (!response.ok) throw new Error(`Upload failed with status ${response.status}`);
```
```bash tab="cURL"
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: text/csv" \
-H "If-None-Match: *" \
--data-binary @history.csv
```
A successful upload returns `204` with no body. A failed upload returns the standard [error envelope](/docs/api-reference/errors):
| Code | Status | Meaning |
|---|---|---|
| [`UPLOAD_HEADERS`](/docs/api-reference/errors#invalid-request) | 400 | The request didn't send the headers in `upload_headers` exactly. |
| [`UPLOAD_EMPTY`](/docs/api-reference/errors#invalid-request) | 400 | The request had no file. |
| [`IMPORT_SIZE`](/docs/api-reference/errors#invalid-request) | 413 | The file is larger than 25 MiB. Split it into smaller files. |
| [`TRANSFER_EXPIRED`](/docs/api-reference/errors#permission) | 403 | The 5 minutes have passed. Create a new upload. |
| [`INVALID_TRANSFER`](/docs/api-reference/errors#permission) | 403 | The URL was changed or isn't complete. Use it exactly as returned. |
| [`UPLOAD_EXISTS`](/docs/api-reference/errors#conflict) | 409 | A file was already uploaded to this import. Create a new import to upload another file. |
## Validate the import [#validate-the-import]
Starts validation of the uploaded file. Call it once the upload succeeds.
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `POST` | `/v1/imports/{id}/validate` | [`imports:write`](/docs/api-reference/authentication#scopes) | Required |
Also available as [`abusezero imports validate`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_validate_import`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`validateImport()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
### Path parameters [#path-parameters]
| Parameter | Type | Description |
|---|---|---|
| `id` | string | The import ID, starting with `imp_`. |
### Example [#example-2]
```ts tab="Node.js"
const job = await abusezero.validateImport(upload.id, "validate-imp-2b8e4d6f");
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/imports/imp_2b8e4d6f-1a3c-4f5e-9b7d-8c0a2e4f6b13/validate \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: validate-imp-2b8e4d6f" \
-d '{}'
```
### Response [#response-2]
Returns `201` with the [import object](#the-import-object), usually in `validating`. Calling it again after validation has started returns the import as it is.
Retrieve the import until it reaches `preview` or `failed`. A file fails validation when its columns don't match the [file format](#file-format), it has no rows or more than 100,000, it's larger than 25 MiB, it can't be read as CSV, or it changed during validation.
### Errors [#errors-2]
| Code | Status | Meaning |
|---|---|---|
| [`IMPORT_NOT_FOUND`](/docs/api-reference/errors#not-found) | 404 | No import with this ID exists in the workspace. |
| [`IMPORT_NOT_UPLOADED`](/docs/api-reference/errors#conflict) | 409 | The CSV file hasn't been uploaded yet. The import stays in `uploading`; upload the file, then validate again. |
## Retrieve an import [#retrieve-an-import]
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `GET` | `/v1/imports/{id}` | [`imports:read`](/docs/api-reference/authentication#scopes) | Not used |
Also available as [`abusezero imports get`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_get_import`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`getImport()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
### Path parameters [#path-parameters-1]
| Parameter | Type | Description |
|---|---|---|
| `id` | string | The import ID, starting with `imp_`. |
### Example [#example-3]
```ts tab="Node.js"
const job = await abusezero.getImport("imp_2b8e4d6f-1a3c-4f5e-9b7d-8c0a2e4f6b13");
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/imports/imp_2b8e4d6f-1a3c-4f5e-9b7d-8c0a2e4f6b13 \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
### Response [#response-3]
Returns `200` with the [import object](#the-import-object), including up to the first 100 row errors.
### Errors [#errors-3]
| Code | Status | Meaning |
|---|---|---|
| [`IMPORT_NOT_FOUND`](/docs/api-reference/errors#not-found) | 404 | No import with this ID exists in the workspace. |
## Download the error report [#download-the-error-report]
Creates a CSV of every row problem in an import, with the columns `row` and `error`.
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `GET` | `/v1/imports/{id}/errors` | [`imports:read`](/docs/api-reference/authentication#scopes) | Not used |
Also available as [`abusezero imports errors`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_get_import_errors`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`call()`](/docs/tools/server-sdk#call-for-any-endpoint) in the AbuseZero Node.js SDK.
### Path parameters [#path-parameters-2]
| Parameter | Type | Description |
|---|---|---|
| `id` | string | The import ID, starting with `imp_`. |
### Example [#example-4]
```ts tab="Node.js"
const report = await abusezero.call("/v1/imports/imp_2b8e4d6f-1a3c-4f5e-9b7d-8c0a2e4f6b13/errors");
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/imports/imp_2b8e4d6f-1a3c-4f5e-9b7d-8c0a2e4f6b13/errors \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
### Response [#response-4]
Returns `200`. The link works for 5 minutes. Request the report again for a new link.
```json
{
"object": "import_error_report",
"url": "https://api.abusezero.com/…"
}
```
### Errors [#errors-4]
| Code | Status | Meaning |
|---|---|---|
| [`IMPORT_NOT_FOUND`](/docs/api-reference/errors#not-found) | 404 | No import with this ID exists in the workspace. |
| [`IMPORT_REPORT_EXPIRED`](/docs/api-reference/errors#not-found) | 410 | Error reports are available until 7 days after an import finishes. Upload the corrected CSV in a new import. |
## Confirm the import [#confirm-the-import]
Applies an import that's in `preview`. Rows that were rejected during validation aren't applied.
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `POST` | `/v1/imports/{id}/confirm` | [`imports:write`](/docs/api-reference/authentication#scopes) | Required |
Also available as [`abusezero imports confirm`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_confirm_import`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`confirmImport()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
### Path parameters [#path-parameters-3]
| Parameter | Type | Description |
|---|---|---|
| `id` | string | The import ID, starting with `imp_`. |
### Example [#example-5]
```ts tab="Node.js"
await abusezero.confirmImport("imp_2b8e4d6f-1a3c-4f5e-9b7d-8c0a2e4f6b13", "confirm-imp-2b8e4d6f");
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/imports/imp_2b8e4d6f-1a3c-4f5e-9b7d-8c0a2e4f6b13/confirm \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: confirm-imp-2b8e4d6f" \
-d '{}'
```
### Response [#response-5]
Returns `201`.
```json
{
"object": "import",
"id": "imp_2b8e4d6f-1a3c-4f5e-9b7d-8c0a2e4f6b13",
"state": "queued"
}
```
### Errors [#errors-5]
| Code | Status | Meaning |
|---|---|---|
| [`IMPORT_STATE`](/docs/api-reference/errors#conflict) | 409 | The import isn't in `preview`. Validate it and review it before confirming. |
## Cancel an import [#cancel-an-import]
Cancelling is permanent. AbuseZero stops the import and deletes its uploaded file and every row that hasn't been applied yet. Rows already imported stay in your claim history as [outcomes](/docs/concepts/outcomes).
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `POST` | `/v1/imports/{id}/cancel` | [`imports:write`](/docs/api-reference/authentication#scopes) | Required |
Also available as [`abusezero imports cancel`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_cancel_import`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`cancelImport()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
### Path parameters [#path-parameters-4]
| Parameter | Type | Description |
|---|---|---|
| `id` | string | The import ID, starting with `imp_`. |
### Example [#example-6]
```ts tab="Node.js"
await abusezero.cancelImport("imp_2b8e4d6f-1a3c-4f5e-9b7d-8c0a2e4f6b13", "cancel-imp-2b8e4d6f");
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/imports/imp_2b8e4d6f-1a3c-4f5e-9b7d-8c0a2e4f6b13/cancel \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: cancel-imp-2b8e4d6f" \
-d '{}'
```
### Response [#response-6]
Returns `201`.
```json
{
"object": "import",
"id": "imp_2b8e4d6f-1a3c-4f5e-9b7d-8c0a2e4f6b13",
"state": "cancelled"
}
```
### Errors [#errors-6]
| Code | Status | Meaning |
|---|---|---|
| [`IMPORT_NOT_FOUND`](/docs/api-reference/errors#not-found) | 404 | No import with this ID exists in the workspace. |
| [`IMPORT_COMPLETE`](/docs/api-reference/errors#conflict) | 409 | The import already finished, so it can't be cancelled. To remove its history, [delete the affected users' data](/docs/api-reference/data-deletions). |
## List imports [#list-imports]
Returns imports newest first. List items don't include `errors`.
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `GET` | `/v1/imports` | [`imports:read`](/docs/api-reference/authentication#scopes) | Not used |
Also available as [`abusezero imports list`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_list_imports`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`imports()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
### Query parameters [#query-parameters]
| Parameter | Type | Required | Limits | Description |
|---|---|---|---|---|
| `limit` | integer | No | 1 to 100, default 50 | How many imports to return. |
| `cursor` | string | No | | The `next_cursor` from the previous page, unchanged. |
### Example [#example-7]
```ts tab="Node.js"
const page = await abusezero.imports({ limit: 20 });
```
```bash tab="cURL"
curl "https://api.abusezero.com/v1/imports?limit=20" \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
### Response [#response-7]
Returns `200`.
```json
{
"object": "list",
"data": [
{
"object": "import",
"id": "imp_2b8e4d6f-1a3c-4f5e-9b7d-8c0a2e4f6b13",
"state": "complete",
"total": 1240,
"validated": 1240,
"processed": 1240,
"imported": 1191,
"skipped": 47,
"rejected": 2,
"created_at": "2026-10-29T11:20:05.000Z",
"completed_at": "2026-10-29T11:26:48.000Z"
}
],
"next_cursor": null
}
```
## Related [#related]
Plan and run a history import end to end.
Read the outcomes your imports created.
# AbuseZero REST API (/docs/api-reference)
The AbuseZero REST API is a JSON API over HTTPS. One [evaluation](/docs/concepts/evaluations) call decides a [benefit](/docs/concepts/benefits) claim. The rest of the API configures benefits and [policies](/docs/concepts/policies-and-rules), records [outcomes](/docs/concepts/outcomes), and shows what AbuseZero decided and why.
Every endpoint on these pages follows the conventions below.
## REST API base URL [#rest-api-base-url]
```txt
https://api.abusezero.com
```
Every path starts with the API version, `/v1`. All requests use HTTPS.
## REST API versioning [#rest-api-versioning]
The current version is `v1`. Within `v1`, AbuseZero only makes additive changes:
- new endpoints;
- new optional request fields;
- new response fields;
- new values for codes and enums, such as reason codes, skip reasons and error codes.
Write your integration so it ignores fields it doesn't recognize and handles unknown enum values as a generic case. Breaking changes ship only in a new version.
## Authentication [#authentication]
Send a secret key, a restricted key or an OAuth access token as a bearer token:
```http
Authorization: Bearer
```
Requests with an OAuth access token also choose a [workspace](/docs/keys-and-access#workspaces) with the `AbuseZero-Workspace` header. See [REST API authentication](/docs/api-reference/authentication) for credential types, scopes and permissions.
The Node.js samples on these pages call a client named `abusezero`, created from `abusezero/server` as shown in [Authentication](/docs/api-reference/authentication#send-a-credential).
## Requests [#requests]
- Send request bodies as JSON with `Content-Type: application/json`. A write that takes no fields, such as a delete or an import step, can send `{}` or no body at all.
- `GET` endpoints take their parameters in the query string.
- Path parameters are the prefixed IDs AbuseZero returned, such as `eval_…` or `ben_…`.
- Request bodies are limited to 16 KiB for [evaluations](/docs/concepts/evaluations) and [outcome](/docs/concepts/outcomes) reports, 1 MiB for imports sent as JSON, and 256 KiB for every other endpoint. A larger body returns `PAYLOAD_TOO_LARGE`.
- A request that includes a field the endpoint doesn't define is rejected with `UNSUPPORTED_FIELD` or `INVALID_FIELD`.
## Responses [#responses]
Every successful response is a JSON object with an `object` field that names its type, such as `evaluation`, `benefit` or `list`.
| Status | When |
|---|---|
| `200` | Reads, updates, deletions, [evaluations](/docs/concepts/evaluations) and [outcome](/docs/concepts/outcomes) reports. |
| `201` | Other `POST` requests that create or run something, such as a [benefit](/docs/concepts/benefits), [policy](/docs/concepts/policies-and-rules), preview or replay. |
| `202` | Accepted work that finishes in the background: imports, exports and data deletions. Their response includes the ID to check later. |
Timestamps are ISO 8601 strings in UTC, such as `2026-10-29T14:05:12.000Z`. Responses are never cached.
## Object IDs [#object-ids]
Every object ID carries a type prefix. Send IDs back exactly as you received them.
| Prefix | Object |
|---|---|
| `eval_` | [Evaluation](/docs/concepts/evaluations) |
| `ben_` | [Benefit](/docs/concepts/benefits) |
| `pol_` | [Policy](/docs/concepts/policies-and-rules) |
| `out_` | [Outcome](/docs/concepts/outcomes) |
| `lst_` | [Allow or block list](/docs/concepts/allow-and-block-lists) entry |
| `cid_` | [Custom identifier](/docs/concepts/custom-identifiers) |
| `imp_` | Import |
| `exp_` | Export |
| `del_` | Data deletion |
| `req_` | Request log |
| `aud_` | Audit event |
| `ws_` | [Workspace](/docs/keys-and-access#workspaces) |
An ID with the wrong prefix for an endpoint is rejected with `INVALID_FIELD`.
## Idempotency [#idempotency]
Every write requires an `Idempotency-Key` header. Writes are `POST` requests that create or change something, and every `PATCH` and `DELETE`.
```http
Idempotency-Key: 0b8f5c1e-4d2a-4c7b-9e3f-6a1d8b2c5e70
```
- Use 1 to 128 printable characters with no spaces. A UUID works well.
- Create one key per attempt and reuse it for every retry of that attempt.
- A retry with the same key and the same body returns the original response instead of repeating the write. A [claim](/docs/concepts/evaluations) is never evaluated or recorded twice, a CSV import is never created twice, and a delete that already succeeded returns its original response. A retried CSV import returns a fresh upload link.
- The same key with a different body returns `IDEMPOTENCY_CONFLICT`.
- A write without a key returns `IDEMPOTENCY_KEY_REQUIRED`.
Each endpoint's page says whether it needs the header.
## Pagination [#pagination]
List endpoints return one page at a time.
| Parameter | Type | Description |
|---|---|---|
| `limit` | integer, 1 to 100 | Items per page. Defaults to 50. |
| `cursor` | string | The `next_cursor` from the previous page. Leave it out for the first page. |
```json
{
"object": "list",
"data": [],
"next_cursor": null
}
```
`next_cursor` is `null` on the last page. Cursors are sealed and bound to your [workspace](/docs/keys-and-access#workspaces) and to the list they came from. Pass them back unchanged. A changed cursor, or one from another list, returns `CURSOR_INVALID`.
## Rate limits [#rate-limits]
Traffic limits depend on your plan. When a request goes over a limit, AbuseZero returns `429` with the code `RATE_LIMITED` and a `Retry-After` header that gives the wait in seconds. Wait that long, then retry with the same `Idempotency-Key`.
[Previews](/docs/api-reference/previews-simulations-replays#preview-an-evaluation) and Test console runs have their own limit, separate from evaluations.
The Node.js SDK never turns a rate limit into a local allow or deny, so traffic spikes can't switch protection off.
## Request IDs [#request-ids]
Every response includes an `X-Request-ID` header. Error responses repeat it in the body as `request_id`. Include it when you contact support about a specific request.
## Time limits [#time-limits]
Every endpoint runs within a fixed time limit. An [evaluation](/docs/concepts/evaluations) that can't finish in time returns `DEADLINE_EXCEEDED` with `retryable: true`. Retry it with the same `Idempotency-Key`: if the first attempt finished, you get its result.
## Errors [#errors]
Errors use one envelope across the whole API:
```json
{
"error": {
"type": "invalid_request",
"code": "CURSOR_INVALID",
"message": "Clear the page cursor and try again.",
"field": "cursor",
"retryable": false
},
"request_id": "7d0e5c2a-1f4b-4e8a-b6c3-9a2d5e0f1b74"
}
```
See [REST API errors](/docs/api-reference/errors) for every type and code.
## Resources [#resources]
Evaluate a claim and read past evaluations.
Try requests and policy changes before they reach users.
Report whether a benefit was granted.
Configure what you protect.
Build custom rules on a protection level.
Decide specific users, emails, domains, networks and devices.
Register your own identifiers.
Bring in your existing claim history.
Download your evaluation data.
Delete a user's history.
Read daily allow and deny totals.
Inspect recent API requests.
Review changes made in your workspace.
Manage the sites that send browser data.
List the AI agents AbuseZero recognizes.
List the workspaces a credential can use.
# Outcomes (/docs/api-reference/outcomes)
Every allowed claim has an [outcome](/docs/concepts/outcomes): the record of whether the user actually received the [benefit](/docs/concepts/benefits). The outcomes you report become the history that later [evaluations](/docs/concepts/evaluations) are checked against.
| Endpoint | Scope |
|---|---|
| [`POST /v1/evaluations/{id}/outcome`](#report-an-outcome) | `outcomes:write` |
| [`GET /v1/outcomes`](#list-outcomes) | `outcomes:read` |
| [`GET /v1/outcomes/{id}`](#retrieve-an-outcome) | `outcomes:read` |
How outcomes start depends on the benefit's `outcome_tracking` setting. See [Benefits](/docs/api-reference/benefits#benefit-configuration).
## The outcome object [#the-outcome-object]
| Field | Type | Description |
|---|---|---|
| `object` | string | Always `outcome`. |
| `id` | string | The [outcome](/docs/concepts/outcomes)'s ID, starting with `out_`. |
| `evaluation` | string or `null` | The [evaluation](/docs/api-reference/evaluations#the-evaluation-object) that allowed the claim. `null` for an imported outcome. See [Evaluations](/docs/concepts/evaluations). |
| `state` | string | The outcome's state. See [Outcome states](#outcome-states). |
| `source` | string | `evaluation` when an evaluation created it, or `import` when it came from an [import](/docs/api-reference/imports). |
| `occurred_at` | string | When the claim was allowed, or when an imported [benefit](/docs/concepts/benefits) was granted. |
| `due_at` | string or `null` | When the outcome report is due, for benefits that use `report_outcome`. Otherwise `null`. |
### Outcome states [#outcome-states]
| State | Meaning | Counts toward eligibility |
|---|---|---|
| `assumed_granted` | The benefit uses `assume_granted`, so the allowed claim counts as granted at once. | Yes |
| `awaiting_outcome` | The benefit uses `report_outcome`. AbuseZero is waiting for your report by `due_at`. | Yes |
| `granted` | You reported or imported that the user received the benefit. | Yes |
| `not_granted` | You reported that the user didn't receive the benefit. The user can claim it again. | No |
| `outcome_unknown` | No report arrived by `due_at`. The benefit's `missing_outcome_behavior` decides whether it counts. | `hold_eligibility`: yes. `release_eligibility`: no. |
| `reversed` | You reported that a granted benefit was taken back. The claim still counts. | Yes |
New states can be added within `v1`.
## Report an outcome [#report-an-outcome]
`POST /v1/evaluations/{id}/outcome` · Scope [`outcomes:write`](/docs/api-reference/authentication#scopes) · Requires [`Idempotency-Key`](/docs/api-reference#idempotency)
Also available as [`abusezero outcomes report`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_report_outcome`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`outcome()`](/docs/tools/server-sdk#report-an-outcome) in the AbuseZero Node.js SDK.
Reports what happened to an allowed claim. `{id}` is the ID of the [evaluation](/docs/concepts/evaluations) that allowed it.
Report `granted` when you deliver the [benefit](/docs/concepts/benefits) and `not_granted` when you don't. Report `reversed` when you take a granted benefit back. For a benefit that uses `report_outcome`, report by the `due_at` the evaluation returned.
### Request body [#request-body]
| Field | Type | Required | Description |
|---|---|---|---|
| `outcome` | string | Yes | `granted`, `not_granted` or `reversed`. |
| `reason` | string | With `not_granted` or `reversed` | Why the [outcome](/docs/concepts/outcomes) changed, 3 to 500 characters. Required when you report `not_granted` or `reversed` and the outcome is in a different state. |
| `external_outcome_id` | string | No | Your own ID for the grant, such as a subscription or order ID. 1 to 128 printable characters with no spaces. |
### Allowed changes [#allowed-changes]
| You report | Allowed when the outcome is |
|---|---|
| `granted` | `assumed_granted`, `awaiting_outcome` or `outcome_unknown` |
| `not_granted` | `assumed_granted`, `awaiting_outcome` or `outcome_unknown` |
| `reversed` | `granted` or `assumed_granted` |
Reporting the state the outcome already has changes nothing, and is a safe way to attach an `external_outcome_id`. Every change is recorded in your [audit log](/docs/api-reference/audit-logs).
### Example [#example]
```ts tab="Node.js"
import { randomUUID } from "node:crypto";
const report = await abusezero.outcome(
"eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93",
{ outcome: "granted", external_outcome_id: "sub_20481" },
randomUUID(),
);
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/evaluations/eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93/outcome \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 4a7c1e9b-2f6d-4b83-a5e0-8d1c3f7b9a26" \
-d '{
"outcome": "granted",
"external_outcome_id": "sub_20481"
}'
```
### Response [#response]
Returns `200`.
```json
{
"object": "outcome_report",
"evaluation": "eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93",
"state": "granted",
"conflicts": []
}
```
| Field | Type | Description |
|---|---|---|
| `object` | string | Always `outcome_report`. |
| `evaluation` | string | The evaluation the outcome belongs to. |
| `state` | string | The outcome's state after the report. |
| `conflicts` | array | Outcome IDs that now overlap with this claim. See below. |
### Conflicts [#conflicts]
For a benefit that uses `release_eligibility`, a claim stops counting when its report is overdue. If you then report it `granted`, other claims with the same identifiers may have been allowed in the meantime. `conflicts` lists their outcome IDs so you can reconcile them in your application. It's empty in every other case.
### Errors [#errors]
| Status | Code | When |
|---|---|---|
| 422 | `IDEMPOTENCY_KEY_REQUIRED` | The `Idempotency-Key` header is missing or invalid. |
| 422 | `INVALID_FIELD` | `outcome` is missing, a field is invalid, or the ID doesn't start with `eval_`. |
| 422 | `UNSUPPORTED_FIELD` | The body has a field this endpoint doesn't define. |
| 422 | `REASON_REQUIRED` | A `not_granted` or `reversed` report that changes the outcome has no `reason`. |
| 404 | `EVALUATION_NOT_FOUND` | No evaluation with this ID exists in the workspace. |
| 409 | `OUTCOME_CONFLICT` | The evaluation was denied, a newer evaluation replaced it for this claim, the change isn't allowed from the current state, or the `external_outcome_id` belongs to another outcome. Read the evaluation's current outcome before reporting again. |
| 409 | `IDEMPOTENCY_CONFLICT` | The key was already used with a different body. |
| 429 | `RATE_LIMITED` | The workspace is over a traffic limit. Wait for `Retry-After`. |
## List outcomes [#list-outcomes]
`GET /v1/outcomes` · Scope [`outcomes:read`](/docs/api-reference/authentication#scopes)
Also available as [`abusezero outcomes list`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_list_outcomes`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`outcomes()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
Lists [outcomes](/docs/concepts/outcomes), newest first.
### Query parameters [#query-parameters]
| Parameter | Type | Required | Description |
|---|---|---|---|
| `limit` | integer | No | Items per page, 1 to 100. Defaults to 50. |
| `cursor` | string | No | The `next_cursor` from the previous page. |
### Example [#example-1]
```ts tab="Node.js"
const page = await abusezero.outcomes({ limit: 50 });
```
```bash tab="cURL"
curl "https://api.abusezero.com/v1/outcomes?limit=50" \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
### Response [#response-1]
```json
{
"object": "list",
"data": [
{
"object": "outcome",
"id": "out_9e7c5a31-2d4b-4f86-b0a9-6c1e8d3f5a72",
"evaluation": "eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93",
"state": "assumed_granted",
"source": "evaluation",
"occurred_at": "2026-10-29T14:02:47.000Z",
"due_at": null
},
{
"object": "outcome",
"id": "out_1f3a5c7e-9b2d-4e6f-8a0c-3d5e7f9a1b24",
"evaluation": null,
"state": "granted",
"source": "import",
"occurred_at": "2026-09-14T08:30:00.000Z",
"due_at": null
}
],
"next_cursor": "kZ3x9QvB7mT2pLr8Wn4cYh6dFs1aJe5u"
}
```
### Errors [#errors-1]
| Status | Code | When |
|---|---|---|
| 422 | `CURSOR_INVALID` | The cursor was changed or belongs to another list. |
| 422 | `INVALID_FIELD` | A query parameter is invalid. |
## Retrieve an outcome [#retrieve-an-outcome]
`GET /v1/outcomes/{id}` · Scope [`outcomes:read`](/docs/api-reference/authentication#scopes)
Also available as [`abusezero outcomes get`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_get_outcome`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`getOutcome()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
Returns one [outcome object](#the-outcome-object).
### Example [#example-2]
```ts tab="Node.js"
const outcome = await abusezero.getOutcome("out_9e7c5a31-2d4b-4f86-b0a9-6c1e8d3f5a72");
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/outcomes/out_9e7c5a31-2d4b-4f86-b0a9-6c1e8d3f5a72 \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
### Response [#response-2]
```json
{
"object": "outcome",
"id": "out_9e7c5a31-2d4b-4f86-b0a9-6c1e8d3f5a72",
"evaluation": "eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93",
"state": "granted",
"source": "evaluation",
"occurred_at": "2026-10-29T14:02:47.000Z",
"due_at": null
}
```
### Errors [#errors-2]
| Status | Code | When |
|---|---|---|
| 404 | `RESOURCE_NOT_FOUND` | No [outcome](/docs/concepts/outcomes) with this ID exists in the workspace. |
| 422 | `INVALID_FIELD` | The ID doesn't start with `out_`. |
## Next steps [#next-steps]
When and how to report each outcome in your application.
How outcomes shape later evaluations.
# Policies (/docs/api-reference/policies)
A [policy](/docs/concepts/policies-and-rules) decides the [claims](/docs/concepts/evaluations) of every [benefit](/docs/concepts/benefits) that uses it. Every [workspace](/docs/keys-and-access#workspaces) has three [protection levels](/docs/concepts/protection-levels), Lenient, Balanced and Strict, which AbuseZero defines and maintains. You can use a level as it is, or build a custom policy on one with your own rules.
| Endpoint | Scope |
|---|---|
| [`POST /v1/policies`](#create-a-policy) | `policies:write` |
| [`GET /v1/policies`](#list-policies) | `policies:read` |
| [`GET /v1/policies/{id}`](#retrieve-a-policy) | `policies:read` |
| [`PATCH /v1/policies/{id}`](#update-a-policy) | `policies:write` |
| [`POST /v1/policies/{id}/duplicate`](#duplicate-a-policy) | `policies:write` |
| [`DELETE /v1/policies/{id}`](#delete-a-policy) | `policies:write` |
| [`GET /v1/policies/{id}/versions`](#list-policy-versions) | `policies:read` |
| [`GET /v1/policies/{id}/versions/{version}`](#retrieve-a-policy-version) | `policies:read` |
| [`GET /v1/policy-signals`](#list-conditions) | `policies:read` |
With an OAuth access token, `policies:write` also needs the member's **Publish policies and benefits** permission.
## The policy object [#the-policy-object]
| Field | Type | Description |
|---|---|---|
| `object` | string | Always `policy`. |
| `id` | string | The [policy](/docs/concepts/policies-and-rules)'s ID, starting with `pol_`. |
| `key` | string | The key [benefits](/docs/concepts/benefits) use to select the policy. |
| `name` | string | The policy's display name. |
| `kind` | string | `managed` for a [protection level](/docs/concepts/protection-levels), `custom` for your own policy. |
| `active_version` | integer | The version [evaluations](/docs/concepts/evaluations) use now. |
| `created_at` | string | When the policy was created. |
| `updated_at` | string | When the policy last changed. |
## Protection levels [#protection-levels]
[Protection levels](/docs/concepts/protection-levels) appear in every [workspace](/docs/keys-and-access#workspaces) as `managed` [policies](/docs/concepts/policies-and-rules) with the keys `lenient`, `balanced` and `strict`. AbuseZero defines and maintains what each level contains. Levels are read-only: to change one, [duplicate](#duplicate-a-policy) it and edit the copy.
See [Protection levels](/docs/concepts/protection-levels) for how to choose one.
## Custom policy definition [#custom-policy-definition]
A custom [policy](/docs/concepts/policies-and-rules) is a definition you write. It can build on a [protection level](/docs/concepts/protection-levels).
| Field | Type | Required | Description |
|---|---|---|---|
| `base` | string | No | The protection level to build on: `lenient`, `balanced` or `strict`. |
| `default_result` | string | Yes | `allow` or `deny`: the result when nothing else decides. |
| `risk_threshold` | integer or `null` | No | A multiple of 5 from 5 to 100; other values return `INVALID_FIELD`. A [claim](/docs/concepts/evaluations) whose AbuseZero Risk Score reaches this value is denied when no rule decides it. Leave it out or send `null` to keep the setting of your `base` level, or AbuseZero's standard setting when there is no `base`. |
| `rules` | array | Yes | Up to 50 of your own rules. Send `[]` for none. |
### How a policy decides [#how-a-policy-decides]
1. Your rules are checked in the order you list them. The first rule that matches decides the result.
2. Your rules take priority over the protection level you build on.
3. When no rule decides, a claim whose AbuseZero Risk Score reaches `risk_threshold` is denied.
4. Otherwise, `default_result` applies.
[Allow and block list](/docs/concepts/allow-and-block-lists) entries decide a matching claim outright, whatever the policy says. Each evaluation's `reason` tells you what decided it. See [Reason codes](/docs/reason-codes).
### Rules [#rules]
| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | Yes | Your ID for the rule, 1 to 64 characters, unique in the policy. The evaluation's `reason.rule` returns this ID when the rule decides a claim. |
| `name` | string | Yes | A readable name, 1 to 100 characters. When you retrieve an evaluation the rule decided, its `reason.message` quotes this name. |
| `result` | string | Yes | `allow` or `deny`: the result when the rule matches. |
| `when` | object | Yes | A condition, or a group of conditions. |
### Conditions and groups [#conditions-and-groups]
A condition compares one value with your value:
```json
{ "signal": "network.anonymous", "operator": "eq", "value": true }
```
| Field | Type | Required | Description |
|---|---|---|---|
| `signal` | string | Yes | A condition key from [List conditions](#list-conditions). |
| `operator` | string | Yes | See [Operators](#operators). |
| `value` | string, number or boolean | For comparisons | The value to compare with. Its type must match the condition's type. Leave it out for status operators. |
Combine conditions with a group. `all` matches when every item matches. `any` matches when at least one item matches.
```json
{
"all": [
{ "signal": "network.anonymous", "operator": "eq", "value": true },
{ "any": [
{ "signal": "email.verified", "operator": "eq", "value": false },
{ "signal": "email.disposable", "operator": "eq", "value": true }
] }
]
}
```
- A group holds 1 to 10 items.
- Groups nest at most two levels deep.
- A rule has at most 10 conditions in total.
### Operators [#operators]
| Operator | Matches when | Condition types |
|---|---|---|
| `eq` | The value equals `value`. | All |
| `neq` | The value differs from `value`. | All |
| `gt` | The value is greater than `value`. | Number |
| `gte` | The value is greater than or equal to `value`. | Number |
| `lt` | The value is less than `value`. | Number |
| `lte` | The value is less than or equal to `value`. | Number |
| `is_missing` | You didn't send the input this condition needs. | All |
| `is_unknown` | AbuseZero received the input but couldn't determine a value for it. | All |
| `is_unavailable` | No value was available when the evaluation ran. | All |
Comparison operators match only when the condition has a value. Use the status operators to write rules for inputs you didn't send or values that weren't available.
## Create a policy [#create-a-policy]
`POST /v1/policies` · Scope [`policies:write`](/docs/api-reference/authentication#scopes) · Requires [`Idempotency-Key`](/docs/api-reference#idempotency)
Also available as [`abusezero policies create`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_create_policy`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`createPolicy()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
Creates a custom [policy](/docs/concepts/policies-and-rules) at version 1.
### Request body [#request-body]
| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | Yes | Display name, 1 to 80 characters. |
| `key` | string | Yes | Lowercase letters, numbers and underscores, starting with a letter; up to 64 characters. Unique in the [workspace](/docs/keys-and-access#workspaces). |
| `definition` | object | Yes | The [custom policy definition](#custom-policy-definition). |
### Example [#example]
```ts tab="Node.js"
import { randomUUID } from "node:crypto";
const policy = await abusezero.createPolicy(
{
name: "Trial protection",
key: "trial_protection",
definition: {
base: "balanced",
default_result: "allow",
rules: [
{
id: "unverified_anonymous",
name: "Unverified email on an anonymous network",
result: "deny",
when: {
all: [
{ signal: "network.anonymous", operator: "eq", value: true },
{ signal: "email.verified", operator: "eq", value: false },
],
},
},
],
},
},
randomUUID(),
);
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/policies \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3f9a1c7e-6b2d-4e85-a0c3-8d5f1b7e9a24" \
-d '{
"name": "Trial protection",
"key": "trial_protection",
"definition": {
"base": "balanced",
"default_result": "allow",
"rules": [
{
"id": "unverified_anonymous",
"name": "Unverified email on an anonymous network",
"result": "deny",
"when": {
"all": [
{ "signal": "network.anonymous", "operator": "eq", "value": true },
{ "signal": "email.verified", "operator": "eq", "value": false }
]
}
}
]
}
}'
```
### Response [#response]
Returns `201`.
```json
{
"object": "policy",
"id": "pol_c4d8e2a7-1b5f-4a93-8e6d-2f0b9c7a1e35",
"revision": 1,
"version": 1
}
```
To start using the policy, set it as a [benefit](/docs/concepts/benefits)'s `policy_key`. To see its effect first, run a [replay](/docs/api-reference/previews-simulations-replays#run-a-replay).
### Errors [#errors]
| Status | Code | When |
|---|---|---|
| 409 | `POLICY_KEY_TAKEN` | Another policy uses this key. |
| 409 | `IDEMPOTENCY_CONFLICT` | The key was already used with a different body. |
| 422 | `INVALID_FIELD` | The definition is invalid, for example a duplicate rule ID, a reserved rule ID, a value of the wrong type, or groups nested too deeply. `error.field` gives the path to the problem, such as `definition.rules.0.id`. |
| 422 | `UNSUPPORTED_FIELD` | A rule uses a condition that isn't supported, or a [custom identifier](/docs/concepts/custom-identifiers) that isn't registered. |
| 422 | `POLICY_NOT_FOUND` | `base` isn't an available [protection level](/docs/concepts/protection-levels). |
| 422 | `IDEMPOTENCY_KEY_REQUIRED` | The `Idempotency-Key` header is missing or invalid. |
## List policies [#list-policies]
`GET /v1/policies` · Scope [`policies:read`](/docs/api-reference/authentication#scopes)
Also available as [`abusezero policies list`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_list_policies`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`policies()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
Lists the [workspace](/docs/keys-and-access#workspaces)'s [protection levels](/docs/concepts/protection-levels) and custom [policies](/docs/concepts/policies-and-rules), newest first.
### Query parameters [#query-parameters]
| Parameter | Type | Required | Description |
|---|---|---|---|
| `limit` | integer | No | Items per page, 1 to 100. Defaults to 50. |
| `cursor` | string | No | The `next_cursor` from the previous page. |
### Example [#example-1]
```ts tab="Node.js"
const page = await abusezero.policies({ limit: 50 });
```
```bash tab="cURL"
curl "https://api.abusezero.com/v1/policies?limit=50" \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
### Response [#response-1]
A [list](/docs/api-reference#pagination) of [policy objects](#the-policy-object).
### Errors [#errors-1]
| Status | Code | When |
|---|---|---|
| 422 | `CURSOR_INVALID` | The cursor was changed or belongs to another list. |
## Retrieve a policy [#retrieve-a-policy]
`GET /v1/policies/{id}` · Scope [`policies:read`](/docs/api-reference/authentication#scopes)
Also available as [`abusezero policies get`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_get_policy`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`getPolicy()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
### Example [#example-2]
```ts tab="Node.js"
const policy = await abusezero.getPolicy("pol_c4d8e2a7-1b5f-4a93-8e6d-2f0b9c7a1e35");
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/policies/pol_c4d8e2a7-1b5f-4a93-8e6d-2f0b9c7a1e35 \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
### Response [#response-2]
```json
{
"object": "policy",
"id": "pol_c4d8e2a7-1b5f-4a93-8e6d-2f0b9c7a1e35",
"key": "trial_protection",
"name": "Trial protection",
"kind": "custom",
"active_version": 2,
"created_at": "2026-10-28T09:15:00.000Z",
"updated_at": "2026-10-29T11:20:00.000Z"
}
```
To read the rules, [retrieve the active version](#retrieve-a-policy-version).
### Errors [#errors-2]
| Status | Code | When |
|---|---|---|
| 404 | `RESOURCE_NOT_FOUND` | No [policy](/docs/concepts/policies-and-rules) with this ID exists in the workspace. |
| 422 | `INVALID_FIELD` | The ID doesn't start with `pol_`. |
## Update a policy [#update-a-policy]
`PATCH /v1/policies/{id}` · Scope [`policies:write`](/docs/api-reference/authentication#scopes) · Requires [`Idempotency-Key`](/docs/api-reference#idempotency)
Also available as [`abusezero policies update`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_update_policy`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`updatePolicy()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
Saves a new version of a custom [policy](/docs/concepts/policies-and-rules). Send the complete definition. Every [benefit](/docs/concepts/benefits) that uses the policy evaluates with the new version from the moment the update succeeds. Earlier [evaluations](/docs/concepts/evaluations) keep the version they ran under.
### Request body [#request-body-1]
| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | Yes | Display name, 1 to 80 characters. |
| `definition` | object | Yes | The complete [custom policy definition](#custom-policy-definition). |
| `revision` | integer | Yes | The policy's current `active_version`. |
### Example [#example-3]
```ts tab="Node.js"
import { randomUUID } from "node:crypto";
await abusezero.updatePolicy(
"pol_c4d8e2a7-1b5f-4a93-8e6d-2f0b9c7a1e35",
{
name: "Trial protection",
definition: { base: "strict", default_result: "allow", rules: [] },
revision: 2,
},
randomUUID(),
);
```
```bash tab="cURL"
curl -X PATCH https://api.abusezero.com/v1/policies/pol_c4d8e2a7-1b5f-4a93-8e6d-2f0b9c7a1e35 \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 8e4c2a6f-9d1b-4f37-b8a5-0c6e3d9f2b17" \
-d '{
"name": "Trial protection",
"definition": { "base": "strict", "default_result": "allow", "rules": [] },
"revision": 2
}'
```
### Response [#response-3]
Returns `200`.
```json
{
"object": "policy",
"id": "pol_c4d8e2a7-1b5f-4a93-8e6d-2f0b9c7a1e35",
"revision": 3,
"version": 3
}
```
### Errors [#errors-3]
| Status | Code | When |
|---|---|---|
| 404 | `POLICY_NOT_FOUND` | No custom policy with this ID exists in the workspace. |
| 409 | `POLICY_CONFLICT` | `revision` isn't the current version. Read the policy again. |
| 409 | `PROTECTION_LEVEL_READ_ONLY` | The policy is a [protection level](/docs/concepts/protection-levels). Duplicate it instead. |
| 422 | `INVALID_FIELD` | The definition is invalid. |
| 422 | `UNSUPPORTED_FIELD` | A rule uses an unsupported condition or an unregistered [custom identifier](/docs/concepts/custom-identifiers). |
## Duplicate a policy [#duplicate-a-policy]
`POST /v1/policies/{id}/duplicate` · Scope [`policies:write`](/docs/api-reference/authentication#scopes) · Requires [`Idempotency-Key`](/docs/api-reference#idempotency)
Also available as [`abusezero policies duplicate`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_duplicate_policy`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`duplicatePolicy()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
Creates a new custom [policy](/docs/concepts/policies-and-rules) from the active version of an existing one. Duplicating a [protection level](/docs/concepts/protection-levels) creates a custom policy built on that level, ready for your own rules.
### Request body [#request-body-2]
| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | Yes | Display name for the copy, 1 to 80 characters. |
| `key` | string | Yes | Key for the copy. Lowercase letters, numbers and underscores, starting with a letter; up to 64 characters. |
### Example [#example-4]
```ts tab="Node.js"
import { randomUUID } from "node:crypto";
const copy = await abusezero.duplicatePolicy(
"pol_0e6b4d82-7a3c-4f19-8b5e-1d9c7a3f5e60",
{ name: "Balanced with my rules", key: "balanced_custom" },
randomUUID(),
);
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/policies/pol_0e6b4d82-7a3c-4f19-8b5e-1d9c7a3f5e60/duplicate \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 5a2e8c4b-1f7d-4b90-a6c3-9e2b5d8f1a73" \
-d '{ "name": "Balanced with my rules", "key": "balanced_custom" }'
```
### Response [#response-4]
Returns `201`.
```json
{
"object": "policy",
"id": "pol_b3f7d1a9-2c6e-4d85-9a0b-6e4c2f8d1b39",
"revision": 1,
"version": 1
}
```
### Errors [#errors-4]
| Status | Code | When |
|---|---|---|
| 404 | `POLICY_NOT_FOUND` | No policy with this ID exists in the workspace. |
| 409 | `POLICY_KEY_TAKEN` | Another policy uses this key. |
## Delete a policy [#delete-a-policy]
`DELETE /v1/policies/{id}` · Scope [`policies:write`](/docs/api-reference/authentication#scopes) · Requires [`Idempotency-Key`](/docs/api-reference#idempotency)
Also available as [`abusezero policies delete`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_delete_policy`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`deletePolicy()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
Deleting a custom [policy](/docs/concepts/policies-and-rules) is permanent and removes all its versions. Move every [benefit](/docs/concepts/benefits) that uses it to another policy first. [Protection levels](/docs/concepts/protection-levels) can't be deleted.
### Example [#example-5]
```ts tab="Node.js"
import { randomUUID } from "node:crypto";
await abusezero.deletePolicy("pol_c4d8e2a7-1b5f-4a93-8e6d-2f0b9c7a1e35", randomUUID());
```
```bash tab="cURL"
curl -X DELETE https://api.abusezero.com/v1/policies/pol_c4d8e2a7-1b5f-4a93-8e6d-2f0b9c7a1e35 \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 1c7e3a9f-4b2d-4e68-8f0a-5d3b9c1e7a46" \
-d '{}'
```
### Response [#response-5]
Returns `200`.
```json
{
"object": "deleted",
"id": "pol_c4d8e2a7-1b5f-4a93-8e6d-2f0b9c7a1e35",
"deleted": true
}
```
### Errors [#errors-5]
| Status | Code | When |
|---|---|---|
| 404 | `POLICY_NOT_FOUND` | No custom policy with this ID exists in the workspace. A retry with the same `Idempotency-Key` after a successful delete returns the original response instead. |
| 409 | `POLICY_IN_USE` | Benefits still use the policy. The message says how many. |
| 409 | `PROTECTION_LEVEL_READ_ONLY` | The policy is a protection level. |
## List policy versions [#list-policy-versions]
`GET /v1/policies/{id}/versions` · Scope [`policies:read`](/docs/api-reference/authentication#scopes)
Also available as [`abusezero policies versions`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_list_policy_versions`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`call()`](/docs/tools/server-sdk#call-for-any-endpoint) in the AbuseZero Node.js SDK.
Lists every version of a [policy](/docs/concepts/policies-and-rules), newest first, in one page.
### Example [#example-6]
```ts tab="Node.js"
const versions = await abusezero.call(
"/v1/policies/pol_c4d8e2a7-1b5f-4a93-8e6d-2f0b9c7a1e35/versions",
);
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/policies/pol_c4d8e2a7-1b5f-4a93-8e6d-2f0b9c7a1e35/versions \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
### Response [#response-6]
```json
{
"object": "list",
"data": [
{ "object": "policy_version", "version": 2, "created_at": "2026-10-29T11:20:00.000Z" },
{ "object": "policy_version", "version": 1, "created_at": "2026-10-28T09:15:00.000Z" }
],
"next_cursor": null
}
```
## Retrieve a policy version [#retrieve-a-policy-version]
`GET /v1/policies/{id}/versions/{version}` · Scope [`policies:read`](/docs/api-reference/authentication#scopes)
Also available as [`abusezero policies version`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_get_policy_version`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`call()`](/docs/tools/server-sdk#call-for-any-endpoint) in the AbuseZero Node.js SDK.
Returns one version. For a custom [policy](/docs/concepts/policies-and-rules), the version includes the definition you saved. For a [protection level](/docs/concepts/protection-levels), it names the level only: AbuseZero keeps each level's contents private.
### Example [#example-7]
```ts tab="Node.js"
const version = await abusezero.call(
"/v1/policies/pol_c4d8e2a7-1b5f-4a93-8e6d-2f0b9c7a1e35/versions/2",
);
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/policies/pol_c4d8e2a7-1b5f-4a93-8e6d-2f0b9c7a1e35/versions/2 \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
### Response [#response-7]
```json title="Custom policy"
{
"object": "policy_version",
"version": 2,
"managed": false,
"definition": {
"base": "balanced",
"risk_threshold": 75,
"default_result": "allow",
"rules": [
{
"id": "unverified_anonymous",
"name": "Unverified email on an anonymous network",
"result": "deny",
"when": {
"all": [
{ "signal": "network.anonymous", "operator": "eq", "value": true },
{ "signal": "email.verified", "operator": "eq", "value": false }
]
}
}
]
},
"created_at": "2026-10-29T11:20:00.000Z"
}
```
```json title="Protection level"
{
"object": "policy_version",
"version": 1,
"managed": true,
"level": "balanced",
"created_at": "2026-10-01T08:00:00.000Z"
}
```
| Field | Type | Description |
|---|---|---|
| `object` | string | Always `policy_version`. |
| `version` | integer | The version number. |
| `managed` | boolean | `true` for a protection level. |
| `definition` | object | Custom policies only. The definition saved with this version: `base`, `risk_threshold`, `default_result` and your `rules`. `risk_threshold` is `null` unless you set it yourself, so a definition you read can be saved back unchanged. |
| `level` | string | Protection levels only. `lenient`, `balanced` or `strict`. |
| `created_at` | string | When the version was saved. |
### Errors [#errors-6]
| Status | Code | When |
|---|---|---|
| 404 | `POLICY_VERSION_NOT_FOUND` | The policy has no such version. |
## List conditions [#list-conditions]
`GET /v1/policy-signals` · Scope [`policies:read`](/docs/api-reference/authentication#scopes)
Also available as [`abusezero policies signals`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_list_policy_signals`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`call()`](/docs/tools/server-sdk#call-for-any-endpoint) in the AbuseZero Node.js SDK.
Lists every condition your custom rules can use, in one page. Takes no parameters.
### Example [#example-8]
```ts tab="Node.js"
const conditions = await abusezero.call("/v1/policy-signals");
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/policy-signals \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
### Response [#response-8]
```json
{
"object": "list",
"data": [
{ "object": "condition", "key": "email.verified", "label": "Email verified by your app", "type": "boolean" },
{ "object": "condition", "key": "user.prior_claims", "label": "Earlier claims by this user", "type": "number", "min": 0 },
{ "object": "condition", "key": "network.risk", "label": "Network risk", "type": "string", "values": ["low", "elevated", "high"] }
],
"next_cursor": null
}
```
The example shows three of the conditions.
| Field | Type | Description |
|---|---|---|
| `key` | string | The value to use as `signal` in a condition. |
| `label` | string | A readable name. |
| `type` | string | `boolean`, `number` or `string`. A comparison's `value` must have this type. |
| `values` | array | Present when only certain values are allowed. |
| `min`, `max` | number | Present when numbers have a range. |
### Available conditions [#available-conditions]
| Key | Label | Type |
|---|---|---|
| `email.verified` | Email verified by your app | boolean |
| `email.disposable` | Disposable email address | boolean |
| `phone.verified` | Phone verified by your app | boolean |
| `user.prior_claims` | Earlier [claims](/docs/concepts/evaluations) by this user | whole number, 0 or more |
| `email.prior_claims` | Earlier claims with this email | whole number, 0 or more |
| `device.prior_claims` | Earlier claims from this device | whole number, 0 or more |
| `payment.prior_claims` | Earlier claims with this payment method | whole number, 0 or more |
| `phone.prior_claims` | Earlier claims with this phone | whole number, 0 or more |
| `network.risk` | Network risk | `low`, `elevated` or `high` |
| `network.anonymous` | Anonymous network | boolean |
| `network.repeat_attempts` | Recent attempts from this network | whole number, 0 or more |
| `network.country` | Network country (ISO code) | string, such as `DE` |
| `automation.detected` | Automation detected | boolean |
| `custom..prior_outcomes` | Earlier claims with your [custom identifier](/docs/concepts/custom-identifiers) `` | whole number, 0 or more |
For a custom identifier condition, replace `` with the key of a [registered custom identifier](/docs/api-reference/custom-identifiers), such as `custom.license_id.prior_outcomes`. A custom identifier that a [policy](/docs/concepts/policies-and-rules) uses can't be deleted. New conditions can be added within `v1`.
# Previews, simulations and replays (/docs/api-reference/previews-simulations-replays)
A preview runs the same [evaluation](/docs/concepts/evaluations) as live traffic without recording a claim. Simulations and replays reuse the evidence your past evaluations retained and apply a [policy](/docs/concepts/policies-and-rules) to it, so a changed result comes from your policy change. Use them to see how AbuseZero will decide before anything changes for a real user.
| Endpoint | Scope | What it does | Uses credits |
|---|---|---|---|
| [`POST /v1/evaluation-previews`](#preview-an-evaluation) | `simulations:run` | Runs one full evaluation and records nothing. | Yes |
| [`POST /v1/simulations`](#run-a-simulation) | `simulations:run` | Runs a [benefit](/docs/concepts/benefits)'s recent claims, in sequence, under a policy you choose. | No |
| [`POST /v1/replays`](#run-a-replay) | `simulations:run` | Re-decides a benefit's recent evaluations under a policy you choose, and shows which results would change. | No |
## Preview an evaluation [#preview-an-evaluation]
`POST /v1/evaluation-previews` · Scope [`simulations:run`](/docs/api-reference/authentication#scopes) · Requires [`Idempotency-Key`](/docs/api-reference#idempotency)
Also available as [`abusezero evaluations preview`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_preview_evaluation`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`previewEvaluation()`](/docs/tools/server-sdk#preview-an-evaluation) in the AbuseZero Node.js SDK.
Runs a complete [evaluation](/docs/concepts/evaluations) and returns the result without recording anything. The preview creates no evaluation, claim or [outcome](/docs/concepts/outcomes), and never affects a later evaluation.
A preview is a real evaluation and uses [workspace](/docs/keys-and-access#workspaces) credits, like `POST /v1/evaluations`. Previews and Test console runs also have their own rate limit, separate from evaluations. Over it, a preview returns `429` [`RATE_LIMITED`](/docs/api-reference/errors#rate-limit) with a `Retry-After` header.
### Request body [#request-body]
The same body as [Create an evaluation](/docs/api-reference/evaluations#request-body).
### Example [#example]
```ts tab="Node.js"
import { randomUUID } from "node:crypto";
const preview = await abusezero.previewEvaluation(
{
benefit: "pro_trial",
claim_id: "preview_user_4821",
subject: { external_id: "user_4821", email: "jordan@example.com" },
context: { ip: "203.0.113.42" },
},
randomUUID(),
);
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/evaluation-previews \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6e2b9d47-3a1c-4f85-b7e0-2d9c4a6f8b13" \
-d '{
"benefit": "pro_trial",
"claim_id": "preview_user_4821",
"subject": { "external_id": "user_4821", "email": "jordan@example.com" },
"context": { "ip": "203.0.113.42" }
}'
```
Replace the example IP address with the user's public address. `203.0.113.42` comes from a range reserved for documentation, and AbuseZero rejects documentation, private and other non-routable addresses with `IP_NOT_ROUTABLE`.
### Response [#response]
Returns `201`.
```json
{
"object": "evaluation_preview",
"preview": true,
"result": "deny",
"risk": { "score": null, "band": "not_assessed" },
"reason": {
"code": "prior_claim_user",
"message": "This user already received this benefit."
},
"inputs": {
"email": "received",
"ip": "received",
"user_id": "received",
"phone": "missing",
"payment": "missing",
"device": "missing",
"browser": "missing"
}
}
```
| Field | Type | Description |
|---|---|---|
| `object` | string | Always `evaluation_preview`. |
| `preview` | boolean | Always `true`. |
| `result` | string | `allow` or `deny`: the result a real evaluation would return now. |
| `risk` | object | The AbuseZero Risk Score. See [Evaluations](/docs/api-reference/evaluations#risk). |
| `reason` | object or `null` | Why the result was reached. See [Reason codes](/docs/reason-codes). |
| `inputs` | object | Which inputs the preview received. |
A preview can also return `{ "object": "evaluation_preview", "status": "skipped", "reason": … }` when the workspace can't run it. See [Skipped evaluations](/docs/api-reference/evaluations#skipped-evaluations).
### Errors [#errors]
| Status | Code | When |
|---|---|---|
| 422 | `IDEMPOTENCY_KEY_REQUIRED` | The `Idempotency-Key` header is missing or invalid. |
| 422 | `INVALID_FIELD` | A field is missing, invalid or not defined for this endpoint. `error.field` names it. |
| 422 | `INVALID_EMAIL` | `subject.email` isn't in a supported form. |
| 422 | `INVALID_IP` | `context.ip` isn't a valid IPv4 or IPv6 address. |
| 422 | `IP_NOT_ROUTABLE` | `context.ip` is a private, loopback, documentation or other non-routable address. Send the user's public address, or leave it out. |
| 422 | `UNSUPPORTED_FIELD` | A [custom identifier](/docs/concepts/custom-identifiers) and key version aren't registered. |
| 422 | `DEVICE_TOKEN_INVALID` | `identifiers.device_token` isn't a value the browser SDK returned. |
| 422 | `DEVICE_TOKEN_SCOPE` | The [device token](/docs/concepts/browser-protection) was collected for a different claim ID. |
| 422 | `BROWSER_SDK_UPGRADE_REQUIRED` | The device token came from an outdated browser SDK. |
| 422 | `BOT_EVIDENCE_REQUIRED` | The [benefit](/docs/concepts/benefits) requires a bot check and the preview has none that can be used. |
| 422 | `AGENT_EVIDENCE_SCOPE` | `agent_evidence` was produced for a different claim. |
| 422 | `AGENT_EVIDENCE_OUTDATED` | `agent_evidence` came from a Node.js SDK before 0.11.0. Update the SDK. |
| 404 | `BENEFIT_NOT_FOUND` | No benefit has this key. |
| 409 | `IDEMPOTENCY_CONFLICT` | The key was already used with a different body. |
| 409 | `EVALUATION_PENDING` | A preview with this key is still running. Retry with the same key. |
| 409 | `CLAIM_CONFLICT` | The claim ID belongs to a different user. |
| 409 | `DEVICE_TOKEN_USED` | The device token was already used for another claim. |
| 409 | `DELETION_IN_PROGRESS` | The user's history or the workspace is being deleted. |
| 409 | `POLICY_UNAVAILABLE` | The benefit's [policy](/docs/concepts/policies-and-rules) isn't available. |
| 413 | `PAYLOAD_TOO_LARGE` | The body is over 256 KiB. |
| 429 | `RATE_LIMITED` | The workspace is over a traffic limit. Wait for `Retry-After`. |
| 503 | `BOT_EVIDENCE_UNAVAILABLE` | The bot check couldn't be completed. Collect new browser data and retry. |
| 504 | `DEADLINE_EXCEEDED` | The preview reached its time limit. Retry with the same key. |
## Run a simulation [#run-a-simulation]
`POST /v1/simulations` · Scope [`simulations:run`](/docs/api-reference/authentication#scopes)
Also available as [`abusezero simulations run`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_simulate_sequence`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`simulate()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
Runs a [benefit](/docs/concepts/benefits)'s most recent [evaluations](/docs/concepts/evaluations) again, one after another, under the [policy](/docs/concepts/policies-and-rules) you choose, as if that policy had been active at the time. The sequence starts from an empty history, and each step sees the claims the simulation allowed before it, so a simulation shows how a policy handles repeat claims across a sequence.
Simulations use evaluations whose request details are still retained. They apply the chosen policy and the benefit's current settings, not your [allow and block list](/docs/concepts/allow-and-block-lists) entries. They use no credits and change nothing.
### Request body [#request-body-1]
| Field | Type | Required | Description |
|---|---|---|---|
| `benefit_id` | string | Yes | The benefit's ID, starting with `ben_`. |
| `policy_key` | string | Yes | The key of the policy to simulate: a [protection level](/docs/concepts/protection-levels) (`lenient`, `balanced` or `strict`) or one of your custom policies. Up to 64 characters. |
### Example [#example-1]
```ts tab="Node.js"
const simulation = await abusezero.simulate({
benefit_id: "ben_5b2e9c41-7d3a-4f68-a1c0-9e8d7b6a5f24",
policy_key: "strict",
});
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/simulations \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"benefit_id": "ben_5b2e9c41-7d3a-4f68-a1c0-9e8d7b6a5f24",
"policy_key": "strict"
}'
```
### Response [#response-1]
Returns `201`.
```json
{
"object": "simulation",
"data": [
{
"step": 1,
"evaluation": "eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93",
"result": "allow",
"reason": {
"code": "default_allow",
"message": "No rule matched, so the policy's default allowed this claim."
}
},
{
"step": 2,
"evaluation": "eval_8a1d4f20-6c3b-4e57-b9d2-0f7e6a5c3b18",
"result": "deny",
"reason": {
"code": "prior_claim_email",
"message": "An earlier claim used this email address."
}
}
]
}
```
| Field | Type | Description |
|---|---|---|
| `object` | string | Always `simulation`. |
| `data[].step` | integer | The step's position in the sequence, starting at 1. |
| `data[].evaluation` | string | The original evaluation the step re-ran. |
| `data[].result` | string | `allow` or `deny` under the simulated policy. |
| `data[].reason` | object or `null` | Why the simulated policy reached that result. |
The benefit's current protection mode applies. In observe mode, every result is `allow`.
### Errors [#errors-1]
| Status | Code | When |
|---|---|---|
| 404 | `POLICY_NOT_FOUND` | The benefit or the policy doesn't exist in this workspace. |
| 422 | `INVALID_FIELD` | `benefit_id` or `policy_key` is missing or invalid. |
## Run a replay [#run-a-replay]
`POST /v1/replays` · Scope [`simulations:run`](/docs/api-reference/authentication#scopes)
Also available as [`abusezero simulations replay`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_replay_evaluations`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`replay()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
Re-decides a [benefit](/docs/concepts/benefits)'s most recent [evaluations](/docs/concepts/evaluations) under the [policy](/docs/concepts/policies-and-rules) you choose, using what each evaluation recorded, and shows every result that would change. Save a change as a draft policy and replay it before you assign it to a benefit.
Replays make no new lookups. They apply the chosen policy and the benefit's current settings to each evaluation's retained record, not your [allow and block list](/docs/concepts/allow-and-block-lists) entries. They use no credits and change nothing.
### Request body [#request-body-2]
| Field | Type | Required | Description |
|---|---|---|---|
| `benefit_id` | string | Yes | The benefit's ID, starting with `ben_`. |
| `policy_key` | string | Yes | The key of the policy to replay: a [protection level](/docs/concepts/protection-levels) (`lenient`, `balanced` or `strict`) or one of your custom policies. Up to 64 characters. |
### Example [#example-2]
```ts tab="Node.js"
const replay = await abusezero.replay({
benefit_id: "ben_5b2e9c41-7d3a-4f68-a1c0-9e8d7b6a5f24",
policy_key: "trial_protection",
});
const rows = replay.data as { evaluation: string; changed: boolean }[];
const changed = rows.filter((row) => row.changed);
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/replays \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"benefit_id": "ben_5b2e9c41-7d3a-4f68-a1c0-9e8d7b6a5f24",
"policy_key": "trial_protection"
}'
```
### Response [#response-2]
Returns `201`.
```json
{
"object": "replay",
"limit": 500,
"data": [
{
"evaluation": "eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93",
"original": "allow",
"replayed": "allow",
"changed": false
},
{
"evaluation": "eval_d2b7f9a4-5e1c-4a38-9f6d-7c3e1b8a0d52",
"original": "allow",
"replayed": "deny",
"changed": true
}
]
}
```
| Field | Type | Description |
|---|---|---|
| `object` | string | Always `replay`. |
| `limit` | integer | The most evaluations one replay covers. The newest evaluations are replayed first. |
| `data[].evaluation` | string | The evaluation that was replayed. |
| `data[].original` | string | The result it received. |
| `data[].replayed` | string | The result under the chosen policy. |
| `data[].changed` | boolean | `true` when the two results differ. |
The benefit's current protection mode applies. In observe mode, every replayed result is `allow`.
### Errors [#errors-2]
| Status | Code | When |
|---|---|---|
| 404 | `POLICY_NOT_FOUND` | The benefit or the policy doesn't exist in this workspace. |
| 422 | `INVALID_FIELD` | `benefit_id` or `policy_key` is missing or invalid. |
## Next steps [#next-steps]
A step-by-step workflow for changing a policy without surprises.
Create and update the policies you test.
# Request logs (/docs/api-reference/request-logs)
Request logs record your [workspace's](/docs/keys-and-access#workspaces) [evaluation](/docs/api-reference/evaluations#create-an-evaluation) requests, successful and failed: when each was recorded, the HTTP status it returned and, for a failure, its public [error `code`](/docs/api-reference/errors#error-codes). Every AbuseZero REST API response carries an [`X-Request-ID`](/docs/api-reference#request-ids) header, and every error repeats it as `request_id`. The request log ID is that value with a `req_` prefix, for requests made over the REST API and through the [AbuseZero MCP server](/docs/tools/mcp), so a request your servers recorded can be looked up here directly.
Two kinds of request aren't logged: requests whose credential never authenticated, because they belong to no workspace, and [rate-limited](/docs/api-reference#rate-limits) requests (`429`). Keep the `request_id` of any request, and include it whenever you contact AbuseZero about it.
Examples on this page use a server client:
```ts
import { AbuseZero } from "abusezero/server";
const abusezero = new AbuseZero({ key: process.env.ABUSEZERO_SECRET_KEY!, outage: "throw" });
```
## The request log object [#the-request-log-object]
```json
{
"object": "request_log",
"id": "req_7d0e5c2a-1f4b-4e8a-b6c3-9a2d5e0f1b74",
"status": 200,
"code": null,
"created_at": "2026-10-29T14:02:19.000Z"
}
```
| Field | Type | Description |
|---|---|---|
| `object` | `"request_log"` | The object type. |
| `id` | string | The request log ID, starting with `req_`. |
| `status` | integer | The HTTP status AbuseZero returned. |
| `code` | string or `null` | The [error code](/docs/api-reference/errors#error-codes) returned, or `null` when the request succeeded. |
| `created_at` | string | When the request was recorded. |
## List request logs [#list-request-logs]
Returns request logs newest first.
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `GET` | `/v1/request-logs` | [`request_logs:read`](/docs/api-reference/authentication#scopes) | Not used |
Also available as [`abusezero request-logs list`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_list_request_logs`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`requestLogs()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
### Query parameters [#query-parameters]
| Parameter | Type | Required | Limits | Description |
|---|---|---|---|---|
| `limit` | integer | No | 1 to 100, default 50 | How many request logs to return. |
| `cursor` | string | No | | The `next_cursor` from the previous page, unchanged. |
### Example [#example]
```ts tab="Node.js"
const page = await abusezero.requestLogs({ limit: 100 });
```
```bash tab="cURL"
curl "https://api.abusezero.com/v1/request-logs?limit=100" \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
### Response [#response]
Returns `200`.
```json
{
"object": "list",
"data": [
{
"object": "request_log",
"id": "req_7d0e5c2a-1f4b-4e8a-b6c3-9a2d5e0f1b74",
"status": 200,
"code": null,
"created_at": "2026-10-29T14:02:19.000Z"
},
{
"object": "request_log",
"id": "req_4b2d6f8a-0c1e-4a3b-9d5f-7e8a1c3b5d02",
"status": 404,
"code": "BENEFIT_NOT_FOUND",
"created_at": "2026-10-29T14:01:47.000Z"
}
],
"next_cursor": "…"
}
```
When `next_cursor` is a string, pass it as `cursor` to read the next page. See [Pagination](/docs/api-reference#pagination).
## Retrieve a request log [#retrieve-a-request-log]
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `GET` | `/v1/request-logs/{id}` | [`request_logs:read`](/docs/api-reference/authentication#scopes) | Not used |
Also available as [`abusezero request-logs get`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_get_request_log`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`call()`](/docs/tools/server-sdk#call-for-any-endpoint) in the AbuseZero Node.js SDK.
### Path parameters [#path-parameters]
| Parameter | Type | Description |
|---|---|---|
| `id` | string | The request log ID: `req_` followed by the request's `X-Request-ID`. |
### Example [#example-1]
```ts tab="Node.js"
const log = await abusezero.call("/v1/request-logs/req_7d0e5c2a-1f4b-4e8a-b6c3-9a2d5e0f1b74");
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/request-logs/req_7d0e5c2a-1f4b-4e8a-b6c3-9a2d5e0f1b74 \
-H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"
```
### Response [#response-1]
Returns `200` with a [request log object](#the-request-log-object).
### Errors [#errors]
| Code | Status | Meaning |
|---|---|---|
| [`RESOURCE_NOT_FOUND`](/docs/api-reference/errors#not-found) | 404 | No request log with this ID exists in the workspace. |
For every other code, see [REST API errors](/docs/api-reference/errors).
## Related [#related]
Watch your integration's health from your own monitoring.
Find and fix common integration problems.
# Workspaces (/docs/api-reference/workspaces)
A workspace holds your [benefits](/docs/concepts/benefits), [policies](/docs/concepts/policies-and-rules), [allow and block lists](/docs/concepts/allow-and-block-lists), claim history and [credentials](/docs/keys-and-access#credential-types). Each workspace's data stays in that workspace. Keys, OAuth tokens and [AbuseZero MCP server](/docs/tools/mcp) sessions reach only the workspaces they were granted.
Workspace IDs start with `ws_`.
## How requests choose a workspace [#how-requests-choose-a-workspace]
- **API keys** belong to one workspace. See [secret and restricted keys](/docs/keys-and-access#secret-and-restricted-keys). Every request made with a key acts on that workspace.
- **OAuth tokens** act for the signed-in person and reach only the workspaces they were granted. Every request made with a token, except [listing workspaces](#list-workspaces), names its workspace in the [`AbuseZero-Workspace` header](/docs/api-reference/authentication#choose-a-workspace).
```bash
curl https://api.abusezero.com/v1/benefits \
-H "Authorization: Bearer $ABUSEZERO_ACCESS_TOKEN" \
-H "AbuseZero-Workspace: ws_3a9c1e5b-7d2f-4b8a-a6e0-2c4f8b1d9e57"
```
The [AbuseZero Node.js SDK](/docs/tools/server-sdk#create-a-nodejs-sdk-client) sends the header for you when you pass `workspace`:
```ts
import { AbuseZero } from "abusezero/server";
const abusezero = new AbuseZero({
accessToken: process.env.ABUSEZERO_ACCESS_TOKEN!,
workspace: "ws_3a9c1e5b-7d2f-4b8a-a6e0-2c4f8b1d9e57",
outage: "throw",
});
```
The AbuseZero CLI chooses its workspace with `abusezero workspaces use ws_…`. See [AbuseZero CLI](/docs/tools/cli#choose-a-workspace).
## The workspace object [#the-workspace-object]
```json
{
"object": "workspace",
"id": "ws_3a9c1e5b-7d2f-4b8a-a6e0-2c4f8b1d9e57",
"name": "Acme Analytics"
}
```
| Field | Type | Description |
|---|---|---|
| `object` | `"workspace"` | The object type. |
| `id` | string | The workspace ID, starting with `ws_`. |
| `name` | string or `null` | The workspace's name. |
## List workspaces [#list-workspaces]
With an API key, returns the key's own workspace. With an OAuth token granted for one workspace, returns only that workspace. With any other OAuth token, returns every workspace the signed-in person belongs to. This request doesn't need the `AbuseZero-Workspace` header.
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
| `GET` | `/v1/workspaces` | [`workspaces:read`](/docs/api-reference/authentication#scopes) | Not used |
Also available as [`abusezero workspaces list`](/docs/tools/cli#cli-command-reference) in the AbuseZero CLI, [`abusezero_list_workspaces`](/docs/tools/mcp#mcp-tools) on the AbuseZero MCP server and [`workspaces()`](/docs/tools/server-sdk#additional-methods) in the AbuseZero Node.js SDK.
### Example [#example]
```ts tab="Node.js"
const workspaces = await abusezero.workspaces();
```
```bash tab="cURL"
curl https://api.abusezero.com/v1/workspaces \
-H "Authorization: Bearer $ABUSEZERO_ACCESS_TOKEN"
```
### Response [#response]
Returns `200`. Every workspace comes back in one response, so `next_cursor` is always `null`.
```json
{
"object": "list",
"data": [
{
"object": "workspace",
"id": "ws_3a9c1e5b-7d2f-4b8a-a6e0-2c4f8b1d9e57",
"name": "Acme Analytics"
},
{
"object": "workspace",
"id": "ws_9e1b5d3f-2a7c-4e6b-8f0d-4a2c6e8b0d13",
"name": "Acme Labs"
}
],
"next_cursor": null
}
```
## Workspace errors [#workspace-errors]
These errors can come back from any request made with an OAuth token.
| Code | Status | Meaning |
|---|---|---|
| [`WORKSPACE_REQUIRED`](/docs/api-reference/errors#permission) | 403 | The request didn't name a workspace, or the `AbuseZero-Workspace` value isn't a `ws_` ID. Choose a workspace and retry. |
| [`WORKSPACE_ACCESS_DENIED`](/docs/api-reference/errors#permission) | 403 | The token was granted for a different workspace, or the signed-in person no longer has access to this one. Choose another workspace, or ask its owner for access. |
For every other code, see [REST API errors](/docs/api-reference/errors).
## Related [#related]
Keys, OAuth tokens and scopes.
Choose the right credential for each part of your integration.
# AbuseZero browser SDK (/docs/tools/browser-sdk)
The AbuseZero browser SDK, `abusezero/browser`, connects your pages to AbuseZero's device and automation intelligence, as described in [Browser protection](/docs/concepts/browser-protection). On each claim it gathers browser evidence in the background, without blocking your page. It returns one value, the device token, which your server passes on with the evaluation. The SDK always resolves. When there's no browser data, such as when a visitor declines consent or a network blocks the request, the device token carries the reason instead. The evaluation still runs and reports exactly what it received.
```ts
import { init, observe, prepareBotObservation, forgetBrowser } from "abusezero/browser";
```
Use your [publishable key](/docs/keys-and-access#publishable-key) (`pk_…`) in the browser. It can only send browser data from the sites in your [site list](/docs/api-reference/browser-collection#update-the-site-list), so it is safe to ship in your pages. Keep [secret keys](/docs/keys-and-access#secret-and-restricted-keys) and the [AbuseZero Node.js SDK](/docs/tools/server-sdk) out of your browser bundle. For a step-by-step setup, see [Set up browser protection](/docs/guides/browser-setup).
## How the browser SDK works [#how-the-browser-sdk-works]
```mermaid
flowchart LR
A[Your page] -->|observe| B[AbuseZero]
B -->|device token| A
A -->|device token| C[Your server]
C -->|evaluation with device_token| B
B -->|allow or deny| C
```
1. Your page calls `observe()` or `prepareBotObservation()` with the claim's ID.
2. It sends the returned `deviceToken` to your server.
3. Your server sends the token in the [evaluation](/docs/api-reference/evaluations#create-an-evaluation) as `identifiers.device_token`.
Use the same claim ID in the browser and on your server. The browser's `claimId` is the evaluation's [`claim_id`](/docs/concepts/evaluations).
## `init` [#init]
Call `init` once when your app starts.
```ts
init({ key: "pk_…" });
```
It reports once per page load that browser protection is installed on this site. It sends only the site's address and the SDK version, never visitor data. Developers → Browser setup, also available through [`GET /v1/browser-collection`](/docs/api-reference/browser-collection#retrieve-browser-setup), shows each site where it was seen and which version it runs.
| Option | Type | Required | Description |
|---|---|---|---|
| `key` | string | Yes | Your publishable key. |
## `observe` [#observe]
Call `observe` when the person submits the claim, then send the token to your server with it.
```ts
const observation = await observe({ key: "pk_…", claimId });
await fetch("/api/start-trial", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ claimId, deviceToken: observation.deviceToken }),
});
```
| Option | Type | Required | Description |
|---|---|---|---|
| `key` | string | Yes | Your publishable key. |
| `claimId` | string | Yes | The claim's ID, sent by your server as the evaluation's `claim_id`. |
| `consent` | `() => boolean \| Promise` | No | Collection runs unless this resolves to `false`. See [Consent](#consent). |
### The observation [#the-observation]
`observe` never throws or rejects. It resolves to one of two shapes, and both carry `deviceToken`.
| Field | Type | Description |
|---|---|---|
| `status` | `"available"` or `"unavailable"` | Whether browser data was collected. |
| `deviceToken` | string | Send this to your server and on to the evaluation as `identifiers.device_token`, whatever the status. The evaluation reports what it received as [`browser_data`](/docs/api-reference/evaluations#browser-data). |
| `token` | string | With `available`. The same value as `deviceToken`. |
| `expiresIn` | number | With `available`. Seconds until the token expires. Send it in an evaluation before then. |
| `reason` | string | With `unavailable`. Why there is no browser data. See [Unavailable reasons](#unavailable-reasons). |
Always send `deviceToken`, including when the status is `unavailable`. It tells AbuseZero why there is no
browser data, and the evaluation reports it as the `browser_data` status.
## `prepareBotObservation` [#preparebotobservation]
Add a bot check by calling `prepareBotObservation` instead of `observe` when the person submits the form. A benefit's [automation settings](/docs/api-reference/benefits#automation) decide whether the bot check is required; see [Bots and AI agents](/docs/concepts/bots-and-ai-agents). It collects the same browser data and adds the bot check, with the same options and result.
```ts
form.addEventListener("submit", async event => {
event.preventDefault();
const { deviceToken } = await prepareBotObservation({ key: "pk_…", claimId });
await fetch("/api/start-trial", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ claimId, deviceToken }),
});
});
```
Send its `deviceToken` in an evaluation within 2 minutes. AbuseZero verifies the bot check during that evaluation. When the token is too old, or a benefit that requires the bot check receives none, the evaluation returns a bot-check error, such as [`BOT_EVIDENCE_REQUIRED`](/docs/api-reference/errors#invalid-request). See [Add a bot check](/docs/guides/bot-check). Collect a fresh token and evaluate again. See [Add a bot check](/docs/guides/bot-check).
## Consent [#consent]
[Browser protection](/docs/concepts/browser-protection) runs by default. Pass a `consent` callback to make it depend on your consent tool. See [Respect consent](/docs/guides/browser-setup#respect-consent).
```ts
const observation = await observe({
key: "pk_…",
claimId,
consent: () => consentManager.hasConsent("security"), // Your consent tool.
});
```
- When `consent` resolves to `false`, nothing is collected and `deviceToken` is `unavailable:consent_declined`.
- When your consent tool keeps the SDK from loading at all, send `unavailable:not_loaded` as the device token.
- When a visitor withdraws consent, call `forgetBrowser()`. It clears what browser protection stored in this browser. Claim history already recorded in your workspace stays.
## Unavailable reasons [#unavailable-reasons]
When there is no browser data, `reason` and the device token say why. The same values appear as the evaluation's [browser data status](/docs/concepts/browser-protection#browser-data-statuses), and [Troubleshooting](/docs/troubleshooting) covers each one. The token has the form `unavailable:`.
| Reason | Meaning | What to do |
|---|---|---|
| `consent_declined` | Your `consent` callback resolved to `false`. | Nothing. This is expected. |
| `not_loaded` | Your consent tool kept the SDK from loading. You send this one yourself. | Nothing. This is expected. |
| `blocked` | The browser couldn't reach AbuseZero, for example because of an extension or a network rule. | Nothing. This is expected. |
| `timed_out` | Collection didn't finish in time. | Nothing. This is expected. |
| `failed` | The publishable key is invalid, or the response couldn't be read. | Check that you use a publishable key (`pk_…`) from this workspace. |
| `site_not_listed` | The page's site isn't in your site list, or doesn't use HTTPS. | Add the site in Developers → Browser setup. |
| `sdk_outdated` | AbuseZero no longer accepts this SDK version. | Upgrade `abusezero`, or update the standalone script's address. |
Developers → Browser setup shows how often each status appears across recent evaluations, and flags the ones you can fix as [issues](/docs/api-reference/browser-collection#issues).
## Your sites [#your-sites]
AbuseZero accepts browser data only from HTTPS sites in your site list. Manage the list in Developers → Browser setup or through the [Browser setup API](/docs/api-reference/browser-collection).
If your site sets a Content Security Policy, copy the sources shown in Developers → Browser setup into it, as in [Set up browser protection](/docs/guides/browser-setup#allow-the-sdk-in-your-content-security-policy).
## Standalone browser script [#standalone-browser-script]
For pages without a bundler, load the standalone script with your publishable key in its address. It runs `init` by itself.
```html
```
To collect for a claim, import from the same address in a module script:
```html
```
The address includes the version. When you upgrade, update every page to the new address.
## Next steps [#next-steps]
Add the SDK to your pages, list your sites and confirm data arrives.
What browser protection adds to every evaluation.
# AbuseZero CLI (/docs/tools/cli)
The AbuseZero CLI, the `abusezero` command, gives you every action that AbuseZero offers to AI apps, from a terminal. It signs in with [OAuth](/docs/keys-and-access#oauth) and works in the [workspace](/docs/keys-and-access#workspaces) you choose. Every command uses the same envelopes, permission checks and [idempotency](/docs/api-reference#idempotency) as the [AbuseZero REST API](/docs/api-reference), and every command has a matching tool on the [AbuseZero MCP server](/docs/tools/mcp). Output is JSON by default, so scripts can read it directly. Exit codes are stable, so CI can branch on them.
## Install the AbuseZero CLI [#install-the-abusezero-cli]
Run any command once with `npx`, or install the command globally. Both need Node.js 22 or later.
```bash
npx abusezero --help
```
```npm
npm install -g abusezero
```
## Sign in to the CLI [#sign-in-to-the-cli]
```bash
abusezero login
```
`login` opens your browser to sign in to AbuseZero. You approve the [scopes](/docs/api-reference/authentication#scopes) the CLI asks for. Your role in each workspace still limits what each command can do, as described in [member permissions for OAuth](/docs/api-reference/authentication#member-permissions-for-oauth).
| Command | Description |
|---|---|
| `abusezero login` | Sign in with your browser. |
| `abusezero login --device` | Sign in on a machine without a browser. Enter the code it shows on another device. |
| `abusezero login --machine-client` | Check the machine client set in `ABUSEZERO_CLIENT_ID` and `ABUSEZERO_CLIENT_SECRET`. Nothing is saved. |
| `abusezero whoami` | Show the sign-in and workspace in use. |
| `abusezero logout` | Revoke the sign-in and delete it from this computer. |
Sign-ins are stored only on your computer, in a file only your account can read. Set `ABUSEZERO_CONFIG_DIR` to store them somewhere else.
### Use the CLI in scripts and CI [#use-the-cli-in-scripts-and-ci]
For scripts and CI, create a [machine client](/docs/keys-and-access#machine-clients) in **Developers → Connected apps** and set its credentials as environment variables. Every command uses the machine client while they are set, and nothing is written to disk. A machine client belongs to one workspace.
```bash
export ABUSEZERO_CLIENT_ID="…"
export ABUSEZERO_CLIENT_SECRET="…"
abusezero login --machine-client
```
## Choose a workspace [#choose-a-workspace]
A sign-in can reach several [workspaces](/docs/api-reference/workspaces). Choose the one later commands use:
```bash
abusezero workspaces list --output table
abusezero workspaces use ws_8a1f3c6e-2d4b-4e9a-b7c5-0f3d6a9e2b81
```
Machine clients already belong to one workspace, so they skip this step.
## Run CLI commands [#run-cli-commands]
Commands take the form `abusezero [id] [options]`.
```bash
abusezero evaluations list --result deny --days 7 --output table
abusezero evaluations get eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93
abusezero access-lists add --kind allow --type email --value jordan@example.com --reason "Verified customer"
abusezero policies create --input policy.json
```
- **IDs** come after the command, in the order the help shows.
- **Fields** are options in kebab case. For example, the `expires_in_days` field is `--expires-in-days 30`.
- **Booleans** are `--include-evidence` or `--no-include-evidence`.
- **Lists** repeat the option, or take a JSON array.
- **Objects** take JSON, such as `--subject '{"email":"jordan@example.com"}'`.
- **Whole requests** come from a file with `--input file.json`, or from standard input with `--input -`. Options you also pass override the file's fields.
Run `abusezero --help`, `abusezero --help` or `abusezero --help` to see the commands, options and permission each one needs.
### CLI output [#cli-output]
Output is JSON by default. Add `--output table` for a readable table of the main fields. When a list has more results, the table ends with the `--cursor` option that fetches the next page, as in [REST API pagination](/docs/api-reference#pagination).
### Idempotency keys [#idempotency-keys]
Every command that changes data prints its [idempotency key](/docs/api-reference#idempotency) to standard error. If the command fails or you aren't sure it finished, run it again with the same key:
```bash
abusezero benefits create --input benefit.json --idempotency-key 0b7e4f2a-9c3d-4a1e-8f6b-2d5c7e9a1b30
```
Permanent actions, such as deleting a benefit or cancelling an import, ask you to confirm by typing the ID, or `delete` when the command has no ID. In scripts, add `--yes`. Without it, a permanent action fails in a non-interactive run.
### Global CLI options [#global-cli-options]
| Option | Description |
|---|---|
| `--output ` | Output format. Defaults to `json`. |
| `--input ` | Read the command's input as a JSON object from a file or standard input. |
| `--idempotency-key ` | Reuse a key to retry a change safely. |
| `--yes` | Confirm a permanent action without asking. |
| `--help` | Show help for the CLI, a command group or a command. |
| `--version` | Show the installed version. |
### CLI exit codes [#cli-exit-codes]
| Code | Meaning |
|---|---|
| `0` | Success. |
| `1` | Failure. |
| `2` | Invalid input, such as an unknown option or a missing ID. |
| `3` | Sign-in needed. |
| `4` | Permission denied. |
| `5` | Not found. |
| `6` | Conflict. |
| `7` | Temporarily unavailable. Retry with the same idempotency key. |
Errors print to standard error with the [error code](/docs/api-reference/errors#error-codes) and [request ID](/docs/api-reference#request-ids), for example `Error: Clear the page cursor and try again. (CURSOR_INVALID, request 7d0e5c2a-1f4b-4e8a-b6c3-9a2d5e0f1b74)`.
## CLI environment variables [#cli-environment-variables]
| Variable | Description |
|---|---|
| `ABUSEZERO_CLIENT_ID` | The machine client's ID. Set it with `ABUSEZERO_CLIENT_SECRET`. |
| `ABUSEZERO_CLIENT_SECRET` | The machine client's secret. |
| `ABUSEZERO_CONFIG_DIR` | Where sign-ins are stored. |
| `ABUSEZERO_API_KEY` | Used only by `abusezero mcp`. A server key the local MCP server uses instead of your sign-in. |
## CLI command reference [#cli-command-reference]
Every command links to the [AbuseZero REST API](/docs/api-reference) endpoint it calls. The concepts behind each group are in [Evaluations](/docs/concepts/evaluations), [Outcomes](/docs/concepts/outcomes), [Benefits](/docs/concepts/benefits), [Policies and rules](/docs/concepts/policies-and-rules), [Allow and block lists](/docs/concepts/allow-and-block-lists), [Custom identifiers](/docs/concepts/custom-identifiers), [Browser protection](/docs/concepts/browser-protection) and [Bots and AI agents](/docs/concepts/bots-and-ai-agents). Imports, exports and data deletions are walked through in [Import outcome history](/docs/guides/import-claim-history) and [Export and delete data](/docs/guides/export-and-delete-data). The Permission column is the command's [scope](/docs/api-reference/authentication#scopes).
| Command | What it does | Permission |
|---|---|---|
| [`workspaces list`](/docs/api-reference/workspaces#list-workspaces) | List the workspaces this sign-in can use. | `workspaces:read` |
| `workspaces use ` | Choose the workspace for later commands. | `workspaces:read` |
| [`evaluations create`](/docs/api-reference/evaluations#create-an-evaluation) | Evaluate a claim. | `evaluations:create` |
| [`evaluations preview`](/docs/api-reference/previews-simulations-replays#preview-an-evaluation) | Preview how AbuseZero would evaluate a claim. | `simulations:run` |
| [`evaluations list`](/docs/api-reference/evaluations#list-evaluations) | List evaluations, filtered by result, benefit or time. | `evaluations:read` |
| [`evaluations get `](/docs/api-reference/evaluations#retrieve-an-evaluation) | Read an evaluation. | `evaluations:read` |
| [`evaluations graph `](/docs/api-reference/evaluations#retrieve-an-evaluations-association-graph) | Read the earlier claims linked to an evaluation. | `evaluations:read` |
| [`evaluations evidence `](/docs/api-reference/evaluations#retrieve-an-evaluations-evidence) | Read the evidence retained for an evaluation. | `evaluation_evidence:read` |
| [`outcomes report `](/docs/api-reference/outcomes#report-an-outcome) | Report what happened after an evaluation. `` is the evaluation's ID. | `outcomes:write` |
| [`outcomes list`](/docs/api-reference/outcomes#list-outcomes) | List outcomes. | `outcomes:read` |
| [`outcomes get `](/docs/api-reference/outcomes#retrieve-an-outcome) | Read an outcome. | `outcomes:read` |
| [`simulations run`](/docs/api-reference/previews-simulations-replays#run-a-simulation) | Run a benefit's recent claims through a policy, in order, as one sequence. | `simulations:run` |
| [`simulations replay`](/docs/api-reference/previews-simulations-replays#run-a-replay) | Compare recent evaluations with how a saved policy would decide them. | `simulations:run` |
| [`benefits list`](/docs/api-reference/benefits#list-benefits) | List benefits. | `benefits:read` |
| [`benefits create`](/docs/api-reference/benefits#create-a-benefit) | Create a benefit. | `benefits:write` |
| [`benefits get `](/docs/api-reference/benefits#retrieve-a-benefit) | Read a benefit. | `benefits:read` |
| [`benefits update `](/docs/api-reference/benefits#update-a-benefit) | Update a benefit. | `benefits:write` |
| [`benefits delete `](/docs/api-reference/benefits#delete-a-benefit) | Delete a benefit. Permanent. | `benefits:write` |
| [`policies list`](/docs/api-reference/policies#list-policies) | List policies. | `policies:read` |
| [`policies create`](/docs/api-reference/policies#create-a-policy) | Create a custom policy. | `policies:write` |
| [`policies get `](/docs/api-reference/policies#retrieve-a-policy) | Read a policy. | `policies:read` |
| [`policies update `](/docs/api-reference/policies#update-a-policy) | Save a new version of a policy. | `policies:write` |
| [`policies duplicate `](/docs/api-reference/policies#duplicate-a-policy) | Copy a policy. | `policies:write` |
| [`policies delete `](/docs/api-reference/policies#delete-a-policy) | Delete a policy. Permanent. | `policies:write` |
| [`policies versions `](/docs/api-reference/policies#list-policy-versions) | List a policy's versions. | `policies:read` |
| [`policies version `](/docs/api-reference/policies#retrieve-a-policy-version) | Read one version of a policy. | `policies:read` |
| [`policies signals`](/docs/api-reference/policies#list-conditions) | List the conditions custom rules can use. | `policies:read` |
| [`access-lists list`](/docs/api-reference/access-lists#list-entries) | List allow and block list entries. | `access_lists:read` |
| [`access-lists add`](/docs/api-reference/access-lists#add-an-entry) | Add one or more values to an allow or block list. | `access_lists:write` |
| [`access-lists get `](/docs/api-reference/access-lists#retrieve-an-entry) | Read a list entry. | `access_lists:read` |
| [`access-lists delete `](/docs/api-reference/access-lists#remove-an-entry) | Remove a list entry. Permanent. | `access_lists:write` |
| [`custom-identifiers list`](/docs/api-reference/custom-identifiers#list-custom-identifiers) | List custom identifiers. | `custom_identifiers:read` |
| [`custom-identifiers register`](/docs/api-reference/custom-identifiers#register-a-custom-identifier) | Register a custom identifier. | `custom_identifiers:write` |
| [`custom-identifiers get `](/docs/api-reference/custom-identifiers#retrieve-a-custom-identifier) | Read a custom identifier. | `custom_identifiers:read` |
| [`custom-identifiers delete `](/docs/api-reference/custom-identifiers#remove-a-custom-identifier) | Delete a custom identifier. Permanent. | `custom_identifiers:write` |
| [`imports list`](/docs/api-reference/imports#list-imports) | List imports. | `imports:read` |
| [`imports create`](/docs/api-reference/imports#import-outcomes-as-json) | Import granted outcomes as JSON. | `imports:write` |
| [`imports create-csv`](/docs/api-reference/imports#create-an-upload) | Start a CSV import and get its upload address. | `imports:write` |
| [`imports get `](/docs/api-reference/imports#retrieve-an-import) | Read an import's progress. | `imports:read` |
| [`imports errors `](/docs/api-reference/imports#download-the-error-report) | Get the error report for an import. | `imports:read` |
| [`imports validate `](/docs/api-reference/imports#validate-the-import) | Validate an uploaded import. | `imports:write` |
| [`imports confirm `](/docs/api-reference/imports#confirm-the-import) | Confirm a validated import. | `imports:write` |
| [`imports cancel `](/docs/api-reference/imports#cancel-an-import) | Cancel an import and delete its uploaded file and unfinished rows. Permanent. | `imports:write` |
| [`exports list`](/docs/api-reference/exports#list-exports) | List exports. | `exports:read` |
| [`exports create`](/docs/api-reference/exports#request-an-export) | Request an export. | `exports:create` |
| [`exports get `](/docs/api-reference/exports#retrieve-an-export) | Read an export's status. | `exports:read` |
| [`exports download `](/docs/api-reference/exports#download-an-export) | Get a download address for a finished export. | `exports:read` |
| [`data-deletions preview`](/docs/api-reference/data-deletions#preview-a-deletion) | Preview what a data deletion would remove. | `data_deletions:write` |
| [`data-deletions create`](/docs/api-reference/data-deletions#request-a-deletion) | Request a data deletion. Permanent. | `data_deletions:write` |
| [`data-deletions list`](/docs/api-reference/data-deletions#list-deletions) | List data deletions. | `data_deletions:read` |
| [`data-deletions get `](/docs/api-reference/data-deletions#retrieve-a-deletion) | Read a data deletion's status. | `data_deletions:read` |
| [`analytics get`](/docs/api-reference/analytics#read-analytics) | Read daily evaluation totals. | `analytics:read` |
| [`request-logs list`](/docs/api-reference/request-logs#list-request-logs) | List evaluation requests, including failed ones. | `request_logs:read` |
| [`request-logs get `](/docs/api-reference/request-logs#retrieve-a-request-log) | Read a request log. | `request_logs:read` |
| [`audit-logs list`](/docs/api-reference/audit-logs#list-audit-events) | List audit events. | `audit_logs:read` |
| [`audit-logs get `](/docs/api-reference/audit-logs#retrieve-an-audit-event) | Read an audit event. | `audit_logs:read` |
| [`browser-collection get`](/docs/api-reference/browser-collection#retrieve-browser-setup) | Read browser setup: sites, issues and recent coverage. | `browser_collection:read` |
| [`browser-collection update`](/docs/api-reference/browser-collection#update-the-site-list) | Update your site list. | `browser_collection:write` |
| [`agents list`](/docs/api-reference/agents#list-recognized-agents) | List the AI agents AbuseZero recognizes. | `agents:read` |
Each command's fields are the request fields of the REST API endpoint it links to. See [REST API, MCP server and CLI coverage](/docs/tools/coverage) for the matching MCP tool of every command.
## Run a local MCP server from the CLI [#run-a-local-mcp-server-from-the-cli]
`abusezero mcp` runs a local MCP server for AI apps that start MCP servers on your computer. See [Local MCP server](/docs/tools/mcp#local-mcp-server).
# REST API, MCP server and CLI coverage (/docs/tools/coverage)
The [AbuseZero REST API](/docs/api-reference), the [AbuseZero MCP server](/docs/tools/mcp) and the [AbuseZero CLI](/docs/tools/cli) are three ways to reach the same AbuseZero actions, so an action takes the same input and returns the same result wherever you call it. The [AbuseZero Node.js SDK](/docs/tools/server-sdk) covers every REST API endpoint, with a typed method or [`call()`](/docs/tools/server-sdk#call-for-any-endpoint).
## Actions [#actions]
Each REST API endpoint links to its reference section, each MCP tool to the [MCP tools](/docs/tools/mcp#mcp-tools) list and each command to the [CLI command reference](/docs/tools/cli#cli-command-reference). Every action needs the same [scope](/docs/api-reference/authentication#scopes) on every surface.
| Action | REST endpoint | MCP tool | Command |
|---|---|---|---|
| List workspaces | [`GET /v1/workspaces`](/docs/api-reference/workspaces#list-workspaces) | [`abusezero_list_workspaces`](/docs/tools/mcp#mcp-tools) | [`workspaces list`](/docs/tools/cli#cli-command-reference) |
| Evaluate a claim | [`POST /v1/evaluations`](/docs/api-reference/evaluations#create-an-evaluation) | [`abusezero_create_evaluation`](/docs/tools/mcp#mcp-tools) | [`evaluations create`](/docs/tools/cli#cli-command-reference) |
| Preview an evaluation | [`POST /v1/evaluation-previews`](/docs/api-reference/previews-simulations-replays#preview-an-evaluation) | [`abusezero_preview_evaluation`](/docs/tools/mcp#mcp-tools) | [`evaluations preview`](/docs/tools/cli#cli-command-reference) |
| List evaluations | [`GET /v1/evaluations`](/docs/api-reference/evaluations#list-evaluations) | [`abusezero_list_evaluations`](/docs/tools/mcp#mcp-tools) | [`evaluations list`](/docs/tools/cli#cli-command-reference) |
| Read an evaluation | [`GET /v1/evaluations/{id}`](/docs/api-reference/evaluations#retrieve-an-evaluation) | [`abusezero_get_evaluation`](/docs/tools/mcp#mcp-tools) | [`evaluations get`](/docs/tools/cli#cli-command-reference) |
| Read an evaluation's association graph | [`GET /v1/evaluations/{id}/graph`](/docs/api-reference/evaluations#retrieve-an-evaluations-association-graph) | [`abusezero_get_evaluation_graph`](/docs/tools/mcp#mcp-tools) | [`evaluations graph`](/docs/tools/cli#cli-command-reference) |
| Read an evaluation's retained evidence | [`GET /v1/evaluations/{id}/evidence`](/docs/api-reference/evaluations#retrieve-an-evaluations-evidence) | [`abusezero_get_evaluation_evidence`](/docs/tools/mcp#mcp-tools) | [`evaluations evidence`](/docs/tools/cli#cli-command-reference) |
| Report an outcome | [`POST /v1/evaluations/{id}/outcome`](/docs/api-reference/outcomes#report-an-outcome) | [`abusezero_report_outcome`](/docs/tools/mcp#mcp-tools) | [`outcomes report`](/docs/tools/cli#cli-command-reference) |
| List outcomes | [`GET /v1/outcomes`](/docs/api-reference/outcomes#list-outcomes) | [`abusezero_list_outcomes`](/docs/tools/mcp#mcp-tools) | [`outcomes list`](/docs/tools/cli#cli-command-reference) |
| Read an outcome | [`GET /v1/outcomes/{id}`](/docs/api-reference/outcomes#retrieve-an-outcome) | [`abusezero_get_outcome`](/docs/tools/mcp#mcp-tools) | [`outcomes get`](/docs/tools/cli#cli-command-reference) |
| Run a simulation | [`POST /v1/simulations`](/docs/api-reference/previews-simulations-replays#run-a-simulation) | [`abusezero_simulate_sequence`](/docs/tools/mcp#mcp-tools) | [`simulations run`](/docs/tools/cli#cli-command-reference) |
| Replay evaluations | [`POST /v1/replays`](/docs/api-reference/previews-simulations-replays#run-a-replay) | [`abusezero_replay_evaluations`](/docs/tools/mcp#mcp-tools) | [`simulations replay`](/docs/tools/cli#cli-command-reference) |
| List benefits | [`GET /v1/benefits`](/docs/api-reference/benefits#list-benefits) | [`abusezero_list_benefits`](/docs/tools/mcp#mcp-tools) | [`benefits list`](/docs/tools/cli#cli-command-reference) |
| Create a benefit | [`POST /v1/benefits`](/docs/api-reference/benefits#create-a-benefit) | [`abusezero_create_benefit`](/docs/tools/mcp#mcp-tools) | [`benefits create`](/docs/tools/cli#cli-command-reference) |
| Read a benefit | [`GET /v1/benefits/{id}`](/docs/api-reference/benefits#retrieve-a-benefit) | [`abusezero_get_benefit`](/docs/tools/mcp#mcp-tools) | [`benefits get`](/docs/tools/cli#cli-command-reference) |
| Update a benefit | [`PATCH /v1/benefits/{id}`](/docs/api-reference/benefits#update-a-benefit) | [`abusezero_update_benefit`](/docs/tools/mcp#mcp-tools) | [`benefits update`](/docs/tools/cli#cli-command-reference) |
| Delete a benefit | [`DELETE /v1/benefits/{id}`](/docs/api-reference/benefits#delete-a-benefit) | [`abusezero_delete_benefit`](/docs/tools/mcp#mcp-tools) | [`benefits delete`](/docs/tools/cli#cli-command-reference) |
| List policies | [`GET /v1/policies`](/docs/api-reference/policies#list-policies) | [`abusezero_list_policies`](/docs/tools/mcp#mcp-tools) | [`policies list`](/docs/tools/cli#cli-command-reference) |
| Create a policy | [`POST /v1/policies`](/docs/api-reference/policies#create-a-policy) | [`abusezero_create_policy`](/docs/tools/mcp#mcp-tools) | [`policies create`](/docs/tools/cli#cli-command-reference) |
| Read a policy | [`GET /v1/policies/{id}`](/docs/api-reference/policies#retrieve-a-policy) | [`abusezero_get_policy`](/docs/tools/mcp#mcp-tools) | [`policies get`](/docs/tools/cli#cli-command-reference) |
| Update a policy | [`PATCH /v1/policies/{id}`](/docs/api-reference/policies#update-a-policy) | [`abusezero_update_policy`](/docs/tools/mcp#mcp-tools) | [`policies update`](/docs/tools/cli#cli-command-reference) |
| Duplicate a policy | [`POST /v1/policies/{id}/duplicate`](/docs/api-reference/policies#duplicate-a-policy) | [`abusezero_duplicate_policy`](/docs/tools/mcp#mcp-tools) | [`policies duplicate`](/docs/tools/cli#cli-command-reference) |
| Delete a policy | [`DELETE /v1/policies/{id}`](/docs/api-reference/policies#delete-a-policy) | [`abusezero_delete_policy`](/docs/tools/mcp#mcp-tools) | [`policies delete`](/docs/tools/cli#cli-command-reference) |
| List a policy's versions | [`GET /v1/policies/{id}/versions`](/docs/api-reference/policies#list-policy-versions) | [`abusezero_list_policy_versions`](/docs/tools/mcp#mcp-tools) | [`policies versions`](/docs/tools/cli#cli-command-reference) |
| Read a policy version | [`GET /v1/policies/{id}/versions/{version}`](/docs/api-reference/policies#retrieve-a-policy-version) | [`abusezero_get_policy_version`](/docs/tools/mcp#mcp-tools) | [`policies version`](/docs/tools/cli#cli-command-reference) |
| List rule conditions | [`GET /v1/policy-signals`](/docs/api-reference/policies#list-conditions) | [`abusezero_list_policy_signals`](/docs/tools/mcp#mcp-tools) | [`policies signals`](/docs/tools/cli#cli-command-reference) |
| List allow and block list entries | [`GET /v1/access-lists`](/docs/api-reference/access-lists#list-entries) | [`abusezero_list_access_lists`](/docs/tools/mcp#mcp-tools) | [`access-lists list`](/docs/tools/cli#cli-command-reference) |
| Add allow or block list entries | [`POST /v1/access-lists`](/docs/api-reference/access-lists#add-an-entry) | [`abusezero_add_access_list_entry`](/docs/tools/mcp#mcp-tools) | [`access-lists add`](/docs/tools/cli#cli-command-reference) |
| Read a list entry | [`GET /v1/access-lists/{id}`](/docs/api-reference/access-lists#retrieve-an-entry) | [`abusezero_get_access_list_entry`](/docs/tools/mcp#mcp-tools) | [`access-lists get`](/docs/tools/cli#cli-command-reference) |
| Remove a list entry | [`DELETE /v1/access-lists/{id}`](/docs/api-reference/access-lists#remove-an-entry) | [`abusezero_delete_access_list_entry`](/docs/tools/mcp#mcp-tools) | [`access-lists delete`](/docs/tools/cli#cli-command-reference) |
| List custom identifiers | [`GET /v1/custom-identifiers`](/docs/api-reference/custom-identifiers#list-custom-identifiers) | [`abusezero_list_custom_identifiers`](/docs/tools/mcp#mcp-tools) | [`custom-identifiers list`](/docs/tools/cli#cli-command-reference) |
| Register a custom identifier | [`POST /v1/custom-identifiers`](/docs/api-reference/custom-identifiers#register-a-custom-identifier) | [`abusezero_register_custom_identifier`](/docs/tools/mcp#mcp-tools) | [`custom-identifiers register`](/docs/tools/cli#cli-command-reference) |
| Read a custom identifier | [`GET /v1/custom-identifiers/{id}`](/docs/api-reference/custom-identifiers#retrieve-a-custom-identifier) | [`abusezero_get_custom_identifier`](/docs/tools/mcp#mcp-tools) | [`custom-identifiers get`](/docs/tools/cli#cli-command-reference) |
| Delete a custom identifier | [`DELETE /v1/custom-identifiers/{id}`](/docs/api-reference/custom-identifiers#remove-a-custom-identifier) | [`abusezero_delete_custom_identifier`](/docs/tools/mcp#mcp-tools) | [`custom-identifiers delete`](/docs/tools/cli#cli-command-reference) |
| List imports | [`GET /v1/imports`](/docs/api-reference/imports#list-imports) | [`abusezero_list_imports`](/docs/tools/mcp#mcp-tools) | [`imports list`](/docs/tools/cli#cli-command-reference) |
| Import granted outcomes | [`POST /v1/imports`](/docs/api-reference/imports#import-outcomes-as-json) | [`abusezero_create_outcome_import`](/docs/tools/mcp#mcp-tools) | [`imports create`](/docs/tools/cli#cli-command-reference) |
| Start a CSV import | [`POST /v1/imports/csv`](/docs/api-reference/imports#create-an-upload) | [`abusezero_create_csv_import`](/docs/tools/mcp#mcp-tools) | [`imports create-csv`](/docs/tools/cli#cli-command-reference) |
| Read an import | [`GET /v1/imports/{id}`](/docs/api-reference/imports#retrieve-an-import) | [`abusezero_get_import`](/docs/tools/mcp#mcp-tools) | [`imports get`](/docs/tools/cli#cli-command-reference) |
| Read an import's errors | [`GET /v1/imports/{id}/errors`](/docs/api-reference/imports#download-the-error-report) | [`abusezero_get_import_errors`](/docs/tools/mcp#mcp-tools) | [`imports errors`](/docs/tools/cli#cli-command-reference) |
| Validate an import | [`POST /v1/imports/{id}/validate`](/docs/api-reference/imports#validate-the-import) | [`abusezero_validate_import`](/docs/tools/mcp#mcp-tools) | [`imports validate`](/docs/tools/cli#cli-command-reference) |
| Confirm an import | [`POST /v1/imports/{id}/confirm`](/docs/api-reference/imports#confirm-the-import) | [`abusezero_confirm_import`](/docs/tools/mcp#mcp-tools) | [`imports confirm`](/docs/tools/cli#cli-command-reference) |
| Cancel an import | [`POST /v1/imports/{id}/cancel`](/docs/api-reference/imports#cancel-an-import) | [`abusezero_cancel_import`](/docs/tools/mcp#mcp-tools) | [`imports cancel`](/docs/tools/cli#cli-command-reference) |
| List exports | [`GET /v1/exports`](/docs/api-reference/exports#list-exports) | [`abusezero_list_exports`](/docs/tools/mcp#mcp-tools) | [`exports list`](/docs/tools/cli#cli-command-reference) |
| Request an export | [`POST /v1/exports`](/docs/api-reference/exports#request-an-export) | [`abusezero_request_export`](/docs/tools/mcp#mcp-tools) | [`exports create`](/docs/tools/cli#cli-command-reference) |
| Read an export | [`GET /v1/exports/{id}`](/docs/api-reference/exports#retrieve-an-export) | [`abusezero_get_export`](/docs/tools/mcp#mcp-tools) | [`exports get`](/docs/tools/cli#cli-command-reference) |
| Download an export | [`GET /v1/exports/{id}/download`](/docs/api-reference/exports#download-an-export) | [`abusezero_get_export_download`](/docs/tools/mcp#mcp-tools) | [`exports download`](/docs/tools/cli#cli-command-reference) |
| Preview a data deletion | [`POST /v1/data-deletions/preview`](/docs/api-reference/data-deletions#preview-a-deletion) | [`abusezero_preview_data_deletion`](/docs/tools/mcp#mcp-tools) | [`data-deletions preview`](/docs/tools/cli#cli-command-reference) |
| Request a data deletion | [`POST /v1/data-deletions`](/docs/api-reference/data-deletions#request-a-deletion) | [`abusezero_request_data_deletion`](/docs/tools/mcp#mcp-tools) | [`data-deletions create`](/docs/tools/cli#cli-command-reference) |
| List data deletions | [`GET /v1/data-deletions`](/docs/api-reference/data-deletions#list-deletions) | [`abusezero_list_data_deletions`](/docs/tools/mcp#mcp-tools) | [`data-deletions list`](/docs/tools/cli#cli-command-reference) |
| Read a data deletion | [`GET /v1/data-deletions/{id}`](/docs/api-reference/data-deletions#retrieve-a-deletion) | [`abusezero_get_data_deletion`](/docs/tools/mcp#mcp-tools) | [`data-deletions get`](/docs/tools/cli#cli-command-reference) |
| Read analytics | [`GET /v1/analytics`](/docs/api-reference/analytics#read-analytics) | [`abusezero_get_analytics`](/docs/tools/mcp#mcp-tools) | [`analytics get`](/docs/tools/cli#cli-command-reference) |
| List request logs | [`GET /v1/request-logs`](/docs/api-reference/request-logs#list-request-logs) | [`abusezero_list_request_logs`](/docs/tools/mcp#mcp-tools) | [`request-logs list`](/docs/tools/cli#cli-command-reference) |
| Read a request log | [`GET /v1/request-logs/{id}`](/docs/api-reference/request-logs#retrieve-a-request-log) | [`abusezero_get_request_log`](/docs/tools/mcp#mcp-tools) | [`request-logs get`](/docs/tools/cli#cli-command-reference) |
| List audit events | [`GET /v1/audit-logs`](/docs/api-reference/audit-logs#list-audit-events) | [`abusezero_list_audit_logs`](/docs/tools/mcp#mcp-tools) | [`audit-logs list`](/docs/tools/cli#cli-command-reference) |
| Read an audit event | [`GET /v1/audit-logs/{id}`](/docs/api-reference/audit-logs#retrieve-an-audit-event) | [`abusezero_get_audit_log`](/docs/tools/mcp#mcp-tools) | [`audit-logs get`](/docs/tools/cli#cli-command-reference) |
| Read browser setup | [`GET /v1/browser-collection`](/docs/api-reference/browser-collection#retrieve-browser-setup) | [`abusezero_get_browser_collection`](/docs/tools/mcp#mcp-tools) | [`browser-collection get`](/docs/tools/cli#cli-command-reference) |
| Update your site list | [`PATCH /v1/browser-collection`](/docs/api-reference/browser-collection#update-the-site-list) | [`abusezero_update_browser_collection`](/docs/tools/mcp#mcp-tools) | [`browser-collection update`](/docs/tools/cli#cli-command-reference) |
| List recognized agents | [`GET /v1/agents`](/docs/api-reference/agents#list-recognized-agents) | [`abusezero_list_agents`](/docs/tools/mcp#mcp-tools) | [`agents list`](/docs/tools/cli#cli-command-reference) |
## Connection actions [#connection-actions]
These manage the connection itself, so each one exists only where it applies.
| Action | AbuseZero MCP server | AbuseZero CLI |
|---|---|---|
| Sign in | Your app's sign-in flow, see [MCP server authentication](/docs/tools/mcp#mcp-server-authentication) | [`abusezero login`](/docs/tools/cli#sign-in-to-the-cli) |
| Sign out | Your app's disconnect option | `abusezero logout` |
| Choose a workspace | [`abusezero_select_workspace`](/docs/tools/mcp#choose-a-workspace) | [`abusezero workspaces use`](/docs/tools/cli#choose-a-workspace) |
| Show the connection | `abusezero_get_connection_context` | `abusezero whoami` |
| Run a local MCP server | None | [`abusezero mcp`](/docs/tools/cli#run-a-local-mcp-server-from-the-cli) |
The [MCP prompts](/docs/tools/mcp#mcp-prompts) `review_policy`, `investigate_denial` and `simulate_policy_change` are instructions for an AI app, so the CLI doesn't offer them.
## Exclusions [#exclusions]
Some capabilities are deliberately available on one surface only.
| Capability | Where it's available | Why |
|---|---|---|
| Browser observation | The [AbuseZero browser SDK](/docs/tools/browser-sdk) and [standalone browser script](/docs/tools/browser-sdk#standalone-browser-script), see [Browser protection](/docs/concepts/browser-protection) | Browser evidence must come from a real visitor's browser. A remote agent, a server or the CLI can't produce it. |
| Account security: password, two-step verification and linked sign-ins | Dashboard | These protect your own sign-in, so they always need you, signed in, in the dashboard. |
| API keys and machine clients | Dashboard, under Developers. See [Keys and access](/docs/keys-and-access) | A credential can never create or widen another credential. |
| Billing, credits and plans | Dashboard, under Workspace settings → Billing & Subscriptions | Only the workspace owner manages billing. |
| Members, roles and ownership | Dashboard. See [Workspaces](/docs/keys-and-access#workspaces) | Changes to who can reach a workspace always need a signed-in person. |
| Workspace deletion | Dashboard | Deleting a workspace is permanent, so it always needs a signed-in person in the dashboard. |
# SDKs, CLI and MCP server (/docs/tools)
AbuseZero ships as one npm package, `abusezero`. It contains the [AbuseZero Node.js SDK](/docs/tools/server-sdk), the [AbuseZero browser SDK](/docs/tools/browser-sdk), the [AbuseZero CLI](/docs/tools/cli) and a [local MCP server](/docs/tools/mcp#local-mcp-server). All four are built on the same request contracts as the [AbuseZero REST API](/docs/api-reference), so a field means the same thing wherever you send it. Everything the package needs ships inside it, so installing it adds no other dependencies to your project.
```npm
npm install abusezero
```
## Entry points [#entry-points]
The package has no root import. Import the entry point for the runtime your code runs in.
| Entry point | Runtime | Use it to |
|---|---|---|
| [`abusezero/server`](/docs/tools/server-sdk) | Node.js 22 or later, as an ES module or CommonJS | Evaluate claims, report outcomes and manage your workspace from your backend. See [Evaluations](/docs/concepts/evaluations) and [Outcomes](/docs/concepts/outcomes). |
| [`abusezero/browser`](/docs/tools/browser-sdk) | Browsers, through your bundler, as an ES module | Run [browser protection](/docs/concepts/browser-protection) and the [bot check](/docs/guides/bot-check) on your pages. |
| [`abusezero` command](/docs/tools/cli) | Node.js 22 or later | Work with AbuseZero from a terminal, a script or CI, and run a [local MCP server](/docs/tools/mcp#local-mcp-server). |
| [Standalone script](/docs/tools/browser-sdk#standalone-browser-script) | Browsers, without a bundler | Add browser protection to pages that load scripts directly. |
Keep `abusezero/server` and your [secret keys](/docs/keys-and-access#secret-and-restricted-keys) in your backend. The browser entry point never loads
server code, credentials or Node.js modules, so nothing from your backend reaches a visitor's browser.
```ts title="Server"
import { AbuseZero } from "abusezero/server";
```
```ts title="Browser"
import { init, observe } from "abusezero/browser";
```
```html title="Standalone script"
```
## Versions [#versions]
The current version is `0.11.0`. Every version works with [REST API `v1`](/docs/api-reference#rest-api-versioning).
Within `v1`, responses can gain new fields and new values, such as [reason codes](/docs/reason-codes) and [skip reasons](/docs/api-reference/evaluations#skipped-evaluations). Write your integration to accept a value it doesn't recognize. Each reference page says how to handle unknown values for its fields.
The standalone script's address includes its version. When you upgrade the package, update the script address on your pages to the same version. AbuseZero reports pages running a version it no longer accepts as [`sdk_outdated`](/docs/concepts/browser-protection#browser-data-statuses) in Developers → Browser setup.
## Upgrade to 0.11.0 [#upgrade-to-0110]
- **Browser:** update the [AbuseZero browser SDK](/docs/tools/browser-sdk) or the standalone script address to `0.11.0`. AbuseZero refuses browser data from earlier versions, which report [`sdk_outdated`](/docs/concepts/browser-protection#browser-data-statuses).
- **AI agents:** [`verifyAgentRequest()`](/docs/tools/server-sdk#verify-ai-agents) returns an opaque `AgentEvidence` token. Send it unchanged as `agent_evidence`. Evidence from earlier versions is refused with [`AGENT_EVIDENCE_OUTDATED`](/docs/troubleshooting#agent_evidence_outdated-422).
- **Previews:** [`previewEvaluation()`](/docs/tools/server-sdk#preview-an-evaluation) results no longer include `signals`. Read signal family assessments from [`evaluationEvidence()`](/docs/api-reference/evaluations#retrieve-an-evaluations-evidence) or the dashboard.
- **Scores:** `risk.score` moves in steps of 5, and a custom policy's `risk_threshold` must be a multiple of 5. See [Policies and rules](/docs/concepts/policies-and-rules).
## Upgrade to 0.10.0 [#upgrade-to-0100]
- **Keys:** keys start with `sk_`, `rk_` or `pk_`. Keys created before this version no longer work, and `new AbuseZero()` rejects them with `INVALID_KEY`. Create new keys in Developers and replace them in your app. See [Keys and access](/docs/keys-and-access).
- **Browser sites:** AbuseZero accepts browser data only from HTTPS sites in your site list. Check the list in Developers → Browser setup, or with [`GET /v1/browser-collection`](/docs/api-reference/browser-collection#retrieve-browser-setup).
- **CLI:** run [`abusezero login`](/docs/tools/cli#sign-in-to-the-cli) again. Sign-ins saved by earlier versions no longer work. One sign-in now covers every command.
- **MCP:** reconnect each AI app to `https://mcp.abusezero.com/mcp` and sign in again, as in [Connect an AI app to the MCP server](/docs/tools/mcp#connect-an-ai-app-to-the-mcp-server).
- **From `@abusezero/sdk`:** change imports to `abusezero/server` and `abusezero/browser`, and replace the `abusezero-mcp` command with `abusezero mcp`.
## License [#license]
The package is proprietary. Its license ships with it as `LICENSE`, and third-party notices as `THIRD-PARTY-NOTICES`.
## Next steps [#next-steps]
Evaluate claims and manage your workspace from Node.js.
Connect your pages to browser protection and the bot check.
Run every action from a terminal, a script or CI.
Connect AI apps to your workspace.
# AbuseZero MCP server (/docs/tools/mcp)
AbuseZero runs a remote MCP server that any client supporting the standard Streamable HTTP transport can use. AI apps work with the same actions as the [AbuseZero REST API](/docs/api-reference), with the same envelopes, permission checks and [idempotency](/docs/api-reference#idempotency). Every tool also has a matching command in the [AbuseZero CLI](/docs/tools/cli#cli-command-reference). A connection only sees the tools its [scopes](/docs/api-reference/authentication#scopes) allow, and it only reaches the [workspaces](/docs/keys-and-access#workspaces) it was granted.
```txt
https://mcp.abusezero.com/mcp
```
## Connect an AI app to the MCP server [#connect-an-ai-app-to-the-mcp-server]
### Remote MCP server [#remote-mcp-server]
Add a remote MCP server with the address above. The app opens your browser to sign in to AbuseZero with [OAuth](/docs/keys-and-access#oauth), where you approve the permissions it asks for. Configuration formats vary by app. A typical entry looks like this:
```json title="MCP client configuration"
{
"mcpServers": {
"abusezero": { "url": "https://mcp.abusezero.com/mcp" }
}
}
```
When you approve a new app, AbuseZero emails you that it was connected. You can disconnect an app at any time in **Developers → Connected apps**.
### Local MCP server [#local-mcp-server]
For apps that start MCP servers on your computer, run the local server from the `abusezero` package:
```json title="MCP client configuration"
{
"mcpServers": {
"abusezero": { "command": "npx", "args": ["-y", "abusezero", "mcp"] }
}
}
```
The local server connects to the remote server, so its tools, resources and prompts are identical. It uses your [AbuseZero CLI](/docs/tools/cli) sign-in and selected workspace. To use a [secret or restricted key](/docs/keys-and-access#secret-and-restricted-keys) instead, set `ABUSEZERO_API_KEY` in the app's environment for the server.
## MCP server authentication [#mcp-server-authentication]
| Credential | Workspace | Permissions |
|---|---|---|
| OAuth sign-in | Chosen in the session with `abusezero_select_workspace` | The scopes you approved, limited by your role in the workspace |
| Secret or restricted key, sent as `Authorization: Bearer ` | The key's workspace | The key's scopes |
[Publishable keys](/docs/keys-and-access#publishable-key) can't connect.
## Choose a workspace [#choose-a-workspace]
OAuth connections start without a workspace:
1. Call `abusezero_list_workspaces` to see the workspaces the sign-in can use.
2. Call `abusezero_select_workspace` with a `workspace_id` such as `ws_8a1f3c6e-2d4b-4e9a-b7c5-0f3d6a9e2b81`.
3. Call `abusezero_get_connection_context` at any time to see the credential type and selected workspace.
Workspace tools return [`WORKSPACE_REQUIRED`](/docs/api-reference/errors#permission) until a workspace is selected. Key connections already use their key's workspace, so selecting one returns `WORKSPACE_ALREADY_BOUND`.
## MCP tool behavior [#mcp-tool-behavior]
- **Inputs** are the request fields of the matching [REST API endpoint](/docs/tools/coverage). Path IDs, such as `id`, are inputs too.
- **Changes** take an `idempotency_key` input of 1 to 128 printable characters with no spaces. Send the same key to retry a change safely.
- **Annotations** mark each tool as read-only, changing data or permanent, so your app can ask before it acts.
- **Results** carry the same [response envelope](/docs/api-reference#responses) as the REST API in `structuredContent`, plus [`request_id`](/docs/api-reference#request-ids), and a one-line text summary.
- **Errors** set `isError`. `structuredContent` carries the [error envelope](/docs/api-reference/errors), and the text reads `CODE: message`.
## MCP tools [#mcp-tools]
Every tool links to the [AbuseZero REST API](/docs/api-reference) endpoint it calls, where its inputs, response and errors are described. The Scope column is the tool's [scope](/docs/api-reference/authentication#scopes).
| Tool | What it does | Scope |
|---|---|---|
| `abusezero_select_workspace` | Choose the workspace for this session. | `workspaces:read` |
| `abusezero_get_connection_context` | Show the credential type and selected workspace. | None |
| [`abusezero_list_workspaces`](/docs/api-reference/workspaces#list-workspaces) | List the workspaces this connection can use. | `workspaces:read` |
| [`abusezero_create_evaluation`](/docs/api-reference/evaluations#create-an-evaluation) | Evaluate a claim. | `evaluations:create` |
| [`abusezero_preview_evaluation`](/docs/api-reference/previews-simulations-replays#preview-an-evaluation) | Preview how AbuseZero would evaluate a claim, without writing claim history. | `simulations:run` |
| [`abusezero_list_evaluations`](/docs/api-reference/evaluations#list-evaluations) | List evaluations. | `evaluations:read` |
| [`abusezero_get_evaluation`](/docs/api-reference/evaluations#retrieve-an-evaluation) | Read an evaluation. | `evaluations:read` |
| [`abusezero_get_evaluation_graph`](/docs/api-reference/evaluations#retrieve-an-evaluations-association-graph) | Read the earlier claims linked to an evaluation. | `evaluations:read` |
| [`abusezero_get_evaluation_evidence`](/docs/api-reference/evaluations#retrieve-an-evaluations-evidence) | Read the evidence retained for an evaluation. | `evaluation_evidence:read` |
| [`abusezero_report_outcome`](/docs/api-reference/outcomes#report-an-outcome) | Report what happened after an evaluation. | `outcomes:write` |
| [`abusezero_list_outcomes`](/docs/api-reference/outcomes#list-outcomes) | List outcomes. | `outcomes:read` |
| [`abusezero_get_outcome`](/docs/api-reference/outcomes#retrieve-an-outcome) | Read an outcome. | `outcomes:read` |
| [`abusezero_simulate_sequence`](/docs/api-reference/previews-simulations-replays#run-a-simulation) | Run a benefit's recent claims through a policy, in order, as one sequence. | `simulations:run` |
| [`abusezero_replay_evaluations`](/docs/api-reference/previews-simulations-replays#run-a-replay) | Compare recent evaluations with how a saved policy would decide them. | `simulations:run` |
| [`abusezero_list_benefits`](/docs/api-reference/benefits#list-benefits) | List benefits. | `benefits:read` |
| [`abusezero_create_benefit`](/docs/api-reference/benefits#create-a-benefit) | Create a benefit. | `benefits:write` |
| [`abusezero_get_benefit`](/docs/api-reference/benefits#retrieve-a-benefit) | Read a benefit. | `benefits:read` |
| [`abusezero_update_benefit`](/docs/api-reference/benefits#update-a-benefit) | Update a benefit. | `benefits:write` |
| [`abusezero_delete_benefit`](/docs/api-reference/benefits#delete-a-benefit) | Delete a benefit. Permanent. | `benefits:write` |
| [`abusezero_list_policies`](/docs/api-reference/policies#list-policies) | List policies. | `policies:read` |
| [`abusezero_create_policy`](/docs/api-reference/policies#create-a-policy) | Create a custom policy. | `policies:write` |
| [`abusezero_get_policy`](/docs/api-reference/policies#retrieve-a-policy) | Read a policy. | `policies:read` |
| [`abusezero_update_policy`](/docs/api-reference/policies#update-a-policy) | Save a new version of a policy. | `policies:write` |
| [`abusezero_duplicate_policy`](/docs/api-reference/policies#duplicate-a-policy) | Copy a policy. | `policies:write` |
| [`abusezero_delete_policy`](/docs/api-reference/policies#delete-a-policy) | Delete a policy. Permanent. | `policies:write` |
| [`abusezero_list_policy_versions`](/docs/api-reference/policies#list-policy-versions) | List a policy's versions. | `policies:read` |
| [`abusezero_get_policy_version`](/docs/api-reference/policies#retrieve-a-policy-version) | Read one version of a policy. | `policies:read` |
| [`abusezero_list_policy_signals`](/docs/api-reference/policies#list-conditions) | List the conditions custom rules can use. | `policies:read` |
| [`abusezero_list_access_lists`](/docs/api-reference/access-lists#list-entries) | List allow and block list entries. | `access_lists:read` |
| [`abusezero_add_access_list_entry`](/docs/api-reference/access-lists#add-an-entry) | Add one or more values to an allow or block list. | `access_lists:write` |
| [`abusezero_get_access_list_entry`](/docs/api-reference/access-lists#retrieve-an-entry) | Read a list entry. | `access_lists:read` |
| [`abusezero_delete_access_list_entry`](/docs/api-reference/access-lists#remove-an-entry) | Remove a list entry. Permanent. | `access_lists:write` |
| [`abusezero_list_custom_identifiers`](/docs/api-reference/custom-identifiers#list-custom-identifiers) | List custom identifiers. | `custom_identifiers:read` |
| [`abusezero_register_custom_identifier`](/docs/api-reference/custom-identifiers#register-a-custom-identifier) | Register a custom identifier. | `custom_identifiers:write` |
| [`abusezero_get_custom_identifier`](/docs/api-reference/custom-identifiers#retrieve-a-custom-identifier) | Read a custom identifier. | `custom_identifiers:read` |
| [`abusezero_delete_custom_identifier`](/docs/api-reference/custom-identifiers#remove-a-custom-identifier) | Delete a custom identifier. Permanent. | `custom_identifiers:write` |
| [`abusezero_list_imports`](/docs/api-reference/imports#list-imports) | List imports. | `imports:read` |
| [`abusezero_create_outcome_import`](/docs/api-reference/imports#import-outcomes-as-json) | Import granted outcomes as JSON. | `imports:write` |
| [`abusezero_create_csv_import`](/docs/api-reference/imports#create-an-upload) | Start a CSV import and get its upload address. | `imports:write` |
| [`abusezero_get_import`](/docs/api-reference/imports#retrieve-an-import) | Read an import's progress. | `imports:read` |
| [`abusezero_get_import_errors`](/docs/api-reference/imports#download-the-error-report) | Get the error report for an import. | `imports:read` |
| [`abusezero_validate_import`](/docs/api-reference/imports#validate-the-import) | Validate an uploaded import. | `imports:write` |
| [`abusezero_confirm_import`](/docs/api-reference/imports#confirm-the-import) | Confirm a validated import. | `imports:write` |
| [`abusezero_cancel_import`](/docs/api-reference/imports#cancel-an-import) | Cancel an import and delete its uploaded file and unfinished rows. Permanent. | `imports:write` |
| [`abusezero_list_exports`](/docs/api-reference/exports#list-exports) | List exports. | `exports:read` |
| [`abusezero_request_export`](/docs/api-reference/exports#request-an-export) | Request an export. | `exports:create` |
| [`abusezero_get_export`](/docs/api-reference/exports#retrieve-an-export) | Read an export's status. | `exports:read` |
| [`abusezero_get_export_download`](/docs/api-reference/exports#download-an-export) | Get a download address for a finished export. | `exports:read` |
| [`abusezero_preview_data_deletion`](/docs/api-reference/data-deletions#preview-a-deletion) | Preview what a data deletion would remove. | `data_deletions:write` |
| [`abusezero_request_data_deletion`](/docs/api-reference/data-deletions#request-a-deletion) | Request a data deletion. Permanent. | `data_deletions:write` |
| [`abusezero_list_data_deletions`](/docs/api-reference/data-deletions#list-deletions) | List data deletions. | `data_deletions:read` |
| [`abusezero_get_data_deletion`](/docs/api-reference/data-deletions#retrieve-a-deletion) | Read a data deletion's status. | `data_deletions:read` |
| [`abusezero_get_analytics`](/docs/api-reference/analytics#read-analytics) | Read daily evaluation totals. | `analytics:read` |
| [`abusezero_list_request_logs`](/docs/api-reference/request-logs#list-request-logs) | List evaluation requests, including failed ones. | `request_logs:read` |
| [`abusezero_get_request_log`](/docs/api-reference/request-logs#retrieve-a-request-log) | Read a request log. | `request_logs:read` |
| [`abusezero_list_audit_logs`](/docs/api-reference/audit-logs#list-audit-events) | List audit events. | `audit_logs:read` |
| [`abusezero_get_audit_log`](/docs/api-reference/audit-logs#retrieve-an-audit-event) | Read an audit event. | `audit_logs:read` |
| [`abusezero_get_browser_collection`](/docs/api-reference/browser-collection#retrieve-browser-setup) | Read browser setup: sites, issues and recent coverage. | `browser_collection:read` |
| [`abusezero_update_browser_collection`](/docs/api-reference/browser-collection#update-the-site-list) | Update your site list. | `browser_collection:write` |
| [`abusezero_list_agents`](/docs/api-reference/agents#list-recognized-agents) | List the AI agents AbuseZero recognizes. | `agents:read` |
## MCP resources [#mcp-resources]
Apps that read MCP resources can open single records by address:
| Resource | Contents |
|---|---|
| `abusezero://benefits/{id}` | A [benefit](/docs/concepts/benefits), as returned by [`abusezero_get_benefit`](/docs/api-reference/benefits#retrieve-a-benefit). |
| `abusezero://policies/{id}` | A [policy](/docs/concepts/policies-and-rules), as returned by [`abusezero_get_policy`](/docs/api-reference/policies#retrieve-a-policy). |
| `abusezero://evaluations/{id}` | An [evaluation](/docs/concepts/evaluations), as returned by [`abusezero_get_evaluation`](/docs/api-reference/evaluations#retrieve-an-evaluation). |
| `abusezero://imports/{id}` | An [import](/docs/guides/import-claim-history), as returned by [`abusezero_get_import`](/docs/api-reference/imports#retrieve-an-import). |
## MCP prompts [#mcp-prompts]
Prompts give an AI app a ready-made, careful workflow. None of them changes anything on its own.
| Prompt | Argument | What it does |
|---|---|---|
| `review_policy` | `policy_id` | Reviews a saved [policy](/docs/concepts/policies-and-rules) without changing it. |
| `investigate_denial` | `evaluation_id` | Walks through a denied [evaluation](/docs/concepts/evaluations), read-only, using its [reason code](/docs/reason-codes). |
| `simulate_policy_change` | `policy_id` | Proposes a change to a policy and tests it with a [simulation](/docs/api-reference/previews-simulations-replays#run-a-simulation) before anything is published. See [Test changes safely](/docs/guides/test-changes-safely). |
## Actions not available through the MCP server [#actions-not-available-through-the-mcp-server]
Browser observation, which needs the [AbuseZero browser SDK](/docs/tools/browser-sdk), and dashboard-only work, such as API keys, billing and members, aren't MCP tools. See [Exclusions](/docs/tools/coverage#exclusions) for the full list and the reasons.
# AbuseZero Node.js SDK (/docs/tools/server-sdk)
The AbuseZero Node.js SDK, `abusezero/server`, is the typed Node.js client for the [AbuseZero REST API](/docs/api-reference). It checks evaluations and outcomes against the API contract before they leave your server, and checks AbuseZero's answer before it reaches your code. It retries transient failures within a fixed request budget, and you set in advance what happens if AbuseZero can't be reached. Every `evaluate()` call ends in one of three well-defined results, or in an `AbuseZeroError`.
## Install the Node.js SDK [#install-the-nodejs-sdk]
```npm
npm install abusezero
```
The Node.js SDK needs Node 22 or later. Keep it, and your [secret key](/docs/keys-and-access#secret-and-restricted-keys), on your server. For every entry point in the package, see [SDKs, CLI and MCP server](/docs/tools).
## Create a Node.js SDK client [#create-a-nodejs-sdk-client]
```ts title="lib/abusezero.ts"
import { AbuseZero } from "abusezero/server";
export const abusezero = new AbuseZero({
key: process.env.ABUSEZERO_SECRET_KEY!,
outage: "throw",
});
```
CommonJS projects use `const { AbuseZero } = require("abusezero/server");`.
Create one client and reuse it. The client keeps its connection to AbuseZero open between calls, so occasional evaluations don't pay to open a new one.
### Client options [#client-options]
| Option | Type | Required | Description |
|---|---|---|---|
| `key` | string | One of `key` or `accessToken` | A [secret key (`sk_…`) or restricted key (`rk_…`)](/docs/keys-and-access#secret-and-restricted-keys) from Developers. [Publishable keys](/docs/keys-and-access#publishable-key) are rejected. |
| `accessToken` | string | One of `key` or `accessToken` | An [OAuth](/docs/keys-and-access#oauth) access token. Requires `workspace`. |
| `workspace` | string | With `accessToken` | The `ws_…` ID of the [workspace](/docs/api-reference/workspaces) this OAuth connection works in. |
| `outage` | `"throw"`, `"allow"` or `"deny"` | Yes | What `evaluate()` does when AbuseZero can't be reached. See [the `outage` option](#the-outage-option). |
| `fetch` | function | No | Sends every request through your own `fetch`, for example through a proxy. |
Pass exactly one of `key` and `accessToken`. The constructor throws `CREDENTIAL_REQUIRED` for neither or both, `INVALID_KEY` for a key that isn't a server key, `WORKSPACE_REQUIRED` for an access token without a workspace, and `OUTAGE_BEHAVIOR_REQUIRED` when `outage` is missing.
## Evaluate a claim [#evaluate-a-claim]
Call `evaluate()` before you grant the [benefit](/docs/concepts/benefits). It calls [`POST /v1/evaluations`](/docs/api-reference/evaluations#create-an-evaluation). The second argument is the [idempotency key](/docs/api-reference#idempotency).
```ts title="app/api/start-trial/route.ts"
import { abusezero } from "@/lib/abusezero";
export async function POST(request: Request) {
const { claimId, deviceToken } = await request.json();
const user = await currentUser(request); // Your own session lookup.
const ip = clientIp(request); // The visitor's IP from your trusted proxy chain.
const evaluation = await abusezero.evaluate(
{
benefit: "pro_trial",
claim_id: claimId,
subject: { external_id: user.id, email: user.email, email_verified: user.emailVerified },
context: {
ip,
user_agent: request.headers.get("user-agent") ?? undefined,
},
identifiers: { device_token: deviceToken },
},
`trial:${claimId}`,
);
if ("status" in evaluation) {
// Skipped: AbuseZero decided nothing. Handle it as its own branch.
return Response.json({ trial: "not_started", skipped: evaluation.reason }, { status: 503 });
}
if (evaluation.result === "deny") {
return Response.json({ trial: "not_available" }, { status: 403 });
}
// "allow". When evaluation.source is "local_outage", this is your outage setting.
return Response.json({ trial: "started" });
}
```
Use one idempotency key per claim, and send the same key when you retry that claim. AbuseZero then returns the original result instead of evaluating the claim twice. Keys are 1 to 128 printable characters with no spaces.
Every field of the request is described in the [Evaluations API reference](/docs/api-reference/evaluations). For which identifiers to send and how, see [Send identifiers](/docs/guides/send-identifiers).
### Results [#results]
`evaluate()` returns one of three shapes. Check for `status` first, then `source`.
| Shape | Fields | Meaning |
|---|---|---|
| Evaluated | `source: "abusezero"`, `id`, `result`, `risk`, `reason`, `outcome` | AbuseZero evaluated the claim. `result` is `allow` or `deny`. |
| Skipped | `source: "abusezero"`, `status: "skipped"`, `reason` | The workspace couldn't run the evaluation, so there is no `result`. |
| Local outage | `source: "local_outage"`, `claim_id`, `result`, `error` | AbuseZero couldn't be reached. `result` is your `outage` setting, and `error` is the `AbuseZeroError` behind it. |
[Skip reasons](/docs/api-reference/evaluations#skipped-evaluations) are `insufficient_credits`, `billing_overdue`, `service_inactive` and `benefit_paused`. New reasons can appear within API `v1`, so handle a reason you don't recognize like any other skip. Your `outage` setting never applies to a skip. The workspace owner resolves skips in **Workspace settings → Billing & Subscriptions**.
`risk`, `reason` and `outcome` are described in [Evaluations](/docs/concepts/evaluations), and every reason code in [Reason codes](/docs/reason-codes).
## The `outage` option [#the-outage-option]
`outage` decides what `evaluate()` does when AbuseZero can't be reached or can't finish the request in time:
- `"throw"`: `evaluate()` throws the `AbuseZeroError`, and your code decides.
- `"allow"` or `"deny"`: `evaluate()` returns a `local_outage` result with that decision and the error.
These are always thrown, whatever your setting:
- **[Rate limits](/docs/api-reference#rate-limits) (`429`).** Request volume can never switch protection off.
- **Bot-check errors:** codes that start with `BOT_EVIDENCE_`, such as `BOT_EVIDENCE_REQUIRED` and `BOT_EVIDENCE_UNAVAILABLE`. Collect a fresh device token with `prepareBotObservation()` and evaluate again. See [Add a bot check](/docs/guides/bot-check).
- **Errors that aren't retryable,** such as an invalid request or a revoked key.
A `local_outage` result was decided by your code, not by AbuseZero. When AbuseZero is reachable again, evaluate the same claim with the same idempotency key to get AbuseZero's result. See [Handle outages and retries](/docs/guides/outages-and-retries).
## Retries [#retries]
Every call runs within a request budget. Inside it, the SDK retries network failures, `429`, `502`, `503` and `504` responses, and any error AbuseZero marks [`retryable`](/docs/api-reference/errors#error-envelope). Each retry waits at least as long as the response's `Retry-After` header asks. Retries reuse the call's idempotency key, so a retried write is never applied twice.
When the budget runs out, the call fails with `DEADLINE_EXCEEDED`. When AbuseZero can't be reached or the response doesn't arrive complete, it fails with `TRANSPORT_ERROR`. When a response arrives but can't be used, it fails with `INVALID_RESPONSE`. All three are retryable: send the same call with the same idempotency key to recover its result before acting on the claim.
## Preview an evaluation [#preview-an-evaluation]
`previewEvaluation()` takes the same input as `evaluate()` and calls [`POST /v1/evaluation-previews`](/docs/api-reference/previews-simulations-replays#preview-an-evaluation). It returns how AbuseZero would treat the claim, without writing claim history. It also returns which [inputs](/docs/concepts/risk-score-and-signals#inputs) AbuseZero received.
```ts
const preview = await abusezero.previewEvaluation(input, `preview:${claimId}`);
if (!("status" in preview)) {
console.log(preview.result, preview.risk.band, preview.signals, preview.inputs);
}
```
Previews use credits, like evaluations. See [Test changes safely](/docs/guides/test-changes-safely).
## Report an outcome [#report-an-outcome]
Tell AbuseZero what happened after an evaluation, so later evaluations are checked against it. `outcome()` calls [`POST /v1/evaluations/{id}/outcome`](/docs/api-reference/outcomes#report-an-outcome). For outcome states and tracking, see [Outcomes](/docs/concepts/outcomes).
```ts
await abusezero.outcome(
evaluation.id,
{ outcome: "granted", external_outcome_id: "sub_2291" },
`outcome:${evaluation.id}`,
);
```
| Field | Type | Required | Description |
|---|---|---|---|
| `outcome` | `"granted"`, `"not_granted"` or `"reversed"` | Yes | What happened to the benefit. |
| `reason` | string, 3 to 500 characters | With `not_granted` and `reversed` | Why the outcome changed. A change to `not_granted` or `reversed` without a reason returns `REASON_REQUIRED`. |
| `external_outcome_id` | string, 1 to 128 printable characters | No | Your own reference for the outcome. |
`outcome()` returns an `outcome_report` with the evaluation's ID, its new outcome `state` and any `conflicts`. See [Report outcomes](/docs/guides/report-outcomes).
To bring in benefits you granted before you used AbuseZero, pass up to 1,000 granted outcomes per call to `importOutcomes(outcomes, idempotencyKey)`, which calls [`POST /v1/imports`](/docs/api-reference/imports#import-outcomes-as-json). See [Import outcome history](/docs/guides/import-claim-history).
## Hash phone numbers and custom identifiers [#hash-phone-numbers-and-custom-identifiers]
Phone numbers and [custom identifiers](/docs/concepts/custom-identifiers) travel to AbuseZero as keyed hashes. AbuseZero matches them across your claim history without ever receiving the raw value. Hashes use a secret that only your backend holds.
```ts
import { createCustomIdentifier, createPhoneHash } from "abusezero/server";
const secret = process.env.IDENTIFIER_HASH_SECRET!;
const phone_hash = createPhoneHash({ secret, keyVersion: "v1", e164: "+14155550123" });
const team = createCustomIdentifier({ secret, keyVersion: "v1", key: "team_id", value: "team_8812" });
await abusezero.evaluate(
{
...input,
identifiers: { ...input.identifiers, phone_hash, phone_verified: true, custom: [team] },
},
`trial:${claimId}`,
);
```
| Function | Input | Returns |
|---|---|---|
| `createPhoneHash` | `secret`, `keyVersion`, and `e164`: the number in E.164 format, such as `+14155550123` | `{ algorithm, key_version, value }`, sent as `identifiers.phone_hash` |
| `createCustomIdentifier` | `secret`, `keyVersion`, `key` and `value` | `{ key, value_hash }`, sent as an item of `identifiers.custom` |
- Use the same secret and key version on every call, so the same value always produces the same hash.
- `phone_hash` and `phone_verified` are sent together.
- [Register](/docs/api-reference/custom-identifiers#register-a-custom-identifier) a custom identifier's `key` before you send it. Keys are lowercase, start with a letter and are up to 64 characters. An evaluation carries up to 10 custom identifiers.
- Normalize values before hashing. `createPhoneHash` throws `INVALID_PHONE` for a number that isn't in E.164 format.
A server that doesn't run Node.js can compute the same values. See [Hash identifiers in other languages](/docs/guides/send-identifiers#hash-identifiers-in-other-languages). For which identifiers to send, see [Send identifiers](/docs/guides/send-identifiers) and [Custom identifiers](/docs/concepts/custom-identifiers).
## Verify AI agents [#verify-ai-agents]
`verifyAgentRequest()` turns an incoming request into an opaque `AgentEvidence` token. Send the token unchanged with the evaluation as `agent_evidence`. AbuseZero verifies it during that evaluation and recognizes any agent listed by [`GET /v1/agents`](/docs/api-reference/agents#list-recognized-agents), and your benefit's [automation settings](/docs/api-reference/benefits#automation) decide whether that agent may claim it.
```ts
import { verifyAgentRequest } from "abusezero/server";
const agent_evidence = await verifyAgentRequest(request, claimId, "https://app.example.com");
const evaluation = await abusezero.evaluate({ ...input, claim_id: claimId, agent_evidence }, `trial:${claimId}`);
```
| Argument | Description |
|---|---|
| `request` | The incoming request, as a standard Fetch API `Request`. Pass it before any proxy or middleware rewrites its headers. |
| `claimId` | The `claim_id` of the evaluation you send the result with. |
| `publicOrigin` | Your app's public HTTPS origin, such as `https://app.example.com`. Read it from your own configuration, never from a forwarded header. |
- Send the token unchanged, with the evaluation for the same claim, as soon as you have it. Tokens are short-lived, and a token made for another claim is refused with [`AGENT_EVIDENCE_SCOPE`](/docs/api-reference/errors#invalid-request).
- The token is opaque. Don't parse, store or modify it.
- Evidence from SDK versions before 0.11.0 is refused with [`AGENT_EVIDENCE_OUTDATED`](/docs/api-reference/errors#invalid-request).
- `verifyAgentRequest` throws when `publicOrigin` isn't a bare HTTPS origin, or when `claimId` is empty or longer than 128 characters.
See [Verify AI agents](/docs/guides/verify-ai-agents) and [Bots and AI agents](/docs/concepts/bots-and-ai-agents).
## Additional methods [#additional-methods]
Most other actions have a typed method, and [`call()`](#call-for-any-endpoint) reaches the rest. Methods that change data take an [idempotency key](/docs/api-reference#idempotency) as their last argument. List methods accept `limit` and `cursor`, as described in [pagination](/docs/api-reference#pagination). The same actions are available as [AbuseZero CLI commands](/docs/tools/cli#cli-command-reference) and [AbuseZero MCP server tools](/docs/tools/mcp#mcp-tools).
| Method | Endpoint | Reference |
|---|---|---|
| `workspaces()` | [`GET /v1/workspaces`](/docs/api-reference/workspaces#list-workspaces) | [Workspaces](/docs/api-reference/workspaces) |
| `evaluations(query)`, `getEvaluation(id)` | [`GET /v1/evaluations`](/docs/api-reference/evaluations#list-evaluations), [`GET /v1/evaluations/{id}`](/docs/api-reference/evaluations#retrieve-an-evaluation) | [Evaluations](/docs/api-reference/evaluations) |
| `evaluationGraph(id)` | [`GET /v1/evaluations/{id}/graph`](/docs/api-reference/evaluations#retrieve-an-evaluations-association-graph) | [Evaluations](/docs/api-reference/evaluations) |
| `evaluationEvidence(id)` | [`GET /v1/evaluations/{id}/evidence`](/docs/api-reference/evaluations#retrieve-an-evaluations-evidence) | [Evaluations](/docs/api-reference/evaluations) |
| `outcomes(query)`, `getOutcome(id)` | [`GET /v1/outcomes`](/docs/api-reference/outcomes#list-outcomes), [`GET /v1/outcomes/{id}`](/docs/api-reference/outcomes#retrieve-an-outcome) | [Outcomes](/docs/api-reference/outcomes) |
| `simulate(input)`, `replay(input)` | [`POST /v1/simulations`](/docs/api-reference/previews-simulations-replays#run-a-simulation), [`POST /v1/replays`](/docs/api-reference/previews-simulations-replays#run-a-replay) | [Previews, simulations and replays](/docs/api-reference/previews-simulations-replays) |
| `benefits(query)`, `getBenefit(id)`, `createBenefit(input, key)`, `updateBenefit(id, input, key)`, `deleteBenefit(id, key)` | `/v1/benefits` | [Benefits](/docs/api-reference/benefits) |
| `policies(query)`, `getPolicy(id)`, `createPolicy(input, key)`, `updatePolicy(id, input, key)`, `duplicatePolicy(id, input, key)`, `deletePolicy(id, key)` | `/v1/policies` | [Policies](/docs/api-reference/policies) |
| `accessLists(query)`, `addAccessListEntry(input, key)`, `addAccessListEntries(input, key)`, `deleteAccessListEntry(id, key)` | `/v1/access-lists` | [Allow and block lists](/docs/api-reference/access-lists) |
| `customIdentifiers(query)`, `registerCustomIdentifier(input, key)`, `deleteCustomIdentifier(id, key)` | `/v1/custom-identifiers` | [Custom identifiers](/docs/api-reference/custom-identifiers) |
| `imports(query)`, `getImport(id)`, `createCsvImport(key)`, `validateImport(id, key)`, `confirmImport(id, key)`, `cancelImport(id, key)` | `/v1/imports` | [Imports](/docs/api-reference/imports) |
| `exports(query)`, `requestExport(input, key)` | `/v1/exports` | [Exports](/docs/api-reference/exports) |
| `dataDeletions(query)`, `previewDataDeletion(input)`, `requestDataDeletion(input, key)` | `/v1/data-deletions` | [Data deletions](/docs/api-reference/data-deletions) |
| `analytics(days)` | [`GET /v1/analytics`](/docs/api-reference/analytics#read-analytics) | [Analytics](/docs/api-reference/analytics) |
| `requestLogs(query)` | [`GET /v1/request-logs`](/docs/api-reference/request-logs#list-request-logs) | [Request logs](/docs/api-reference/request-logs) |
| `auditLogs(query)` | [`GET /v1/audit-logs`](/docs/api-reference/audit-logs#list-audit-events) | [Audit logs](/docs/api-reference/audit-logs) |
| `browserCollection()`, `updateBrowserCollection(input, key)` | `/v1/browser-collection` | [Browser setup](/docs/api-reference/browser-collection) |
| `agents()` | [`GET /v1/agents`](/docs/api-reference/agents#list-recognized-agents) | [Agents](/docs/api-reference/agents) |
`addAccessListEntries()` adds up to 50 values to one [allow or block list](/docs/concepts/allow-and-block-lists) in a single call and returns a result for each value.
### `call()` for any endpoint [#call-for-any-endpoint]
`call(path, options)` sends a request to any endpoint with the client's credentials, retries and error handling. For `GET` requests, `input` becomes query parameters.
```ts
const versions = await abusezero.call("/v1/policies/pol_5b0e7c3d-2a91-4f6e-8d14-7c9a2e5f1b03/versions");
const conditions = await abusezero.call("/v1/policy-signals");
const download = await abusezero.call("/v1/exports/exp_0c4d8e2f-6a17-4b3c-9e5d-1f7a3b9c2d46/download");
```
| Option | Type | Description |
|---|---|---|
| `method` | `"GET"`, `"POST"`, `"PATCH"`, `"PUT"` or `"DELETE"` | Defaults to `"GET"`. |
| `input` | object | The request body, or the query parameters for `GET`. |
| `idempotencyKey` | string | Required for every write. |
| `timeoutMs` | number | The request budget for this call, in milliseconds. |
## Node.js SDK errors [#nodejs-sdk-errors]
Failed calls throw an `AbuseZeroError`.
| Property | Description |
|---|---|
| `code` | The error code, such as `CURSOR_INVALID`. |
| `message` | What to do next, written for a developer. |
| `status` | The HTTP status. `0` when no response arrived. |
| `requestId` | The [`X-Request-ID`](/docs/api-reference#request-ids) of the failed request, when there is one. Include it when you contact support. |
| `retryable` | `true` when the same call, with the same idempotency key, can succeed. |
```ts
import { AbuseZeroError } from "abusezero/server";
try {
await abusezero.createBenefit(input, `benefit:${input.key}`);
} catch (error) {
if (error instanceof AbuseZeroError && error.retryable) {
// Retry later with the same idempotency key.
}
throw error;
}
```
REST API error codes are listed in [REST API errors](/docs/api-reference/errors), and how to fix the common ones in [Troubleshooting](/docs/troubleshooting). The SDK adds these, raised before a request is sent or when a response can't be used:
| Code | Meaning |
|---|---|
| `CREDENTIAL_REQUIRED` | Pass exactly one of `key` and `accessToken`. |
| `INVALID_KEY` | Use a secret key (`sk_…`) or restricted key (`rk_…`). |
| `WORKSPACE_REQUIRED` | Pass `workspace` with an OAuth access token. |
| `OUTAGE_BEHAVIOR_REQUIRED` | Set `outage` to `"throw"`, `"allow"` or `"deny"`. |
| `INVALID_IDEMPOTENCY_KEY` | Use 1 to 128 printable characters with no spaces. |
| `INVALID_REQUEST` | The evaluation input doesn't match the API contract. The message names the field. |
| `INVALID_OUTCOME` | Check the outcome, and include a reason when you correct one. |
| `INVALID_IMPORT` | Send 1 to 1,000 valid granted outcomes. |
| `INVALID_PHONE` | Normalize the phone number to E.164 before hashing it. |
| `INVALID_IDENTIFIER_KEY` | Use a lowercase identifier key that starts with a letter. |
| `INVALID_IDENTIFIER_VALUE` | Normalize the identifier to a non-empty value before hashing it. |
| `INVALID_KEY_VERSION` | Use a printable key version with no spaces. |
| `DEADLINE_EXCEEDED` | The request budget ran out. Retry with the same idempotency key. |
| `TRANSPORT_ERROR` | AbuseZero couldn't be reached, or the response didn't arrive complete. Retry with the same idempotency key. |
| `INVALID_RESPONSE` | AbuseZero's response couldn't be used. Retry with the same idempotency key before acting on the claim. |