Error codes
Every stable error and reason code AgentGate returns, from the browser SDK, /v1/siteverify, the gateway, the agent API and the console API.
Codes are stable and safe to branch on. Messages are for people and may change. Codes never reveal which rule matched or any score.
Browser SDK
Returned by /v1/browser/challenge, /v1/browser/instrument and /v1/browser/verify as {"error": "<code>", "message": "…"}, and surfaced by the SDK as AgentGate.Error.code (the rejection of execute(), or the argument of errorCallback). Refusals decided by the rule engine also carry a decisionId.
| Code | HTTP | Meaning | Fix |
|---|---|---|---|
site_invalid | 400 | unknown or malformed site key | check data-sitekey / siteKey |
action_invalid | 400 | the action is not [a-z0-9_]{1,64} or not configured for the site | configure the action on the site, or fix data-action |
origin_denied | 403 | the page's Origin is missing or not in the site's allowed origins, or the challenge was issued to another origin | add the origin to the site |
site_disabled | 403 | the site's account was deleted, so the key no longer issues challenges | use a site of an active account |
rate_limited | 429 | over the per-address limit (60 challenges or 120 verifications per minute) or the site's limit | wait for Retry-After (60 s) |
challenge_invalid | 400 | unknown challenge, or one issued for another action | call execute() again |
challenge_expired | 410 | the challenge is older than five minutes | call execute() again |
challenge_used | 409 | the challenge already produced a token | call execute() again |
too_many_attempts | 429 | the challenge's four verify attempts are used up | call execute() again |
challenge_failed | 403 | the evidence was declined, the step-up answer was invalid, or the visitor was still challenged after passing the check | let the person retry; check the decision in the dashboard |
schema_unsupported | 400 | the SDK's signal schema is not the server's | load the SDK from the same AgentGate you verify against |
payload_too_large | 413 | the request body is over the limit | a bug; report it |
bad_request | 400 | malformed JSON or fields | a bug; report it |
unavailable | 503 | a dependency failed | retry after Retry-After (5 s) |
Codes the SDK adds itself (no HTTP status):
| Code | Meaning |
|---|---|
timeout | timeout (default 30 s) or, once the check is shown, checkTimeout (default 5 min) passed |
aborted | the signal you passed was aborted |
canceled | the person closed the check with Escape or Cancel |
network | the request failed: offline, DNS, TLS, CORS or a CSP connect-src block |
error | anything else |
With the testing site key site_test_always_fail the SDK always fails with challenge_failed (HTTP 403). See Testing.
Server-side validation
POST /v1/siteverify answers {"success": false, "error": "<code>", "message": "…"}:
| Code | HTTP | Meaning |
|---|---|---|
invalid_request | 400 | malformed JSON (over 4 KiB included), missing token or expectedAction, or a requestId outside [A-Za-z0-9._:-]{1,128} |
invalid_credential | 401 | the backend secret is missing, malformed, revoked or another site's, or Authorization and secret differ |
invalid_token | 422 | not a token, bad signature, unknown site or key; also a test token sent with a real secret, or a real token sent with a test secret |
expired_token | 422 | past its expiry (default two minutes after issue) |
action_mismatch | 422 | issued for another action than expectedAction |
origin_mismatch | 422 | issued to another origin than expectedOrigin |
token_used | 409 | already redeemed, by another (or no) requestId |
unavailable | 503 | the store failed; retry with the same requestId |
The testing secrets return fixed answers for test tokens: ags_test_always_fail gives 422 invalid_token and ags_test_token_spent gives 409 token_used.
Gateway
GET /v1/gateway/check answers with a status and the headers X-AgentGate-Decision (allow, challenge or block) and X-AgentGate-Reason; denials also carry a small JSON body {"decision": "…", "reason": "…"}.
| Reason | Status | Meaning |
|---|---|---|
allowed | 204 | enforce mode allowed the request |
monitor | 204 | monitor mode; X-AgentGate-Would-Decision says what enforce would do |
receipt_required | 401 | a browser route and no token (or clearance) was sent |
invalid_token | 401 | the token is not valid for this site (a test token included) |
expired_token | 401 | the token expired |
token_used | 401 | the token was already spent |
action_mismatch | 401 | the token is for another action than the route's |
origin_mismatch | 401 | the token or clearance is for another origin |
invalid_clearance | 401 | the clearance is malformed, badly signed or for another site |
expired_clearance | 401 | the clearance expired |
agent_signature_invalid | 403 | a Web Bot Auth signature failed (bad, expired, replayed, or the body digest does not match) |
rate_limited | 403 | over the gateway rate budget or the agent's quota; the nginx example answers the client 429 |
payment_required | 403 | the action has a price and no acceptable offer was sent; the nginx example answers 402 with Crawler-Price |
payment_invalid | 403 | the price offer is malformed, conflicting or unsigned |
agent_denied | 403 | the site's agent policy does not allow this agent this action |
agent_unverified | 401 or 403 | a User-Agent claims an AI agent or crawler without a valid signature (challenge or block, per the site's policy) |
policy | 401 or 403 | any other rule (the rule name is never exposed) |
invalid_credential | 403 | the caller's backend secret is wrong; nothing was evaluated |
invalid_request | 403 | the adapter's X-Original-* headers are missing or malformed |
service_unavailable | 503, or 204 in monitor mode | a dependency failed; enforce mode fails closed |
The gateway never answers 429 itself. The complete header contract is on Gateway overview.
Agents
/agent/submit and gateway routes answer agents with plain HTTP statuses:
| Status | Meaning |
|---|---|
| 400 | invalid request body, or a malformed price offer (crawler-error, below) |
| 401 | no valid Web Bot Auth signature or agent key ("invalid agent credential") |
| 402 | payment required: see crawler-price and crawler-error |
| 403 | the site's policy does not permit this agent this action ("not permitted"), or a challenge; a challenge for a verified agent carries a handoff object |
| 409 | the submission id was already used for different content |
| 429 | over the agent's per-site quota; Retry-After: 60 |
| 503 | a dependency failed |
Pay-per-crawl errors, in the crawler-error header:
crawler-error | Status | Meaning |
|---|---|---|
MissingCrawlerPrice | 402 | no offer sent |
InvalidCrawlerExactPrice | 402 | the exact offer differs from the price (value or currency) |
InvalidCrawlerMaxPrice | 402 | the maximum is below the price |
InvalidCrawlerPriceValue | 400 | not CUR 0.00 |
ConflictingPriceHeaders | 400 | both offer headers sent |
StrongAuthRequired | 400 or 402 | the offer is not covered by a signature, or the agent is not verified |
Human handoff polling (GET /v1/handoff/{id}): 404 when anyone other than the session or agent that received the handoff polls, 410 once it is redeemed or expired. See Human handoff.
Console and admin API
/v1/console/*, /v1/account/* and /v1/admin/* answer {"error": "<code>", "message": "…"}. The codes you are likely to meet:
| Code | HTTP | Meaning |
|---|---|---|
unauthorized | 401 | not signed in, or a wrong operator credential |
session_expired | 401 | the session ended (console sessions last 12 h, or 2 h without use); sign in again |
invalid_credentials | 401 | wrong email or password |
mfa_required | 401 | the password was right; enter the two-factor code (POST /v1/account/login/mfa) |
invalid_code | 401 | wrong or already used two-factor or recovery code |
invalid_token | 401 | the API token is invalid, revoked or expired |
csrf | 403 | the X-CSRF-Token header is missing or wrong |
forbidden | 403 | your role in the account, or the API token's scopes, lack the permission message names |
account_suspended | 403 | the account is suspended: console and API are closed, its sites keep protecting |
user_suspended | 403 | your user is suspended |
no_account | 403 | you belong to no account; create one or ask for an invitation |
token_not_allowed | 403 | API tokens work only on /v1/console |
mfa_setup_required | 403 | the account requires two-factor authentication; turn it on first |
invalid_password | 403 | the current password is wrong (password change, turning 2FA off) |
cross_origin | 403 | the request came from another site |
browser_check_failed | 403 | the request-access form's browser check did not pass |
not_found, site_not_found | 404 | no such object, or not yours |
invalid_link | 404 | an invite or reset link is invalid, used or expired |
invalid_request, invalid_site, invalid_filter, invalid_limit, invalid_range, invalid_cursor | 400 | the request or a parameter is malformed; message says which |
forbidden_field, immutable_field | 400 | a field the server sets, or that cannot change (siteKey, customer) |
weak_password | 400 | the password does not meet the rules |
site_limit, credential_limit, member_limit | 409 | the account's limits (default 5 sites, 3 live secrets per site, 10 members) |
account_limit | 409 | a user can own at most 3 accounts |
not_member | 404 | switching to an account you do not belong to |
already_member, already_active | 409 | the person is already a member, or has already set a password |
last_owner | 409 | every account keeps an owner; make someone else owner first |
sites_enforcing | 409 | switch every site to monitor mode before deleting the account |
confirm_mismatch | 400 | type the account name exactly to confirm deletion |
site_key_taken | 409 | the site key exists |
revision_conflict | 409 | the rules changed since you loaded them; reload and retry |
token_limit | 409 | at most 25 live API tokens per account |
mfa_already_enabled, mfa_not_enabled, mfa_not_pending, mfa_required_by_account | 409 | the two-factor state does not allow that step |
rate_limited | 429 | too many attempts; wait |
store_unavailable | 503 | the database did not answer; retry |
The full contracts are in the API reference.