Errors
Every error is the same shape:
{ "error": { "code": "TokenNotFound", "message": "Unknown or revoked secret API key" } }
Branch on code. The message is written for a human reading a log and may
change; the code is the contract. A few validation errors from the agent's own
endpoints (identification, the pulse, the credential lookup) also carry an
issues array naming the fields that failed.
Codes
The Server API, the assess family and the endpoints the agent and the mobile SDKs call:
| Code | Status | Means | Do |
|---|---|---|---|
TokenNotFound |
403 | The key is unknown, revoked, or the wrong kind for this endpoint — a management key sent to the Server API is unknown here | Check you are sending a secret key to the Server API and the public key from the agent and the mobile SDKs. The Management API answers Unauthorized instead |
CardNumberRefused |
400 | A card field held a card number (/assess, /assess/outcome) |
Send your processor's card fingerprint or a keyed hash of the number instead — see protecting checkout. The number is not echoed back. |
KeyNotPermitted |
403 | The secret key was created for other uses — an assess-only key reading events or visitors, say | Use a key allowed to do this; an edge's key should stay assess-only |
OriginNotAllowed |
403 | The request's origin is not in your allowed origins | Add it in Settings, or clear the list to allow every origin |
DomainMismatch |
403 | The request arrived on a verified custom domain owned by a different organization | Use a key from the organization that owns the host |
RequestFiltered |
403 | A request filter rejected an identification | Expected, if you configured it. Not billed, no event stored. (On /assess a filter is a block verdict, not an error.) |
TrialEnded, TrialCallsUsed, SubscriptionEnded |
402 | An identification or assessment from an organization whose trial ended, whose trial calls are used, or whose subscription ended (billing) | An owner subscribes on the dashboard's Billing page. Meanwhile the agent earns no receipt and the edge and server SDKs fail open: the site serves, unprotected |
TooManyRequests |
429 | Rate limit exceeded — identification, challenges, the pulse, integrity reports, the waiting room, App Attest; and the Management API's expensive calls past their allowance (usage, fitting a model, reading calibration) | Back off; Retry-After says how long |
RateLimited |
429 | Too many breached-credential lookups from one address (60 a minute) | Back off; this one sends no Retry-After |
InvalidPayload |
400 | The body did not match the expected shape, or a field in it is unusable: an ip that is not an address, a phone not in E.164 form, a url that is not absolute, a PATCH that names nothing to change, a sealed payload that fails its checks |
Read the message, which names the field; for an identification, check the agent version |
InvalidRequest |
400 | GET /related-visitors without visitor_id |
Pass it |
InvalidJson |
400 | The body was empty, not JSON, or nested more than 32 levels deep | |
PayloadTooLarge |
413 | The body was over the endpoint's limit: 1 MB for an identification and for the Management API; 256 KB for /assess, /assess/outcome and /assess/receipts; 512 KB for a browser's policy reports (/csp/{key}, which answers 204 whatever it holds); 64 KB for everything else. An identification is also held to 10,000 values (each array element and object member counts one) |
An identification is normally under 50 KB and some 600 values; look for something large in tag or a page field you added |
EventNotFound |
404 | No such event in this key's environment | See Reading a 404 |
VisitorNotFound |
404 | No such visitor in this key's environment | See Reading a 404 |
TokenExpired |
401 | The session token sent to the pulse or the waiting room has expired | Identify again for a fresh one (the agent drops the expired token for you) |
TokenInvalid |
403 | The session token was rejected: malformed, a bad signature, or minted for another organization or environment | Identify again; do not retry the same token |
SessionRevoked |
403 | A revoked session tried to join a waiting room | Expected while the revocation lasts |
QueueDenied |
403 | A session convicted as automation, or whose verdict was block, tried to join a waiting room |
Expected. See waiting rooms |
AddressRequired |
403 | A waiting room that binds admissions to the visitor's network (bindIp) was entered with no client address the server could see |
Send the client's address the way your proxy setup says, or turn bindIp off |
RoomNotFound |
404 | No waiting room with that slug in the key's environment | Check the slug and the key's environment |
TicketInvalid |
403 | A queue poll's ticket is not a queueToken this organization issued |
Send the queueToken from the enter answer, not the ticket number |
TicketGone |
410 | The spot in line has expired: abandoned for a day, an admission that lapsed, a revoked session, a closed room | Enter again |
ChallengeInvalid |
400 | An App Attest registration's challenge is unknown, already used or expired | Fetch a new challenge |
AppIdsNotConfigured |
409 | App Attest registration in an environment with no Apple app ids | Set appleAppIds with PATCH /environments/{id} |
AttestationRejected |
400 | Apple's attestation failed verification, or its key id is not the one sent | Do not trust the device's key |
The Management API has codes of its own:
| Code | Status | Means | Do |
|---|---|---|---|
Unauthorized |
401 | No key, or not a live management key (a secret key is not one) | Send a management key |
Forbidden |
403 | The key lacks the scope (the message names it), tries to grant scopes it does not hold, or tries to act on an owner or an admin or to grant ownership | Add the scope, or do it from the dashboard |
BadRequest |
400 | A field failed validation; the body is not JSON, not an object, or nested more than 32 levels deep; a required ?environment= is missing; the body and ?environment= name different environments |
Read the message, which names the field |
NotFound |
404 | No such record — or environment, for ?environment= — in this organization |
|
Conflict |
409 | The change would break a rule: a name already taken, the plan's subdomain limit, the last owner, the default environment, a key revoking itself, or one more than an environment holds (20 webhooks, 10 log streams, 1,000 request filters) or an organization (50 environments, 200 keys not revoked, 20 proxy secrets, 10 encryption keys, 10 SCIM tokens) | Read the message |
SeatLimitReached |
409 | An invitation beyond the plan's seats | Remove a member or change plan |
RuleLimitReached |
409 | A rule beyond the 500 an environment holds | Delete rules no longer needed (let-through rules that stopped matching among them) |
ThreatFeedFull |
409 | Threat-feed entries beyond the 50,000 live ones an environment holds | Delete entries no longer needed, or add them with a ttlHours so they lapse |
PayloadTooLarge |
413 | The body was over 1 MB | |
InternalError |
500 | Something failed on our side | Retry later |
404 (EventNotFound, VisitorNotFound; NotFound on the Management API) is
returned for a record that does not exist —
or that belongs to another environment or organization. That is deliberate:
any other answer confirms the record exists.
Reading a 404
try {
event = await antibot.getEvent(requestId)
} catch (e) {
if (e.status === 404) {
// Either it never existed, or this key cannot see it.
// Most often: a staging key reading a production request id.
}
}
If you are getting 404s for events you can see in the dashboard, check which environment your key belongs to.
Sealed results
unsealEventResponse throws SealedResultError for a payload that is too
short, has the wrong header, or cannot be opened by any supplied key — and when
it is given no keys at all.
A wrong key and a tampered payload are not distinguished. Both mean do not trust this result, and saying which would tell an attacker whether they had guessed the key. Treat the throw as a denial, not a retry.
Webhooks
Your endpoint's failures, not ours. Anything outside 2xx, or slower than three seconds, counts as a failure and is retried once after five minutes with the same event id. See Webhooks.
Signals that are absent rather than false
Not an error, but the same class of mistake:
{ "result": false, "unavailable": true, "reason": "ipPseudonymized" }
unavailable: true means the signal could not be computed. Check it before you
read result, or you will treat "no opinion" as "no detection". See
Privacy.
Agent errors
The browser agent throws if apiKey is missing. Network failures reject the
promise from get(), and so does any error answer from the API
(OriginNotAllowed, TooManyRequests, …), with the status and body in the
message.
Identification failing should not take your page down:
let result = null
try {
result = await agent.get({ ignoreCache: true })
} catch {
// Decide what an unidentified visitor means for this action.
// Usually: continue, and let the server treat it as unknown.
}
Whatever you do, do not fail open on the server side because the client did not send a request id. An absent identification is not a clean one.