Send identifiers
Send every identifier you have with each evaluation, including payment methods, hashed phone numbers and your own custom identifiers.
Every identifier you send is checked against your workspace'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 and Custom identifiers.
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. |
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. Every field goes into evaluate() in the AbuseZero Node.js SDK, or the body of POST /v1/evaluations.
Send the user and email
Send the account's user ID and email with every claim. Set email_verified from your own records. AbuseZero doesn't verify ownership itself.
subject: {
external_id: "user_4821",
email: "jordan@example.com",
email_verified: true,
},Send the IP address
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. If you don't have the address, leave context.ip out. The evaluation still runs.
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
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. |
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
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. Send phone_verified with every phone hash.
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:
{
"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
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 in the AbuseZero REST API, with abusezero custom-identifiers register in the AbuseZero CLI, or with abusezero_register_custom_identifier on the AbuseZero MCP server.
await abusezero.registerCustomIdentifier({
key: "license_id",
key_version: "v1",
description: "Software license ID",
}, "register-license-id-v1");Then hash each value and send up to 10 custom identifiers with an evaluation:
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. Once registered, a custom identifier can also be used in your own policy rules.
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:
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
Run a claim with every identifier you send, then open the evaluation with GET /v1/evaluations/{id}. Its inputs object reports each input as received or missing.
{
"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 explains each input.
How is this guide?
Last updated on