AbuseZero

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 abusezero

The 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

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

OptionTypeRequiredDescription
keystringOne of key or accessTokenA secret key (sk_…) or restricted key (rk_…) from Developers. Publishable keys are rejected.
accessTokenstringOne of key or accessTokenAn OAuth access token. Requires workspace.
workspacestringWith accessTokenThe ws_… ID of the workspace this OAuth connection works in.
outage"throw", "allow" or "deny"YesWhat evaluate() does when AbuseZero can't be reached. See the outage option.
fetchfunctionNoSends 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.

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. For which identifiers to send and how, see Send identifiers.

Results

evaluate() returns one of three shapes. Check for status first, then source.

ShapeFieldsMeaning
Evaluatedsource: "abusezero", id, result, risk, reason, outcomeAbuseZero evaluated the claim. result is allow or deny.
Skippedsource: "abusezero", status: "skipped", reasonThe workspace couldn't run the evaluation, so there is no result.
Local outagesource: "local_outage", claim_id, result, errorAbuseZero 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 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 (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.
  • 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}`,
);
FieldTypeRequiredDescription
outcome"granted", "not_granted" or "reversed"YesWhat happened to the benefit.
reasonstring, 3 to 500 charactersWith not_granted and reversedWhy the outcome changed. A change to not_granted or reversed without a reason returns REASON_REQUIRED.
external_outcome_idstring, 1 to 128 printable charactersNoYour 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}`,
);
FunctionInputReturns
createPhoneHashsecret, keyVersion, and e164: the number in E.164 format, such as +14155550123{ algorithm, key_version, value }, sent as identifiers.phone_hash
createCustomIdentifiersecret, 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 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. 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}`);
ArgumentDescription
requestThe incoming request, as a standard Fetch API Request. Pass it before any proxy or middleware rewrites its headers.
claimIdThe claim_id of the evaluation you send the result with.
publicOriginYour 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.
  • verifyAgentRequest throws when publicOrigin isn't a bare HTTPS origin, or when claimId is 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.

MethodEndpointReference
workspaces()GET /v1/workspacesWorkspaces
evaluations(query), getEvaluation(id)GET /v1/evaluations, GET /v1/evaluations/{id}Evaluations
evaluationGraph(id)GET /v1/evaluations/{id}/graphEvaluations
evaluationEvidence(id)GET /v1/evaluations/{id}/evidenceEvaluations
outcomes(query), getOutcome(id)GET /v1/outcomes, GET /v1/outcomes/{id}Outcomes
simulate(input), replay(input)POST /v1/simulations, POST /v1/replaysPreviews, simulations and replays
benefits(query), getBenefit(id), createBenefit(input, key), updateBenefit(id, input, key), deleteBenefit(id, key)/v1/benefitsBenefits
policies(query), getPolicy(id), createPolicy(input, key), updatePolicy(id, input, key), duplicatePolicy(id, input, key), deletePolicy(id, key)/v1/policiesPolicies
accessLists(query), addAccessListEntry(input, key), addAccessListEntries(input, key), deleteAccessListEntry(id, key)/v1/access-listsAllow and block lists
customIdentifiers(query), registerCustomIdentifier(input, key), deleteCustomIdentifier(id, key)/v1/custom-identifiersCustom identifiers
imports(query), getImport(id), createCsvImport(key), validateImport(id, key), confirmImport(id, key), cancelImport(id, key)/v1/importsImports
exports(query), requestExport(input, key)/v1/exportsExports
dataDeletions(query), previewDataDeletion(input), requestDataDeletion(input, key)/v1/data-deletionsData deletions
analytics(days)GET /v1/analyticsAnalytics
requestLogs(query)GET /v1/request-logsRequest logs
auditLogs(query)GET /v1/audit-logsAudit logs
browserCollection(), updateBrowserCollection(input, key)/v1/browser-collectionBrowser setup
agents()GET /v1/agentsAgents

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");
OptionTypeDescription
method"GET", "POST", "PATCH", "PUT" or "DELETE"Defaults to "GET".
inputobjectThe request body, or the query parameters for GET.
idempotencyKeystringRequired for every write.
timeoutMsnumberThe request budget for this call, in milliseconds.

Node.js SDK errors

Failed calls throw an AbuseZeroError.

PropertyDescription
codeThe error code, such as CURSOR_INVALID.
messageWhat to do next, written for a developer.
statusThe HTTP status. 0 when no response arrived.
requestIdThe X-Request-ID of the failed request, when there is one. Include it when you contact support.
retryabletrue 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:

CodeMeaning
CREDENTIAL_REQUIREDPass exactly one of key and accessToken.
INVALID_KEYUse a secret key (sk_…) or restricted key (rk_…).
WORKSPACE_REQUIREDPass workspace with an OAuth access token.
OUTAGE_BEHAVIOR_REQUIREDSet outage to "throw", "allow" or "deny".
INVALID_IDEMPOTENCY_KEYUse 1 to 128 printable characters with no spaces.
INVALID_REQUESTThe evaluation input doesn't match the API contract. The message names the field.
INVALID_OUTCOMECheck the outcome, and include a reason when you correct one.
INVALID_IMPORTSend 1 to 1,000 valid granted outcomes.
INVALID_PHONENormalize the phone number to E.164 before hashing it.
INVALID_IDENTIFIER_KEYUse a lowercase identifier key that starts with a letter.
INVALID_IDENTIFIER_VALUENormalize the identifier to a non-empty value before hashing it.
INVALID_KEY_VERSIONUse a printable key version with no spaces.
DEADLINE_EXCEEDEDThe request budget ran out. Retry with the same idempotency key.
TRANSPORT_ERRORAbuseZero couldn't be reached, or the response didn't arrive complete. Retry with the same idempotency key.
INVALID_RESPONSEAbuseZero's response couldn't be used. Retry with the same idempotency key before acting on the claim.

How is this guide?

Last updated on

On this page