API Overview
Status — sections below are split into Shipped (Layer 0 + Layer 1) and Planned (Layer 2+). The shipped surface is small — only
organizationanduserdomains exist in code today. Every "Planned" section describes the target shape that lands as the relevant layer ships; the corresponding handlers are not yet wired inservices/api/internal/core/server/routes.go. The OpenAPI spec atapps/docs/openapi.yamlis the source of truth for the shipped routes; the drift test atservices/api/internal/core/server/openapi/spec_test.gokeeps the spec, the routes table, and the test in lockstep.
Base URL
Production: TBD (1E.3 staging deploy)
Development: http://localhost:9000All endpoints require authentication unless marked [Public].
Shipped surface (today)
These routes are wired in services/api/internal/core/server/routes.go and exercised by tests. See reference/local-development.md for examples and reference/rbac-permissions.md for the permission codes that gate each one.
System
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /health | [Public] | Health check (postgres + redis status) |
| GET | /v1/public/system-info | [Public] | Manufacturer + UDI / MDR labelling |
| GET | /v1/public/organizations/resolve | [Public] | Resolve org by ?slug= or ?domain= |
Authentication & Users
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /v1/me | Authenticated | Current user, memberships, current org, permissions |
| PUT | /v1/me/switch-organization | Authenticated | Switch active organization (API path; primary switching is domain-based) |
JIT provisioning runs on the first authenticated request — there is no separate /webhooks/clerk endpoint today (planned, see auth/clerk-integration.md).
Organizations
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /v1/organizations | Authenticated | List organizations the caller is a member of |
| POST | /v1/organizations | RequireSuperadmin | Create organization (clones system role templates atomically) |
| GET | /v1/organizations/{id} | Authenticated (membership) | Get organization |
| PATCH | /v1/organizations/{id} | organizations.update | Update organization |
| GET | /v1/organizations/{id}/members | organizations.manage_members | List members |
| POST | /v1/organizations/{id}/members | organizations.manage_members | Add or upsert a member by email |
| DELETE | /v1/organizations/{id}/members/{userId} | organizations.manage_members | Remove a member |
| GET | /v1/organizations/{id}/roles | organizations.manage_members | List roles defined for the org (system clones + custom) |
| GET | /v1/organizations/{id}/domains | organizations.manage_domains | List custom domains |
| POST | /v1/organizations/{id}/domains | organizations.manage_domains | Add custom domain (returns DNS TXT record) |
| DELETE | /v1/organizations/{id}/domains/{domainId} | organizations.manage_domains | Remove custom domain |
| POST | /v1/organizations/{id}/domains/{domainId}/verify | organizations.manage_domains | Verify domain DNS |
That is the complete shipped surface — 16 routes (counting /health). Everything below is planned.
Global Conventions (shipped today, applied uniformly to Layer 2+ as it lands)
The conventions in this section come from Layer 1.7. They apply to the shipped routes above and become the contract every Layer 2+ handler will follow.
Authentication
Authorization: Bearer <clerk_session_token>Authenticate middleware verifies the JWT and JIT-provisions the internal users row on first call. See auth/session-management.md for the full middleware pipeline.
Organization Context
The active organization is resolved per-request, in this order:
X-Organization-IDheader (set by the frontendproxy.tsfrom the resolved subdomain or custom domain).- The user's
current_organization_idif they are still a member of it. - First membership (auto-selected convenience).
uuid.Nil(safe default — RLS naturally returns zero rows).
Superadmins bypass this and operate without org context (AdminPool, RLS bypassed).
Pagination
GET /v1/<resource>?page=2&limit=50{
"data": [...],
"pagination": { "page": 2, "limit": 50, "total": 237 }
}Defaults: page=1, limit=50. Out-of-range limit clamps to a hard max of 500. Implemented in services/api/internal/shared/apiquery/.
Filtering
Flat query params: ?field=value. Range queries use direction-suffixed keys (created_after=…, created_before=…). No nested ?filter[field]= syntax. Per-feature DSLs are allowed when complex filtering is genuinely needed (Layer 9 Segments).
Sorting
GET /v1/<resource>?sort=field # ascending
GET /v1/<resource>?sort=-field # descending
GET /v1/<resource>?sort=foo,-bar # multi-columnPer-endpoint allow-list. Non-allowlisted fields return 422 with a fields.sort reason. Implemented in services/api/internal/shared/apiquery/.
Idempotency
POST endpoints that create resources accept Idempotency-Key:
Idempotency-Key: <ascii-letters-digits-_-, 4-128 chars>Successful 2xx responses are cached for 24 h, keyed by (org_id, path, key). Replays carry an Idempotency-Replayed: true response header. Implemented in services/api/internal/core/idempotency/.
Error Responses
{
"error": {
"code": "validation_failed",
"message": "Validation error",
"fields": { "email": "invalid format" }
}
}fields is present only on 422 responses. The full envelope contract is in reference/error-envelope.md.
API Versioning
Major version in the URL path (/v1/...). Minor versions are additive — no breaking changes within a major version. Major bumps live at a new path prefix.
Rate Limiting (planned — Layer 1.16)
Auth endpoints and GET /v1/public/organizations/resolve will gate behind a Redis-backed limiter; per-tenant limits land in Layer 12. Retry-After and X-RateLimit-* headers ship with the limiter.
Planned (Layer 2+)
Everything below describes the target shape, not what you can call today. Wired endpoints land as their owning layer ships — see implementation-plan.md and architecture/dependency-map.md. When a section ships, move it up under "Shipped surface" and the OpenAPI spec drift test will keep it honest.
Layer 2 — People
Patients (Layer 2)
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/patients | List patients (gated by patients.view_org) |
| POST | /v1/patients | Create patient (patients.onboard) |
| GET | /v1/patients/{id} | Get patient (patients.view_org or patients.view_self) |
| PUT | /v1/patients/{id} | Update patient |
| DELETE | /v1/patients/{id} | Soft delete (patients.delete) |
| GET | /v1/patients/{id}/profile | Custom-field values (Layer 4 dep) |
| PUT | /v1/patients/{id}/profile | Update custom-field values |
| POST | /v1/patients/{id}/impersonate | Impersonate patient (patients.impersonate) |
Specialists (Layer 2)
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/specialists | List specialists (specialists.view) |
| POST | /v1/specialists | Create specialist (specialists.create) |
| GET | /v1/specialists/{id} | Get specialist |
| PUT | /v1/specialists/{id} | Update specialist |
| DELETE | /v1/specialists/{id} | Soft delete (specialists.delete) |
| GET | /v1/specialists/{id}/availability | Weekly hours + overrides (Layer 5 dep) |
| PUT | /v1/specialists/{id}/availability | Update availability config |
Specialties (Layer 2)
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/specialties | List specialties |
| POST | /v1/specialties | Create (admin) |
| PATCH | /v1/specialties/{id} | Update |
| DELETE | /v1/specialties/{id} | Delete |
Layer 3 — Service Catalog
| Method | Endpoint | Description |
|---|---|---|
| GET / POST / PATCH / DELETE | /v1/services (+ /{id}) | Services CRUD (services.view_org / services.manage) |
| GET / POST / PATCH / DELETE | /v1/service-plans (+ /{id}) | Service plans CRUD |
| GET / POST / PATCH / DELETE | /v1/products (+ /{id}) | Products CRUD |
| GET / POST / PATCH / DELETE | /v1/service-plans/{id}/products (+ /{pid}) | Bundled products junction |
Layer 4 — Custom Fields + Forms
| Method | Endpoint | Description |
|---|---|---|
| GET / POST / PATCH / DELETE | /v1/custom-fields (+ /{id}) | Custom field definitions + versioning |
| GET / POST / PATCH / DELETE | /v1/form-templates (+ /{id}) | Form templates + versioning |
| POST | /v1/form-templates/{id}/publish | Publish a template version |
| GET / POST / PATCH | /v1/forms (+ /{id}) | Form instances |
| POST | /v1/forms/{id}/sign | Sign form (immutable after) — forms.sign |
| POST / GET / DELETE | /v1/forms/{id}/files | File upload via S3 (forms.fill_* + 1A.8) |
Layer 5 — Scheduling
| Method | Endpoint | Description |
|---|---|---|
| GET / POST / PATCH / DELETE | /v1/calendars (+ /{id}) | Calendars CRUD |
| GET | /v1/public/availability | [Public] Public slot availability |
| POST / DELETE | /v1/public/holds (+ /{id}) | [Public] Slot hold lifecycle |
| GET | /v1/public/holds/{id}/stream | [Public] SSE for live availability |
Layer 6 — Appointments
| Method | Endpoint | Description |
|---|---|---|
| GET / POST / PATCH / DELETE | /v1/appointments (+ /{id}) | Appointments + state machine |
| POST | /v1/public/bookings | [Public] Guest booking |
| GET / POST / DELETE | /v1/appointments/{id}/files | Appointment files (S3) |
| POST | /v1/appointments/{id}/reviews | Patient feedback |
Layer 7 — Documents
| Method | Endpoint | Description |
|---|---|---|
| GET / POST / PATCH / DELETE | /v1/pdf-templates (+ /{id}) | Block-based PDF templates + versioning |
| POST | /v1/pdf-templates/{id}/publish | Publish template version |
| POST | /v1/forms/{id}/render | Render a signed form to PDF (writes appointment_documents) |
| GET | /v1/appointment-documents/{id}/download | Pre-signed S3 URL |
Layer 8 — Automations + Webhooks
| Method | Endpoint | Description |
|---|---|---|
| GET / POST / PATCH / DELETE | /v1/automation-rules (+ /{id}) | Automation rules CRUD |
| GET | /v1/automation-executions | Append-only audit trail |
| GET / POST / PATCH / DELETE | /v1/webhook-subscriptions (+ /{id}) | Webhook subscriptions |
| GET | /v1/webhook-subscriptions/{id}/events | Delivery log |
| POST | /v1/webhook-subscriptions/{id}/test | Send a test event |
Layer 9 — Segments
| Method | Endpoint | Description |
|---|---|---|
| GET / POST / PATCH / DELETE | /v1/segments (+ /{id}) | Segments + rules JSONB |
| GET | /v1/segments/{id}/members | Materialized cache |
| POST | /v1/segments/{id}/evaluate | Re-evaluate membership |
Layer 10 — Telerehab (regulatory boundary)
The exercise library, treatment plans, patient enrollment, session execution, and pose tracking endpoints live behind the IEC 62304 boundary. Detailed per-endpoint planning lives in architecture/data-model.md Areas 9–10 and the implementation plan.
Layer 2 (F10) — Telemetry API
A separate Go service that ingests patient exercise-engagement events and pose-tracking landmark batches from the Patient Portal. Three typed endpoints: POST /v1/pose/frames, POST /v1/media/events, POST /v1/sessions/{id}/end. It does not ship as part of Core API; reads flow through Core API. Locked design in /telemetry/index.md and /telemetry/api.md. Earlier framing of telemetry as a Layer 11 concern (audit-forwarding + ClickHouse) has been rejected — see decisions.md → Why telemetry is PG + S3, not ClickHouse.
Layer 12 — GDPR & Audit Read API
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/audit-logs | Paginated audit log read (audit_log.view_org) |
| GET | /v1/audit-logs/{id} | Single event detail |
| GET | /v1/audit-logs/export | CSV export |
| POST | /v1/gdpr/export | DSAR fulfilment by the clinic — full patient data export at that clinic (gdpr.export_data) |
| POST | /v1/gdpr/erase | Right to erasure — anonymise patient data at the requesting clinic (gdpr.erase_data) |
| POST | /v1/gdpr/restrict | Restrict processing at the requesting clinic (gdpr.restrict_processing) |
DSARs are fulfilled by the clinic, not the platform — the clinic is the GDPR data controller for patient data; the platform is processor. The portal exposes a "your clinics" list so a patient knows which controllers to contact; the endpoints above run inside a clinic's tenant scope. Cross-tenant DSAR routing for orphaned requests is the documented break-glass exception path (Foundation 1B.11). See decisions.md → Why clinic is controller, platform is processor.
Related Documentation
- reference/api-conventions.md — pagination, filtering, sorting, idempotency, versioning
- reference/error-envelope.md — error envelope contract
- reference/rbac-permissions.md — permission catalog
- reference/local-development.md — running the shipped surface locally
apps/docs/openapi.yaml— machine-readable spec for the shipped surface- features/index.md — per-feature specs (most are Layer 2+ and forward-looking)