Guides
Errors
Every non-2xx response carries a structured body: a stable snake_case error code an authoring loop can branch on, a human message, and — for some codes — the extra fields a client needs to act. Branch on error. Ignore fields you do not know.
The error body
1{2 "error": "stale_policy",3 "message": "The pinned policy is not the one in force.",4 "pinned": "pol_2s5479gmf7bhrsavd5awacb0fg",5 "active": "pol_9k18tzq4c7m2ha055xvbn31rjd"6}
The error code is never an internal type or variant name. It is a contract, and it is stable.
The code vocabulary
| Code | Status | Meaning |
|---|---|---|
invalid_transaction_id | 400 | The path segment is not a transaction id. |
invalid_account_id | 400 | The path segment is not an account id. |
invalid_usage_id | 400 | The path segment is not a usage-balance id. |
unknown_agent | 401 | The agent:// URI is not bound to a party. |
attestation | 403 | The PASETO attestation was rejected. |
unknown_transaction | 404 | No such transaction. |
unknown_account | 404 | No such account. |
unknown_usage | 404 | No such usage balance, or it is not yours. The two are indistinguishable. |
stale_policy | 409 | The pinned policy is not the one in force (pre-check). Carries pinned, active. |
policy_pin_mismatch | 409 | The policy changed between check and record. Nothing was written. Carries pinned, active. |
no_active_policy | 409 | No policy is in force at all. |
duplicate_binding | 409 | The agent:// URI is already bound. Carries uri. |
not_primary | 421 | Cluster mode: a mutation reached a non-primary. Carries primary_client_addr. |
invalid_agent_uri | 422 | The agent:// URI is malformed. Carries uri. |
capability_mapping | 422 | The action maps to no capability the attestation grants. Carries action. |
multi_currency_mandate_charge | 422 | A mandate-bound spend moved outflow in more than one currency. Carries currencies. |
substrate_rejected | 422 | The ledger refused it — unbalanced legs, unknown account, a breached ceiling. Carries kind: "validation". |
gateway_error | 422 | A gateway rejection with no more specific code. |
commit_unavailable | 503 | Cluster mode: the commit could not be confirmed. Carries reason. |
substrate_rejected | 507 | A ledger capacity bound was breached. Carries kind: "capacity". |
What to retry
Re-pin, then resubmit
stale_policy, policy_pin_mismatch, no_active_policy. Nothing was written. Re-read the policy in force and resubmit the same transaction id against it. Bound the loop at three attempts.
Retry as-is
commit_unavailable. The mutation may or may not have landed. Because the transaction id is the idempotency key, resubmitting is safe: a commit that did land replays its stored outcome. Alternatively, read the transaction and find out.
Surface, never follow
not_primary. The body names the primary in primary_client_addr, and the SDKs surface that address rather than silently re-sending to it. Following a redirect for a mutation is a decision about where your money goes, and that decision is yours to make.
Fix the request
invalid_agent_uri, capability_mapping, multi_currency_mandate_charge, substrate_rejected, unknown_agent, attestation, invalid_usage_id. These will fail identically on every retry.
Surface, and infer nothing
unknown_usage. A usage balance owned by another party, one opened with no owner, and one that was never opened all answer this same 404. That is deliberate: a usage id is never an existence oracle across a tenant boundary. It means you cannot read the balance, and nothing more. Note that a 403 on a usage read is a rejected attestation, never an ownership verdict.
Handle the codes
1use axorum_client::AxorumError;23match client.submit_pinned(draft).await {4 Ok(outcome) => outcome, // includes a recorded refusal5 Err(AxorumError::CommitUnavailable { .. }) => {6 // Idempotent by transaction id — safe to resubmit.7 client.submit_pinned(draft_again).await?8 }9 Err(AxorumError::NotPrimary { primary, .. }) => {10 // Surfaced, never auto-followed.11 return Err(anyhow!("re-send to the primary at {primary:?}"));12 }13 Err(error) => return Err(error.into()),14};
1{2 "error": "not_primary",3 "message": "This replica is not the primary; re-send the mutation to the named address.",4 "reason": "view change",5 "primary_client_addr": "10.0.4.12:8080"6}