Errors
Every error returns the same flat envelope. The HTTP status gives the class of problem, code tells you what to do about it, and request_id is what support will ask for. This page is the catalogue, grouped by where you will hit them.
The envelope
{
"code": "validation_failed",
"message": "One or more fields are invalid.",
"status": 422,
"request_id": "req_7d3c91f4a2",
"field_errors": [
{
"field": "country",
"code": "invalid_format",
"message": "Must be an ISO 3166-1 alpha-2 code."
}
]
}| Field | Always present | Notes |
|---|---|---|
code | Yes | Stable. This is what you branch on. |
message | Yes | Human-readable and subject to rewording. Never parse it. |
status | Yes | Mirrors the HTTP status, so the body is self-contained in logs. |
request_id | Yes | Log it. It is the single identifier support needs to trace a failure. |
field_errors | No | Present on validation_failed. An array of {field, code, message}. |
detail | No | Additional structured context for some errors. |
type | No | Present on some errors for finer classification. |
Codes are stable and safe to switch on. Messages are for humans and may be
reworded. And when something goes wrong that this page does not explain,
request_id is what turns a support conversation into a lookup.
Status classes
| Status | Class | General handling |
|---|---|---|
400 | The request itself is wrong — unparseable body, missing body, or nothing to do | Read the code — it names the rule. This is not the general field-validation status. |
401 | Authentication failed | Check which credential you sent, and whether it was rotated. |
403 | Authenticated, but not permitted | Not retryable as-is. Scope, role, or accreditation forbids it. |
404 | No such object, or not yours | Do not distinguish these in your own UI. |
409 | State conflict — the object is not in a state that allows this | Re-read the object. Retrying the same call will fail identically. |
422 | Validation failed — well-formed but invalid | field_errors[] names the fields. Surface each against its own field. |
429 | Rate limited | Back off with jitter. |
5xx | Nustro-side failure | Retry idempotent calls with backoff. Never retry a key-issuing call without an Idempotency-Key. |
Three statuses, three questions. 400 — was the request itself well formed?
422 — were the values valid? 409 — does the object’s current state allow it?
Field validation is 422 validation_failed carrying field_errors[] — never a
per-field 400. Build form handling against field_errors generically and you
will not need a new case each time a field is added.
It means your view of the object disagrees with Nustro’s. Retry loops on 409
generate load and never succeed — re-read, then decide. The one exception is
idempotency_key_processing, below.
Authentication and access
| Code | HTTP | Meaning | What to do |
|---|---|---|---|
invalid_credentials | 401 | The credential is wrong, unknown, or revoked | Check the key; it may have been rotated |
certificate_required | 401 | A runtime call arrived without an agent certificate | Agents authenticate with AEAP-Certificate, never a management key |
aeap_verification_failed | 401 | The certificate or proof did not verify, or the timestamp is outside the replay window | Check clock sync; refetch the issuer JWKS if a key rotated |
email_not_verified | 403 | The account exists but its email is unverified | Complete email verification |
account_suspended | 403 | The platform account is suspended | Contact support |
tier_exceeds_accreditation | 403 | The action requires a higher accreditation than the account holds | Not retryable; accreditation must be raised |
production_locked | 409 | A production action attempted while production is unavailable | Re-read your account status (GET /v1/platform) |
forbidden | 403 | Generic authorisation failure — the caller is not a party to this object | Check the identifiers in the path |
Account and onboarding
| Code | HTTP | Meaning | What to do |
|---|---|---|---|
email_exists | 409 | Registration attempted with an address that already has an account | Sign in or reset; do not create a second account |
email_already_in_use | 409 | An email change collided with an existing account | Choose a different address |
confirmation_mismatch | 422 | A typed confirmation did not match — account closure, for example | Field-level; correctable |
current_password_invalid | 422 | Password change with the wrong current password | Field-level; correctable |
kyb_in_progress | 409 | A verification submission is already under review | Wait for the decision rather than resubmitting |
verification_already_accredited | 409 | Verification submitted for an already accredited account | No action needed |
not_accredited | 409 | A production action attempted before accreditation | Wait for platform.verification_decided |
already_promoted | 409 | The account is already on production | Do not auto-retry. The key is not returned again; rotate instead |
key_not_provisioned | 409 | A production key requested before it exists | Re-read your account status (GET /v1/platform) |
close_blocked_escrow | 409 | Closure attempted with funded escrow | Release escrow first |
close_blocked_open_disputes | 409 | Closure attempted with open disputes | Cases must resolve before closure |
Document upload
| Code | HTTP | Meaning | What to do |
|---|---|---|---|
document_type_invalid | 422 | Not a recognised document type | Use a type from the required list |
file_too_large | 422 | The upload exceeds the size limit | Compress or split before retrying |
unsupported_file_type | 422 | The file format is not accepted | Convert to an accepted format |
Principals and agents
| Code | HTTP | Meaning | What to do |
|---|---|---|---|
external_ref_exists | 409 | A principal already exists with that external_ref | Deduplication is by external_ref — re-read rather than creating a duplicate |
customer_not_empty | 409 | A Customer deleted while it still holds principals | Reassign or ungroup them first — a label is never deleted out from under live principals |
tenant_principal_field_locked | 409 | A field on your auto-created tenant principal is not editable here — including grouping it under a Customer | Edit it in your company profile — one writer, not two |
invalid_status | 409 | The object is not in a status that permits this action — activating an agent that is not DRAFT, for example | Re-read the object. The request was valid; the state forbids it |
field_not_allowed_for_role | 422 | A field was set that does not apply to the agent’s economic role | Remove it — the role determines which pillars apply. Named in field_errors |
invalid_market_format | 422 | The market identifier is malformed | Check the jurisdiction-currency format. Named in field_errors |
role_not_permitted | 403 | The agent’s role does not permit this market configuration | Consumers have no markets to configure |
environment_mismatch | 409 | Sandbox and production objects were mixed in one operation | Check which key you sent, and the object’s environment |
agent_status_not_mutable | 409 | The agent’s lifecycle status does not allow this edit — identity and scope are not editable on a REVOKED or TERMINATED agent | Re-read the agent. Not retryable as-is |
use_activate_endpoint | 409 | DRAFT → ACTIVE was attempted through the status patch | Call POST /v1/agents/{did}/activate — that transition issues a keypair and certificate, so it has its own endpoint |
escrow_constrained_cannot_reactivate | 409 | Reactivation attempted while escrow is CONSTRAINED | Resolve the escrow deficiency first; the agent cannot return to ACTIVE under-funded |
no_changes_requested | 400 | An identity or scope patch contained no changes | Send only when something changed. This is a request problem, not a validation failure — hence 400 |
catalog_url_not_permitted | 422 | catalog_url was set on a CONSUMER agent | The field is PROVIDER/ENTERPRISE only. Do not offer it on consumer agents |
cert_tier_role_mismatch | 422 | The requested certificate tier does not exist for that economic role | Field-level, inside field_errors |
authorized_action_role_mismatch | 422 | An authorized action does not exist for that economic role | Field-level, inside field_errors |
Runtime and settlement
| Code | HTTP | Meaning | What to do |
|---|---|---|---|
spend_policy_violation | 403 | A ceiling, window, or counterparty floor refused the payment intent | Not retryable. The principal must adjust scope |
settlement_verification_failed | 409 | The on-chain settlement could not be verified against the intent — not found, wrong amount, or wrong network | Confirm the transaction on-chain before facilitating |
facilitation_not_found | 404 | No facilitation record for this reference | Delivery must follow a successful facilitate call |
confirmation_window_closed | 409 | Confirmation arrived after the window closed and the task auto-confirmed | The outcome stands; re-read the task |
Disputes
| Code | HTTP | Meaning | What to do |
|---|---|---|---|
invalid_reason | 422 | The reason is not an accepted value | Use not_delivered, not_as_described, unauthorized, or other |
invalid_disputed_amount | 422 | The disputed amount is malformed or exceeds the settled amount | Field-level — the error a user can fix themselves |
dispute_window_expired | 409 | The dispute window elapsed | Not recoverable; do not offer the form again |
final_sale | 409 | The transaction settled with a dispute window of zero | Not recoverable. Show final sale before the user tries |
dispute_exists | 409 | A dispute is already open on that transaction | Render the existing case instead of the form |
Idempotency and rate limits
| Code | HTTP | Meaning | What to do |
|---|---|---|---|
idempotency_key_reused | 422 | The same key was sent with a different request body | Use a fresh key for a genuinely different request |
idempotency_key_processing | 409 | The original request with this key is still in flight | Retry with the same key. Do not generate a new one |
rate_limited | 429 | Too many requests | Back off with jitter |
idempotency_key_processing is the one 409 you should retry — with the
same key. Generating a new key at that moment is how one timed-out request
becomes two objects.
Handling patterns
| Pattern | Applies to |
|---|---|
Map field_errors to your form | Every 422. Build it once, generically, rather than adding a case per field. |
| Re-read, then decide | Every 409 except idempotency_key_processing. Fetch the object, reconcile, then proceed differently or explain. |
| Do not auto-retry | already_promoted, and any failure on a call that returns a one-time secret. A blind retry destroys the credential rather than recovering it. |
| Translate for principals | spend_policy_violation, final_sale, dispute_window_expired, tier_exceeds_accreditation. Each has a concrete remedy — or a concrete reason there is none. Say which. |
Always log request_id | Everything. It is the difference between a support ticket and a lookup. |