---
title: Error Reference
description: 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.
---

import { Callout } from "fumadocs-ui/components/callout";

# Error Reference

## The error envelope

Most `/v1` endpoints return errors in a nested envelope:

```json
{
  "error": {
    "code": "invalid_body",
    "message": "request body did not match the expected schema",
    "details": { "issues": [] }
  },
  "request_id": "req_a1b2c3d4"
}
```

- **`error.code`** is a stable machine-readable string. Safe to branch on in your code.
- **`error.message`** is a human-readable explanation. Surface to users with caution.
- **`error.details`** is optional and per-error. Validation errors put the schema issues here.
- **`request_id`** matches the `X-Request-Id` response 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](/docs/api/wallets).

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_...`.

<Callout title="401 on api-keys, usage, billing, team and project members" type="warn">
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](/docs/concepts/billing#via-the-usage-api).
</Callout>

**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](/docs/concepts/meta-tools/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](/docs/concepts/authentication#key-scopes). |
| `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](https://codespar.dev/dashboard/logs) 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](https://github.com/codespar/codespar/issues) with the `request_id`, timestamp, and expected behavior. See [Debugging](/docs/debugging) for the full bug-report template.

## Next steps

<NextStepsGrid items={[
  { label: "DEBUGGING", title: "Debugging", description: "Logs, tool-call trace, and bug-report template.", href: "/docs/debugging" },
  { label: "REFERENCE", title: "Wallets API", description: "Wallet-specific error codes: ledger conflicts, funding sources, balance invariants.", href: "/docs/api/wallets" },
  { label: "REFERENCE", title: "Sessions API", description: "Session-specific error surfaces.", href: "/docs/api/sessions" },
  { label: "REFERENCE", title: "Triggers API", description: "Webhook delivery errors and DLQ inspection.", href: "/docs/api/triggers" },
  { label: "CONCEPT", title: "Authentication", description: "API key types, scopes, and rotation for 401/403 issues.", href: "/docs/concepts/authentication" },
]} />
