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
}erroris one of the fixed codes.error_descriptionis for humans and logs. It is not normative, and agents must not parse it for control flow.retryableis recommended. When it is absent, the agent infers retryability from the code.- Return errors as
application/json, not HTML, and do not return200with 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_assertionversusassertion_expired. Useassertion_expiredonly when the token is well formed and the expiry is the reason. A bad signature, an unexpectedalg, an unknownkidafter one refresh, or a malformed JWT isinvalid_assertion.issuer_not_allowedversusaudience_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_requestversusinvalid_assertion. A missingAuthorizationheader, a non-JSON body or a missing required header isinvalid_request. A present but unverifiable token isinvalid_assertion.assertion_replayedversusresolved. A repeatedjtifor the same agent, account and service is not an error; return success withresolution: "resolved".assertion_replayedis for a conflicting context.tos_hash_mismatchversusinvalid_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
McpToolErrorCodeSchemaenvelope (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 asinvalid_attestation,attestation_not_valid_at_signing,attestation_unavailable,signup_conflict, andrp_broker_unavailable. - Contract D.2 (
/api/broker/report-abuse) returnsunauthenticated_reportfor a legacy body andrp_broker_unavailablefor 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, andrate_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_descriptionis present but adds no control-flow semantics. - A repeated
jtifor the same account returns200/201withresolution: "resolved". - Error responses contain no secrets, tokens, stack traces or personal data.
- The body parses as JSON for every error.
Next
- Verify assertions: where each code comes from.
- Troubleshooting: symptom, cause and fix.
- Production checklist: error monitoring by code.