Skip to content

Building on EchoGuide

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.

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.

Terminal window
npm run build --workspace=@echoguide/openapi
npm run generate:kotlin --workspace=@echoguide/openapi

The 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.

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.

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.

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.