Error envelope — the Sendero contract
Every Sendero JSON route returns the same error envelope — `{ code, message, details, docsUrl, agentInstruction, traceId }`. This page is the source of truth for the shape, the naming convention, and the `agentInstruction` field that lets an LLM surface canonical recovery copy without hallucinating.
The Sendero error envelope exists so an SDK can switch on code, a human can read message, an LLM can surface agentInstruction to a customer verbatim, and a support ticket can quote traceId for our audit log to find the request.
Every field is surfaced via apiError(...) / apiErrorResponse(...) from apps/app/lib/api-errors.ts. The HTTP status mirrors the failure family (4xx for caller, 5xx for us). The x-sendero-trace-id header echoes the traceId field so log-correlation tools see it without parsing the body.
Field-by-field
code — the stable contract
Machine-readable, ALL_CAPS_SNAKE_CASE, family-prefixed. Stable across versions — once a code is documented it never silently changes meaning. New codes are additive.
Family prefixes in production today:
| Prefix | Meaning |
|---|---|
MARKUP_ | tenant markup math, ceiling, floor, and override gates |
POLICY_ | tenant pricing policy lifecycle (inactive, partial, missing, version conflict) |
TREASURY_ | Circle wallet provisioning + on-chain treasury preconditions |
OVERRIDE_ | privileged override gate failures |
BOOKING_ | booking-row lookup + state errors |
TENANT_ | tenant resolution + context errors |
OPERATOR_ | admin-only gates (e.g. OPERATOR_ONLY on activate_tenant_pricing_policy) |
Add a new family by adding a new builder cluster in apps/app/lib/api-errors.ts rather than scattering raw apiErrorResponse({ code: 'FOO_BAR' }) calls — keeps the catalog grep-able.
message — human, terse, no PII
Short. Imperative when an action is required. Quotes the failing values when they help the human diagnose (e.g. "Markup 75000000 micro-USDC exceeds tenant ceiling 50000000.").
Never contains: customer email, payment card numbers, raw passport fields, claim-code cleartext, signed-link fragments, or any other PII / secret. The error path is logged; treat it as if it'll be screenshotted into a public ticket.
details — free-form JSON
Optional. Used to surface validation issues (zodIssues), retry hints, or out-of-band identifiers the SDK can use to recover. Stable per-code — if you add a new details shape under an existing code, it's a breaking change for anyone parsing it.
docsUrl — deep link to the failure mode
Resolves to a section anchor under /docs/<family>/error-codes#<code-lowercased>. Built from process.env.NEXT_PUBLIC_DOCS_URL so a tenant running their own docs mirror can repoint. Optional but recommended on every code.
agentInstruction — LLM-friendly recovery copy
The killer field. When an LLM-driven agent receives this envelope, it should surface agentInstruction to the human verbatim instead of paraphrasing.
Why verbatim?
- Recovery copy is canonical. We've thought about exactly what the human should hear; the agent paraphrasing it loses precision.
- It's grep-able for QA. When the eval harness runs scenario 5 (
inactive_policy_graceful_handoff), it asserts the agent's output contains theagentInstructionstring. No paraphrase passes. - It's actionable. Every
agentInstructionends with what the human should DO ("publish a policy at /dashboard/settings/pricing", "mint a key with the override scope"), not just what's wrong.
Tone guide for new errors:
Tell the human {what's wrong}. Direct them to {fix path}, OR {alternative recovery}.
Always second-person, always actionable, always names the dashboard URL or the alternate recovery path. See MarkupOverCeilingError for the canonical exemplar.
traceId — request-scoped correlation
Pulled from inbound x-sendero-trace-id if present; otherwise generated as a fresh UUID. Echoed in the x-sendero-trace-id response header. Every log line in the request scope carries it.
This is the one field every support ticket should quote. Our audit log keys on it; given a traceId we can rebuild the full request → tool → DB → response timeline in seconds.
Sample responses
401 UNAUTHORIZED
403 FORBIDDEN
409 POLICY_INACTIVE
422 MARKUP_CONFIG_INVALID
SDK pattern
Adding a new error
- Add (or extend) a builder cluster in
apps/app/lib/api-errors.ts. Don't open-codeapiErrorResponse({ code: '...' })in route files. - Pick a family prefix. Reuse an existing one when the failure mode lives in the same domain.
- Write the
agentInstructionin second person, ending with an action the human should take. Quote URLs. - Add a section to
/docs/<family>/error-codes.mdxwith the JSON envelope shape so SDK authors can copy-paste their handler. - Cross-link from the relevant concept page (
/docs/<family>/<concept>.mdx).
The convention exists so a customer's first encounter with an error is not their first encounter with our recovery copy.