AbuseZero Node.js SDK
Use the AbuseZero Node.js SDK, abusezero/server, to evaluate claims, report outcomes and manage your workspace from your server.
The AbuseZero Node.js SDK, abusezero/server, is the typed Node.js client for the AbuseZero REST API. 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
npm install abusezeroThe Node.js SDK needs Node 22 or later. Keep it, and your secret key, on your server. For every entry point in the package, see SDKs, CLI and MCP server.
Create a Node.js SDK client
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
| Option | Type | Required | Description |
|---|---|---|---|
key | string | One of key or accessToken | A secret key (sk_…) or restricted key (rk_…) from Developers. Publishable keys are rejected. |
accessToken | string | One of key or accessToken | An OAuth access token. Requires workspace. |
workspace | string | With accessToken | The ws_… ID of the workspace this OAuth connection works in. |
outage | "throw", "allow" or "deny" | Yes | What evaluate() does when AbuseZero can't be reached. See 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
Call evaluate() before you grant the benefit. It calls POST /v1/evaluations. The second argument is the idempotency key.
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. For which identifiers to send and how, see Send identifiers.
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 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, and every reason code in Reason codes.
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 theAbuseZeroError, and your code decides."allow"or"deny":evaluate()returns alocal_outageresult with that decision and the error.
These are always thrown, whatever your setting:
- Rate limits (
429). Request volume can never switch protection off. - Bot-check errors: codes that start with
BOT_EVIDENCE_, such asBOT_EVIDENCE_REQUIREDandBOT_EVIDENCE_UNAVAILABLE. Collect a fresh device token withprepareBotObservation()and evaluate again. See Add a 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.
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. 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
previewEvaluation() takes the same input as evaluate() and calls POST /v1/evaluation-previews. It returns how AbuseZero would treat the claim, without writing claim history. It also returns which inputs AbuseZero received.
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.
Report an outcome
Tell AbuseZero what happened after an evaluation, so later evaluations are checked against it. outcome() calls POST /v1/evaluations/{id}/outcome. For outcome states and tracking, see Outcomes.
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.
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. See Import outcome history.
Hash phone numbers and custom identifiers
Phone numbers and 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.
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_hashandphone_verifiedare sent together.- Register a custom identifier's
keybefore 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.
createPhoneHashthrowsINVALID_PHONEfor 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. For which identifiers to send, see Send identifiers and Custom identifiers.
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, and your benefit's automation settings decide whether that agent may claim it.
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. - 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. verifyAgentRequestthrows whenpublicOriginisn't a bare HTTPS origin, or whenclaimIdis empty or longer than 128 characters.
See Verify AI agents and Bots and AI agents.
Additional methods
Most other actions have a typed method, and call() reaches the rest. Methods that change data take an idempotency key as their last argument. List methods accept limit and cursor, as described in pagination. The same actions are available as AbuseZero CLI commands and AbuseZero MCP server tools.
| Method | Endpoint | Reference |
|---|---|---|
workspaces() | GET /v1/workspaces | Workspaces |
evaluations(query), getEvaluation(id) | GET /v1/evaluations, GET /v1/evaluations/{id} | Evaluations |
evaluationGraph(id) | GET /v1/evaluations/{id}/graph | Evaluations |
evaluationEvidence(id) | GET /v1/evaluations/{id}/evidence | Evaluations |
outcomes(query), getOutcome(id) | GET /v1/outcomes, GET /v1/outcomes/{id} | Outcomes |
simulate(input), replay(input) | POST /v1/simulations, POST /v1/replays | Previews, simulations and replays |
benefits(query), getBenefit(id), createBenefit(input, key), updateBenefit(id, input, key), deleteBenefit(id, key) | /v1/benefits | Benefits |
policies(query), getPolicy(id), createPolicy(input, key), updatePolicy(id, input, key), duplicatePolicy(id, input, key), deletePolicy(id, key) | /v1/policies | Policies |
accessLists(query), addAccessListEntry(input, key), addAccessListEntries(input, key), deleteAccessListEntry(id, key) | /v1/access-lists | Allow and block lists |
customIdentifiers(query), registerCustomIdentifier(input, key), deleteCustomIdentifier(id, key) | /v1/custom-identifiers | Custom identifiers |
imports(query), getImport(id), createCsvImport(key), validateImport(id, key), confirmImport(id, key), cancelImport(id, key) | /v1/imports | Imports |
exports(query), requestExport(input, key) | /v1/exports | Exports |
dataDeletions(query), previewDataDeletion(input), requestDataDeletion(input, key) | /v1/data-deletions | Data deletions |
analytics(days) | GET /v1/analytics | Analytics |
requestLogs(query) | GET /v1/request-logs | Request logs |
auditLogs(query) | GET /v1/audit-logs | Audit logs |
browserCollection(), updateBrowserCollection(input, key) | /v1/browser-collection | Browser setup |
agents() | GET /v1/agents | Agents |
addAccessListEntries() adds up to 50 values to one allow or block list in a single call and returns a result for each value.
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.
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
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 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. |
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, and how to fix the common ones in 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. |
How is this guide?
Last updated on