Skip to content

API Overview

Status — sections below are split into Shipped (Layer 0 + Layer 1) and Planned (Layer 2+). The shipped surface is small — only organization and user domains 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 in services/api/internal/core/server/routes.go. The OpenAPI spec at apps/docs/openapi.yaml is the source of truth for the shipped routes; the drift test at services/api/internal/core/server/openapi/spec_test.go keeps the spec, the routes table, and the test in lockstep.

Base URL

Production:  TBD (1E.3 staging deploy)
Development: http://localhost:9000

All 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

MethodEndpointAuthDescription
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

MethodEndpointAuthDescription
GET/v1/meAuthenticatedCurrent user, memberships, current org, permissions
PUT/v1/me/switch-organizationAuthenticatedSwitch 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

MethodEndpointAuthDescription
GET/v1/organizationsAuthenticatedList organizations the caller is a member of
POST/v1/organizationsRequireSuperadminCreate organization (clones system role templates atomically)
GET/v1/organizations/{id}Authenticated (membership)Get organization
PATCH/v1/organizations/{id}organizations.updateUpdate organization
GET/v1/organizations/{id}/membersorganizations.manage_membersList members
POST/v1/organizations/{id}/membersorganizations.manage_membersAdd or upsert a member by email
DELETE/v1/organizations/{id}/members/{userId}organizations.manage_membersRemove a member
GET/v1/organizations/{id}/rolesorganizations.manage_membersList roles defined for the org (system clones + custom)
GET/v1/organizations/{id}/domainsorganizations.manage_domainsList custom domains
POST/v1/organizations/{id}/domainsorganizations.manage_domainsAdd custom domain (returns DNS TXT record)
DELETE/v1/organizations/{id}/domains/{domainId}organizations.manage_domainsRemove custom domain
POST/v1/organizations/{id}/domains/{domainId}/verifyorganizations.manage_domainsVerify 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

http
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:

  1. X-Organization-ID header (set by the frontend proxy.ts from the resolved subdomain or custom domain).
  2. The user's current_organization_id if they are still a member of it.
  3. First membership (auto-selected convenience).
  4. uuid.Nil (safe default — RLS naturally returns zero rows).

Superadmins bypass this and operate without org context (AdminPool, RLS bypassed).

Pagination

http
GET /v1/<resource>?page=2&limit=50
json
{
  "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

http
GET /v1/<resource>?sort=field        # ascending
GET /v1/<resource>?sort=-field       # descending
GET /v1/<resource>?sort=foo,-bar     # multi-column

Per-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:

http
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

json
{
  "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)

MethodEndpointDescription
GET/v1/patientsList patients (gated by patients.view_org)
POST/v1/patientsCreate 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}/profileCustom-field values (Layer 4 dep)
PUT/v1/patients/{id}/profileUpdate custom-field values
POST/v1/patients/{id}/impersonateImpersonate patient (patients.impersonate)

Specialists (Layer 2)

MethodEndpointDescription
GET/v1/specialistsList specialists (specialists.view)
POST/v1/specialistsCreate 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}/availabilityWeekly hours + overrides (Layer 5 dep)
PUT/v1/specialists/{id}/availabilityUpdate availability config

Specialties (Layer 2)

MethodEndpointDescription
GET/v1/specialtiesList specialties
POST/v1/specialtiesCreate (admin)
PATCH/v1/specialties/{id}Update
DELETE/v1/specialties/{id}Delete

Layer 3 — Service Catalog

MethodEndpointDescription
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

MethodEndpointDescription
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}/publishPublish a template version
GET / POST / PATCH/v1/forms (+ /{id})Form instances
POST/v1/forms/{id}/signSign form (immutable after) — forms.sign
POST / GET / DELETE/v1/forms/{id}/filesFile upload via S3 (forms.fill_* + 1A.8)

Layer 5 — Scheduling

MethodEndpointDescription
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

MethodEndpointDescription
GET / POST / PATCH / DELETE/v1/appointments (+ /{id})Appointments + state machine
POST/v1/public/bookings[Public] Guest booking
GET / POST / DELETE/v1/appointments/{id}/filesAppointment files (S3)
POST/v1/appointments/{id}/reviewsPatient feedback

Layer 7 — Documents

MethodEndpointDescription
GET / POST / PATCH / DELETE/v1/pdf-templates (+ /{id})Block-based PDF templates + versioning
POST/v1/pdf-templates/{id}/publishPublish template version
POST/v1/forms/{id}/renderRender a signed form to PDF (writes appointment_documents)
GET/v1/appointment-documents/{id}/downloadPre-signed S3 URL

Layer 8 — Automations + Webhooks

MethodEndpointDescription
GET / POST / PATCH / DELETE/v1/automation-rules (+ /{id})Automation rules CRUD
GET/v1/automation-executionsAppend-only audit trail
GET / POST / PATCH / DELETE/v1/webhook-subscriptions (+ /{id})Webhook subscriptions
GET/v1/webhook-subscriptions/{id}/eventsDelivery log
POST/v1/webhook-subscriptions/{id}/testSend a test event

Layer 9 — Segments

MethodEndpointDescription
GET / POST / PATCH / DELETE/v1/segments (+ /{id})Segments + rules JSONB
GET/v1/segments/{id}/membersMaterialized cache
POST/v1/segments/{id}/evaluateRe-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

MethodEndpointDescription
GET/v1/audit-logsPaginated audit log read (audit_log.view_org)
GET/v1/audit-logs/{id}Single event detail
GET/v1/audit-logs/exportCSV export
POST/v1/gdpr/exportDSAR fulfilment by the clinic — full patient data export at that clinic (gdpr.export_data)
POST/v1/gdpr/eraseRight to erasure — anonymise patient data at the requesting clinic (gdpr.erase_data)
POST/v1/gdpr/restrictRestrict 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.