Error Reference
Every error code the CodeSpar API can return, with HTTP status, the response shape, when it fires, and the fix. Aggregated across sessions, projects, connections, triggers, wallets, and auth surfaces.
Error Reference
The error envelope
Most /v1 endpoints return errors in a nested envelope:
{
"error": {
"code": "invalid_body",
"message": "request body did not match the expected schema",
"details": { "issues": [] }
},
"request_id": "req_a1b2c3d4"
}error.codeis a stable machine-readable string. Safe to branch on in your code.error.messageis a human-readable explanation. Surface to users with caution.error.detailsis optional and per-error. Validation errors put the schema issues here.request_idmatches theX-Request-Idresponse header. Include it in support tickets.
The HTTP status is on the response itself, not in the body. Branch on error.code, not on the message text.
Exceptions to the envelope
A few surfaces return a different shape. Detect them by checking whether error is a string or an object:
| Surface | Status | Shape |
|---|---|---|
| Missing, malformed, or revoked API key | 401 | Bare: {"error": "unauthorized"}. No message, no request_id. |
| Key lacks a required scope | 403 | Flat: {"error": "forbidden", "message": "...", "status": 403} |
| Caller's account role is too low | 403 | Flat: {"error": "insufficient_role", "required": "admin"} |
| Policy denial (including budget rules) | 403 | Flat: {"reason": "budget_exceeded", "ruleType": "...", "ruleId": "..."}, plus approval_id and expires_at when an approval was created |
| Project resolution failure | 500 | Bare: {"error": "project_resolution_failed"} |
| Session quota and rate limits | 403 / 429 | Flat: {"error": "quota_exceeded", ...} or {"error": "rate_limited", ...} with limit fields at the top level |
| Test-mode mock errors | 403 / 422 | Flat: {"code": "mocks_not_permitted", "message": "..."} style, with code at the top level |
| Some legacy routes | 404 | Bare: {"error": "not_found"} |
When the body has no request_id, take it from the X-Request-Id response header.
Error codes
Grouped by area. Each API reference page (/docs/api/*) also lists the errors specific to its surface. Wallet-specific codes (ledger conflicts, funding-source bindings, balance invariants) are also listed on the Wallets API page.
The tables below are extracted from the backend's two error-emitting shapes — the
apiError() helper (the standard envelope) and the routes that answer with
reply.code(4xx) and a flat body — and cover the routes an API key or a public
gateway can reach: 174 codes, which is every row below. What is deliberately
not listed: operator-only subtrees (admin provisioning, the engine console,
service-auth surfaces like billing and api-keys) and provider webhook receivers,
whose codes no caller can trigger; and any error a provider returns, which
reaches you wrapped as provider_error. Codes are grouped by the surface that
emits them, so the same code can appear once with the status its call site
actually sends.
Validation
| Code | Status | When it fires |
|---|---|---|
invalid_body | 400 | The request body did not match the endpoint's schema. details.issues lists the exact field problems. Covers missing fields, wrong types, out-of-range values, and disallowed or reserved project slugs. |
invalid_query | 400 | Query parameters did not match the endpoint's schema. details.issues lists the problems. |
Fix: read details.issues and correct the named fields. The SDK's schemas catch most of these before the request is sent.
Auth and scopes
| Code | Status | When it fires |
|---|---|---|
unauthorized | 401 | No Authorization: Bearer header, the key is malformed, or it was revoked. Bare-string shape (see exceptions above). |
forbidden | 403 | The key is valid but does not have the scope this route requires. The message names the missing scope. Flat shape. |
budget_exceeded | 403 | A policy budget rule denied the call. Appears as reason in the flat policy-denial body, not as error.code. |
insufficient_role | 403 | The caller's account role is below what the route requires (admin on most administrative writes). The most common 403 in the product. Two shapes: the requireRole guard answers flat — {"error": "insufficient_role", "required": "admin"} — while a handful of route handlers answer it in the standard envelope. It also fires when the acting user is not forwarded at all on a service-auth call (x-codespar-user header required). |
project_resolution_failed | 500 | The key authenticated but no default project could be resolved for the account. Bare-string shape. Open a support ticket. |
Fix for 401: verify the key in the dashboard under Settings, API Keys and confirm it is active and sent as Authorization: Bearer csk_....
401 on api-keys, usage, billing, team and project members
Those five surfaces are registered in the API's service-auth subtree. A csk_
Bearer key is not read there at all, so any Bearer call to them answers
401 unauthorized — not 403, not 404. No key we can issue changes that: the
credential for that subtree is CodeSpar's own platform secret and never leaves
our infrastructure. Reach those surfaces through the dashboard. See the
Billing page.
Fix for 403 forbidden: create a key with the required scope, or widen the existing key's scopes.
Projects
| Code | Status | When it fires |
|---|---|---|
slug_conflict | 400 | Project create or update with a slug another project in the account already uses. Note this is a 400, not a 409. |
cannot_delete_default | 409 | Delete on the default project. Promote another project to default first. |
cannot_delete_last_project | 409 | Delete on the only project in the account. Create another project before deleting this one. |
cannot_delete_with_consumer_records | 409 | Delete on a project that still owns consumer records. Move or close them first. |
not_found | 404 | The project id does not exist or belongs to a different account. Also returned by project-settings and by the member endpoints when there is no override row for the named user. |
user_not_in_org | 404 | A project-member write named a user_id that is not a member of this account. |
cannot_override_owner | 409 | A project-member write targeted the account owner. An owner's role is never narrowed per project. |
Connections and providers
| Code | Status | When it fires |
|---|---|---|
not_connected | 424 | Connection verification found no active connection for this project and server. Connect the server first, then retry. |
server_unknown | 404 | The server_id is not in the catalog. |
not_api_key_server | 400 | The server authenticates with OAuth; credentials cannot be posted to the API-key endpoint. Use POST /v1/connect/start. |
credential_invalid | 400 | The supplied secret could not produce an HMAC — the credential is malformed for this server's signing scheme. |
invalid_secret_shape | 400 | The secret payload does not match the shape the server's auth scheme declares. |
path_secret_keys_mismatch | 400 | The secret object's keys do not match the server's declared path-secret refs. |
no_user_fields | 400 | The connection declares no user-editable fields; re-connect through the modal to change credentials. |
fields_not_user_editable | 400 | A PATCH named keys outside the server's requires_user_fields. The message lists the rejected keys. |
provider_has_no_inbound_webhooks | 400 | Webhook-secret rotation on a server that does not support inbound webhooks in this API. |
user_id_required | 400 | The connection owner could not be determined. Pass user_id, or forward x-codespar-user. |
metadata_update_failed | 409 | The connection row disappeared between the read and the write. Re-read and retry. |
vault_unavailable | 503 | The credential store could not persist or read a secret. Nothing was written; retry with backoff. |
provider_error | 502 | The upstream provider returned an error or an unexpected response. Retriable; retry with backoff. |
Sandbox subaccount provisioning
| Code | Status | When it fires |
|---|---|---|
not_test_environment | 422 | The provisioning lane is sandbox-only. A live-environment project cannot be engine-provisioned. |
not_awaiting_verification | 409 | A verification code was submitted for a run that is not waiting on one. |
run_stale | 410 | The paused provisioning run expired. Start a new one. |
insufficient_role | 403 | The operator-internal status/revoke steps require service auth, not a Bearer key. |
Triggers
| Code | Status | When it fires |
|---|---|---|
unknown_server | 400 | Trigger create referenced a server_id that is not in the catalog. |
trigger_not_active | 409 | Test-fire or event delivery on a trigger that is not active. Activate the trigger first. |
trigger_not_found | 404 | An event replay named a trigger that does not exist in this account. |
trigger_event_mismatch | 400 | The replay target subscribes to a different event type than the event being replayed. |
invalid_delivery_id | 400 | A delivery id that is not a positive integer was passed to a delivery endpoint. |
Consumer wallet and mandates
| Code | Status | When it fires |
|---|---|---|
mandate_not_found | 403 / 404 | The mandate id does not exist or is not visible to this account. |
mandate_not_active | 409 | The mandate's status forbids the operation (for example binding a card to a revoked mandate). |
mandate_already_revoked | 409 | Revoke on a mandate that is already revoked. |
mandate_already_expired | 410 | The mandate's expires_at has passed. Mint a new one. |
mandate_agent_key_invalid | 403 | The agent key that signed the mandate is revoked, or was not valid when the mandate was issued. |
mandate_purpose_required | 400 | The mandate carries several purposes; name which one this spend is charged against. |
invalid_purpose_encoding | 400 | A purpose string contains non-printable or non-ASCII characters. |
expires_at_in_past | 400 | expires_at must be a future UNIX timestamp. |
missing_required_field | 400 | A required field is absent from the mandate or facilitator request body. |
invalid_transition | 409 | The requested action is not legal from the mandate's current status. |
transition_conflict | 409 | The mandate's status changed concurrently. Re-read and retry. |
same_slot_transfer | 422 | Wallet transfer where from_currency and to_currency are the same slot. Nothing to move. |
currency_not_authorized | 422 | Wallet transfer names a currency the wallet has no slot for. Also fires with status 403 on a mandate spend whose payee resolves to a currency the mandate has no slot for. |
unsupported_transfer_route | 422 | No wired route between the two currencies. No FX is ever guessed. |
not_wired | 501 | The transfer route exists in the plan but the execution rail is not wired yet, for example certain cross-currency pairs. |
Wallet ledger and funding sources
| Code | Status | When it fires |
|---|---|---|
wallet_not_active | 409 | The wallet's status is not active; only active wallets accept ledger entries. |
ledger_conflict | 409 | The entry violated a unique constraint other than the idempotency one — a replay of the same idempotency key is handled and returns the existing row with 200, so this signals a genuine conflict. |
balance_constraint_violation | 409 | The entry would violate a wallet balance invariant — a debit past the available balance, for example. Nothing is written. |
connection_not_found | 404 | The connection named as a funding source is not in this account. |
connection_not_active | 409 | The connection exists but its status is not connected. |
funding_rail_mismatch | 409 | The provider does not settle in the slot's currency. Bind a provider whose rail matches. |
funding_source_conflict | 409 | That connection is already bound to a wallet for this currency. |
funding_source_not_found | 404 | The funding-source binding does not exist. |
no_onchain_wallet | 409 | On-chain withdrawal is only available for consumer-scoped wallets. |
no_onchain_address | 409 | On-chain receive is only available for consumer-scoped USDC wallets. |
no_custody_view | 409 | The custody comparison is only available for on-chain USDC wallets. |
anomaly_not_found | 404 | The recon anomaly id does not exist, or is already resolved. |
Consumer funding and receipts
| Code | Status | When it fires |
|---|---|---|
no_celcoin_account | 422 | The consumer has no active BaaS funding source and no account was supplied. Run KYC onboarding first. |
sandbox_funding_not_permitted | 403 | Sandbox funding was requested with a live-environment key. Use a csk_test_ key on a test-environment project. |
transaction_not_found | 404 | The onramp transaction does not exist, or the onramp provider is not connected. |
receipt_not_found | 404 | No agentic receipt exists for that id. |
Card issuing
| Code | Status | When it fires |
|---|---|---|
issuer_corridor_unsupported | 400 | No enabled issuer serves the (country, currency) corridor. Issuers are enabled per deployment by an infrastructure flag; see codespar_issue. |
issuer_controls_unsupported | 400 | The mandate's currency cannot govern a card in the requested currency. |
issuer_card_unknown | 404 | The mint flow or card id is unknown to this account. |
invalid_holder | 400 | The cardholder identity payload is incomplete or malformed. |
holder_already_registered | 409 | That holder identity is already registered to another tenant at the issuer. |
mandate_already_bound | 409 | The mandate already backs an active card or a live mint flow — one card per mandate. |
mandate_already_has_card | 409 | The mandate already has an active card binding; revoke it before binding another. |
no_active_card | 404 | The mandate has no active card binding to revoke. |
cross_tenant_card | 409 | The card belongs to another tenant. |
idempotency_key_conflict | 409 | An Idempotency-Key was reused with a different request body. A changed body is a new request: use a new key. |
live_issuer_test_key | 403 | This deployment issues real cards; a test-mode key cannot mint. |
stage_issuer_live_key | 403 | This deployment issues stage cards; a live-mode key cannot mint. |
simulate_not_allowed | 403 | The card-authorization read-back only runs against a non-production issuer environment. |
Consent (hosted consent + Open Finance)
| Code | Status | When it fires |
|---|---|---|
consent_expired | 410 | The consent token in the URL has expired. Issue a new one. |
consent_already_resolved | 409 | The consent was already approved or denied. |
consent_active_for_consumer | 409 | The project already holds an open consent for this consumer at this bank. Revoke it before issuing another. |
consent_not_authorised | 409 | The operation needs a consent in the authorised state. |
illegal_transition | 409 | The requested consent status change is not legal from the current state. |
db_error | 409 | The consent could not be persisted. Retry. |
Hosted MCP servers
| Code | Status | When it fires |
|---|---|---|
mcp_server_not_found | 404 | No active MCP server for that slug. |
mcp_tool_not_found | 404 | The named tool does not exist on that server. |
invalid_upstream_url | 400 | upstream_url must be a public http(s) URL. |
slug_conflict | 409 | An MCP server with that slug already exists. Slugs are global. |
sweep_in_progress | 409 | A tool sweep for this account is already running. Try again shortly. |
Paywalls, payment links and the gateway
| Code | Status | When it fires |
|---|---|---|
paywall_not_found | 404 | No active paywall for that slug. |
payment_link_not_found | 404 | No active payment link for that slug. |
pricing_model_unsupported | 400 | Only flat, tiered, dynamic and metered paywall pricing are live. |
invalid_tiers | 400 | Tiered pricing needs at least one tier. |
invalid_dynamic_price_url | 400 | Dynamic pricing needs a public http(s) price-hook URL. |
invalid_metered_config | 400 | Metered pricing needs a metered_config block. |
metered_not_enabled | 400 | Metered pricing is not enabled on this deployment. |
metered_requires_provisioned | 400 | Metered pricing needs a provisioned payTo — refunds settle from a wallet we control. |
invalid_payto | 400 | A bring-your-own x402 payTo must be a 0x EVM address. |
invalid_rail | 400 | The payment link declares the same rail twice. |
malformed_authorization | 400 | The Authorization payload on a gateway call is not the expected shape. |
invalid_mandate | 400 | The mandate presented at the gateway is missing agent_id or purpose. |
payment_link_env_mismatch | 409 | The mandate's environment does not match the link's environment. |
no_pix_method | 422 | The payment link has no Pix settlement method. |
idempotency_in_progress | 409 | A request with this Idempotency-Key is still processing. Retry shortly. |
method_not_allowed | 405 | The paywall does not front that HTTP method. |
Agent identity, audit and memory
| Code | Status | When it fires |
|---|---|---|
agent_not_found | 404 | The agent id or did:web document does not resolve. |
org_id_not_did_safe | 400 | The account id contains characters a did:web path segment cannot carry. |
agent_id_not_did_safe | 400 | The agent id contains characters a did:web path segment cannot carry. |
agent_already_registered | 409 | An agent is already registered under that handle in this account. Rotate its key instead of re-registering. |
agent_did_owned_elsewhere | 409 | The derived did:web is registered to a different tenant. Register under a different handle. |
no_active_key | 409 | Rotate was called on an agent that has no active key to rotate. |
agent_key_not_found | 404 | The key id does not exist for this agent. |
key_already_revoked | 409 | Revoke on a key that is already revoked. |
issuer_not_provisioned | 404 | The platform issuer has no did:web document yet. |
cross_org_verify_denied | 403 | The anchor's account does not match the caller's. |
counterparty_not_found | 404 | The counterparty does not belong to this account and project. |
foreign_authorization_not_found | 404 | The facilitator has no record of that foreign authorization. |
Sessions and tool execution
| Code | Status | When it fires |
|---|---|---|
unknown_servers | 400 | Session create named one or more servers that are not in the catalog. |
server_not_connected | 400 | Execution named a server this project has no active connection for. |
session_not_active | 409 | The session was closed or expired; open a new one. |
credentials_unavailable | 424 | The credential for the resolved provider could not be read at dispatch. Nothing was sent upstream. |
invalid_tool_call_id | 400 | The tool_call_id in a status or verification-status path is not a well-formed id. |
tool_call_not_found | 404 | No tool call with that id in this project. |
chaos_invalid | 400 | A chaos block that does not match { fail_provider, meta_tool?, remaining: 1-3, reason }. |
chaos_not_permitted | 400 | Chaos rules were declared with a live-environment key. Use a csk_test_ key. |
payload_too_large | 413 | The request body exceeded the guardrail middleware's ceiling. |
Scope enforcement
These come from the scope guard rather than a route handler, and all three carry
the flat forbidden-style shape.
| Code | Status | When it fires |
|---|---|---|
route_scope_unmapped | 403 | The route carries no scope mapping and the deployment refuses unmapped routes (ROUTE_SCOPE_DENY_UNMAPPED). Off by default — see Authentication. |
route_scope_unresolved | 403 | Scope enforcement could not resolve the matched route template. Alarm-grade; report it. |
scopes_unresolved | 403 | Scope enforcement could not resolve the caller's scopes. Alarm-grade; report it. |
Approvals
| Code | Status | When it fires |
|---|---|---|
approval_not_found | 404 | No approval with that id is visible to this account. |
already_decided | 409 | The approval was already approved or rejected. Its decision is final. |
expired | 410 | The hold expired before anyone decided. The call never ran. |
bearer_token_cannot_decide | 403 | A project API key tried to decide. Deciding is service-auth only, so a leaked csk_ key cannot release a held payment. |
user_token_required | 403 | The decide call carried no user token and the deployment enforces one (APPROVAL_DECIDE_ENFORCE_USER_TOKEN). |
user_token_invalid | 403 | The user token failed verification. |
user_token_identity_mismatch | 403 | The x-codespar-user header disagrees with the verified token's sub. |
rate_limit_exceeded | 429 | Decide throttle hit. The body carries retry_after_seconds and the response a Retry-After header. |
bearer_token_cannot_manage_policies | 403 | Same posture on policy writes: a project API key cannot edit the rules that gate its own spend. |
Audit chain and incidents
| Code | Status | When it fires |
|---|---|---|
invalid_iso_8601 | 400 | A from / to value is not an ISO-8601 timestamp. |
from_after_to | 400 | The window's start is after its end. |
invalid_before_sequence | 400 | The pagination cursor is not a valid sequence number. |
invalid_user_id | 400 | The user_id filter is malformed. |
export_too_large | 413 | The requested export exceeds the size ceiling. Narrow the window. |
unauthenticated | 401 | The incident surface requires an authenticated principal. |
human_session_required | 403 | Acknowledging an incident requires a signed-in human, not a key. |
incident_not_found | 404 | No incident with that id in this account. |
already_acknowledged | 409 | The incident was already acknowledged. |
invalid_status | 400 | The target status is not one this incident can take. |
field_invalid | 400 | A named field failed validation; the body says which. |
window_ordering_invalid | 400 | The assessment window's bounds are out of order. |
pending_assessment_not_terminal | 400 | The assessment is still open; it cannot be closed from this state. |
regulatory_minimum_violation | 400 | The submitted values fall below a retention or disclosure minimum the surface enforces. |
Connect Links and OAuth
| Code | Status | When it fires |
|---|---|---|
server_oauth_not_configured | 404 | POST /v1/connect/start named a server with no OAuth configuration. |
invalid_state | 400 | The callback's state is unknown, expired, or already consumed. Links live 10 minutes and are single-use. |
missing_code_or_state | 400 | The provider redirected back without the parameters the exchange needs. |
provider_error_without_state | 400 | The provider reported an error and sent no state, so there is nowhere to redirect the user. |
server_mismatch | 400 | The callback's server does not match the one the state row was minted for. |
invalid_or_expired_state | 400 | Same failure on the storefront connect flow. Restart it. |
auth_required | 401 | The storefront connect flow was reached without a session. |
cannot_delete_active | 409 | Delete on a connection that is still active. Revoke it first. |
Cart, sellers and KYC onboarding
| Code | Status | When it fires |
|---|---|---|
cart_search_failed | 422 | The store search leg failed upstream. Retriable. |
cart_session_failed | 422 | The store checkout session could not be opened. Retriable — see codespar_shop's retriable reason codes. |
seller_not_found | 404 | No seller with that id in this account. |
document_number_required | 400 | The onboarding call needs the subject's document number. |
MCP Generator and hosted MCP
| Code | Status | When it fires |
|---|---|---|
project_required | 400 | The generate call did not resolve a project. |
invalid_spec | 400 | The supplied OpenAPI document could not be parsed. |
github_fetch_failed | 422 | The GitHub source could not be fetched. |
no_endpoints | 422 | The scan found no endpoints to expose. |
generated_server_unknown | 404 | No generated server with that id in this project. |
invalid_or_missing_mcp_session_id | 400 | The remote-MCP transport was called without a valid Mcp-Session-Id. |
Test mode
| Code | Status | When it fires |
|---|---|---|
tool_not_mocked | 422 | A session declared mocks but the executed tool has no mock registered. Flat shape with code at the top level. |
mocks_not_permitted | 403 | Mocks declared with a live-environment key. Use a csk_test_ key on a test-environment project. Flat shape. |
Limits
| Code | Status | When it fires |
|---|---|---|
quota_exceeded | 403 | The monthly tool-call allowance for the plan is used up. The flat body includes plan, limit, and used. |
rate_limited | 429 | Per-server rate limit hit on session execution. The response includes a Retry-After header (seconds) and retry_after_ms in the body. |
Fix for 429: respect Retry-After. Exponential backoff with jitter is the canonical pattern.
Debugging
Errors in the standard envelope carry a request_id in the body; every response also carries it in the X-Request-Id header. In the dashboard, search the Logs page for that id to see the full trace: every upstream call, its response body, and timings.
For production support: open an issue at github.com/codespar/codespar with the request_id, timestamp, and expected behavior. See Debugging for the full bug-report template.
Next steps
Test Mode
Hosted test mode lets you run an agent against the CodeSpar runtime with inline mock declarations — deterministic responses, no provider OAuth, full Guardrails governance. Declare mocks at session create; assert on the round-trip in your tests.
Debugging
Debug tool calls, inspect execution logs, and monitor agent performance with CodeSpar's built-in observability.