API reference
The API contract is one file: packages/openapi/openapi.yaml. Every route, request, response and
error is defined there, and nowhere else.
The bundled spec itself is at /api-reference/openapi.json for tools and agents.
What is generated from it
Section titled “What is generated from it”Nothing below is written by hand. Change the YAML, rebuild, and every consumer follows.
| Output | Tool | Used by |
|---|---|---|
| Zod schemas | @hey-api/openapi-ts |
The API, for runtime validation |
| TypeScript types | openapi-typescript |
The API, React Native and the admin portal |
| Kotlin client | openapi-generator |
The Android voice pipeline |
| This site’s reference | redocly |
Readers of these docs |
A breaking change to the contract fails the build of whatever depends on it, instead of reaching a user.
Regenerating
Section titled “Regenerating”npm run build --workspace=@echoguide/openapinpm run generate:kotlin --workspace=@echoguide/openapiThe first command lints the YAML, then generates the Zod schemas and TypeScript types and
compiles the package. The Kotlin client is separate because it needs Java 11 or newer; it is
written to packages/openapi/generated/kotlin with its own Gradle wrapper.
Generated files are not committed. The YAML is the only thing to review.
Command statuses
Section titled “Command statuses”POST /v1/commands answers with one of four statuses. The schema says which fields come back;
this is what the phone does with them.
| Status | Meaning | Phone does |
|---|---|---|
ACCEPTED |
Plan passed validation | Executes it |
CONFIRMATION_REQUIRED |
Plan contains a destructive step | Speaks the step, waits for yes |
REPROMPT |
Transcript failed the confidence gate | Asks again, at most twice |
REJECTED |
Plan failed schema or allowlist validation | Speaks the failure, runs nothing |
Every step in a plan is one of five allowlisted actions, and every step must say whether it is destructive. A plan that leaves that out is rejected rather than assumed safe.
Sessions
Section titled “Sessions”Devices register once and receive an opaque session token, bound to the install id. Every user
request sends Authorization: Bearer <session_token> and X-Install-ID. Tokens rotate through
POST /v1/auth/refresh and are revoked by POST /v1/auth/logout. Re-registering an install id that
is already registered requires its current session; without it the answer is 409 and the client
starts a new install. Admin routes use a separate credential, and each one declares the permission
it needs — users.read or users.suspend — in the contract. See
Security.
Errors and limits
Section titled “Errors and limits”| Status | When |
|---|---|
| 401 | Missing, malformed, unknown, expired or revoked credential |
| 403 | Account suspended, or admin lacks the permission |
| 409 | Install id already registered; idempotency key still in flight; account being deleted |
| 413 | Body over the route’s limit (16 KB for JSON routes, 1 MB for commands) |
| 422 | Idempotency key reused with a different body |
| 429 | Rate limit, with Retry-After |
| 502 / 503 / 504 | Voice provider unreadable / unavailable or breaker open / timed out |
Every response carries X-Request-ID. The contract test in
apps/api/src/test/integration/contract.test.ts fails if a route exists in the code but not in
openapi.yaml, or the reverse.