skip to content

Stable error codes

Return the fixed v1 error envelope, keep it distinct from MCP and broker codes, and map each failed assertion check to a status and retryability.

awaiting reviewupdated view markdown ->
for humans

Goal: every rejected signup returns a JSON envelope with one fixed v1 code, so an agent can act without reading prose.

The normative table is spec section 7.2. This page is the implementation step and the recorded output from the samples.

The envelope

{
  "error": "tos_hash_mismatch",
  "error_description": "Accepted ToS hash does not match the published terms",
  "retryable": false
}
  • error is one of the fixed codes.
  • error_description is for humans and logs. It is not normative, and agents must not parse it for control flow.
  • retryable is recommended. When it is absent, the agent infers retryability from the code.
  • Return errors as application/json, not HTML, and do not return 200 with an error body.
  • Do not include stack traces, internal hostnames, assertion bodies or personal data in the response.

The fixed codes

Code HTTP Retryable Agent action
invalid_request 400 no Correct the request; a blind retry fails again.
invalid_assertion 401 no Request a fresh assertion.
issuer_not_allowed 403 no Stop here: this broker is not on your list.
audience_mismatch 403 no Re-read discovery and ask for an assertion aimed at your audience.
assertion_expired 401 no Request a fresh assertion.
assertion_replayed 409 no Request a fresh assertion; the old id is spent.
tos_hash_mismatch 409 no Stop and redo consent: discover, accept and assert again.
insufficient_scope 403 no Send the human back through delegation; do not proceed.
human_unverified 403 no Finish human verification before retrying.
rate_limited 429 yes Wait for the advertised delay, then retry.

The codes and statuses are fixed for v1. Do not rename them, remap them or replace them with your own vocabulary in the core flow. If you add an implementation-specific code outside the core flow, document it, keep it distinguishable from these values, and still return the fixed code for the corresponding core failure.

Recorded responses

These were produced by the generic HTTP sample on 2026-10-21 with synthetic assertions.

Expired token, past the 60-second skew:

HTTP 401
{"error":"assertion_expired","error_description":"Signature or JOSE header verification failed","retryable":false}

Incorrectly signed or malformed token:

HTTP 401
{"error":"invalid_assertion","error_description":"Signature or JOSE header verification failed","retryable":false}

Assertion for another audience:

HTTP 403
{"error":"audience_mismatch","error_description":"Assertion audience does not match","retryable":false}

Human owner not verified:

HTTP 403
{"error":"human_unverified","error_description":"The human owner is not verified","retryable":false}

Stale terms hash:

HTTP 409
{"error":"tos_hash_mismatch","error_description":"Accepted ToS hash does not match the published terms","retryable":false}

Issuer not in the allowlist, recorded from the Worker sample:

HTTP 403
{"error":"issuer_not_allowed","error_description":"Issuer not allowed","retryable":false}

Choosing between similar codes

  • invalid_assertion versus assertion_expired. Use assertion_expired only when the token is well formed and the expiry is the reason. A bad signature, an unexpected alg, an unknown kid after one refresh, or a malformed JWT is invalid_assertion.
  • issuer_not_allowed versus audience_mismatch. The issuer check comes first. If both fail, report the issuer; the agent should not try another audience for a broker you do not accept.
  • invalid_request versus invalid_assertion. A missing Authorization header, a non-JSON body or a missing required header is invalid_request. A present but unverifiable token is invalid_assertion.
  • assertion_replayed versus resolved. A repeated jti for the same agent, account and service is not an error; return success with resolution: "resolved". assertion_replayed is for a conflicting context.
  • tos_hash_mismatch versus invalid_assertion. The signature is valid but the accepted terms are not the current ones. That is a consent problem, not a token problem.

Retry behavior

Only rate_limited is retryable in the core table. An agent should back off on 429 and must not loop on the others:

  • invalid_request: fix the request.
  • invalid_assertion, assertion_expired, assertion_replayed: request a new assertion from the broker.
  • issuer_not_allowed, audience_mismatch, insufficient_scope, human_unverified: do not retry unchanged; the precondition is missing.
  • tos_hash_mismatch: re-discover and re-accept. This is a hard stop until the agent accepts the current hash.

If you send Retry-After on 429, set retryable: true and keep the delay honest.

Keep the families separate

This table is only for your POST endpoints.agent_signup response. Do not reuse these codes for other surfaces:

  • MCP tool errors use the broker's McpToolErrorCodeSchema envelope (rp_unverified, rp_attestation_invalid, discovery_source_not_allowed, and the rest). They are returned inside an MCP result, not by your endpoint.
  • Contract F (/api/broker/signup-receipt) returns broker codes such as invalid_attestation, attestation_not_valid_at_signing, attestation_unavailable, signup_conflict, and rp_broker_unavailable.
  • Contract D.2 (/api/broker/report-abuse) returns unauthenticated_report for a legacy body and rp_broker_unavailable for a transient outage. It does not use your signup codes.
  • Provider API routes use invalid_api_key, provider_suspended, provider_not_eligible, the key/challenge/PoP errors, and rate_limited.

A v1.2 receipt has no exp. Do not reject a receipt because a legacy exp passed, and do not expect a receipt_expired code: validity is the historical [attestation.iat, attestation.exp] window. A provider that adds its own implementation code outside this core flow must keep it distinguishable and still return the fixed code for the corresponding core failure.

Verify your work

  • Every failure path returns one of the fixed codes.
  • The HTTP status matches the table.
  • error_description is present but adds no control-flow semantics.
  • A repeated jti for the same account returns 200/201 with resolution: "resolved".
  • Error responses contain no secrets, tokens, stack traces or personal data.
  • The body parses as JSON for every error.

Next

sections

type to search.

up/down navigateenter selectesc close