REST API Reference
Health Assistant - API Documentation
Overview
Health Assistant provides RESTful APIs for interacting with the health data platform.
Base URL: http://localhost:8000/api/v1
API Docs: http://localhost:8000/docs
Authentication
All API endpoints (except /auth/login, /auth/register, and
/auth/invite — the latter requires a JWT but is the issuance mechanism
for the second one) require authentication.
JWT Token
Include your JWT token in the Authorization header:
Authorization: Bearer <your-jwt-token>Tenant & patient scoping
Every authenticated request carries a tenant_id (from the JWT). All
list/read/write endpoints are tenant-scoped — a caller can only see
rows whose tenant_id matches their own. Cross-tenant calls return
404 (not 403) so the existence of a row in another tenant is not
leaked. The deliberate exception is the SYSTEM_ADMIN role, which
bypasses the tenant filter (operator visibility — see
TENANCY_AND_USER_MANAGEMENT.md).
For patient-scoped endpoints (anything that takes a patient_id), a
USER-role caller must additionally be the patient's linked user
(Patient.user_id == current_user.user_id); ADMIN and MANAGER see
all patients in their tenant. The canonical check is
check_patient_access in app/api/v1/endpoints/utils.py.
Login Endpoint
POST /api/v1/auth/loginRequest Body:
username=test@example.com
password=your-passwordResponse: 200 OK
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 86400
}Refresh Token
POST /api/v1/auth/refreshRequest Body:
{
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}Response: 200 OK
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 86400
}Register New User
POST /api/v1/auth/registerRequest Body:
email: (Required) User email addresspassword: (Required) Password (min 8 characters)tenant_id: (Optional) Tenant ID to join. If provided,invite_tokenis required (see Joining an Existing Tenant).invite_token: (Optional but required whentenant_idis provided) Tenant-scoped JWT issued by that tenant's administrator viaPOST /api/v1/auth/invite.
The register endpoint supports two onboarding paths:
Path 1 — Bootstrap (no tenant_id)
Used for the household self-onboarding UX. The server creates a new Tenant + "Default Household" Organization for the user. The very first registration in the entire database is promoted to SYSTEM_ADMIN; subsequent bootstraps become ADMIN of their own new household.
The first-user check is race-protected via a Postgres advisory lock
(pg_advisory_xact_lock) acquired before COUNT(users) and held across
the insert, so two concurrent bootstraps cannot both promote.
{
"email": "user@example.com",
"password": "securepassword123"
}Path 2 — Joining an Existing Tenant (invite required)
When tenant_id is provided, the server verifies (in order):
- The tenant exists (404 otherwise — no leak).
- The
invite_tokenis a validSECRET_KEY-signed JWT scoped to that tenant (403 otherwise). The token may also bind to a specificemailand encode a role (USER/ADMIN/MANAGER;SYSTEM_ADMINis never grantable via invite — bootstrap is the only SYSTEM_ADMIN grantor; defense in depth: the issuer refuses to encode it and the verifier downgrades any hand-craftedSYSTEM_ADMINclaim toUSER).
See TENANCY_AND_USER_MANAGEMENT.md for the full invite flow + curl examples.
{
"email": "newmember@family.com",
"password": "securepassword123",
"tenant_id": "123e4567-e89b-12d3-a456-426614174001",
"invite_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}Response: 200 OK
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"email": "user@example.com",
"role": "SYSTEM_ADMIN",
"tenant_id": "123e4567-e89b-12d3-a456-426614174001"
}Issue an Invite Token
POST /api/v1/auth/inviteRole gating: ADMIN, MANAGER, or SYSTEM_ADMIN only. A non-
SYSTEM_ADMIN caller can only mint invites for their own tenant.
Query Parameters:
tenant_id(Optional) Target tenant. Defaults to the caller's tenant.SYSTEM_ADMINcan target any tenant; other roles get 403 if they name a different tenant.email(Optional) Bind the token to a specific invitee email. If set, the register endpoint will reject any request using this token with a different email.role(Optional, defaultUSER) Role to grant. Cannot beSYSTEM_ADMIN— that role is bootstrap-only.expires_days(Optional, default7) Token TTL in days.
Response: 200 OK
{
"invite_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"tenant_id": "123e4567-e89b-12d3-a456-426614174001",
"role": "USER",
"expires_in_days": 7
}Validate Token
GET /api/v1/auth/validateResponse: 200 OK
{
"valid": true,
"user_id": "123e4567-e89b-12d3-a456-426614174000"
}Get Current User
GET /api/v1/users/meResponse: 200 OK
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"email": "user@example.com",
"role": "user",
"tenant_id": "123e4567-e89b-12d3-a456-426614174001"
}REST API
Tenants
Create Tenant
POST /api/v1/tenantsRequest Body:
{
"name": "Acme Corporation",
"settings": {
"data_retention_days": 2555,
"unit_system": "metric"
}
}Response: 201 Created
Get Tenant
GET /api/v1/tenants/{tenant_id}Response: 200 OK
Update Tenant
PUT /api/v1/tenants/{tenant_id}Request Body: (same as create)
Delete Tenant
DELETE /api/v1/tenants/{tenant_id}Note: Soft delete - sets deleted_at timestamp
Users
Create User
POST /api/v1/usersRequest Body:
{
"email": "john.doe@example.com",
"password": "SecurePass123!",
"role": "user",
"tenant_id": "123e4567-e89b-12d3-a456-426614174001"
}Response: 201 Created
Get Current User
GET /api/v1/users/meResponse: 200 OK
Update User
PUT /api/v1/users/{user_id}Delete User
DELETE /api/v1/users/{user_id}User Administration
List Tenant Users
GET /api/v1/usersReturns all users belonging to the current tenant. Admin/Manager roles only.
Create User
POST /api/v1/usersRequest Body:
{
"email": "newuser@example.com",
"password": "temporarypassword123",
"role": "USER",
"tenant_id": "optional-uuid"
}If tenant_id is omitted, the user is created in the administrator's current tenant.
Documents
Upload Document
POST /api/v1/documentsForm Data:
file: PDF, JPEG, PNG, DOCX, or TXT filepatient_id: Optional patient reference
Response: 202 Accepted
Get Document
GET /api/v1/documents/{document_id}Download Document
GET /api/v1/documents/{document_id}/downloadResponse: File download
Preview Document
GET /api/v1/documents/{document_id}/preview?page=0Returns a single-page JPEG/PNG preview of a document (DICOM frames or
PDF pages are converted on the fly). Multi-page docs expose total page
count via the X-Total-Pages response header; page is zero-indexed.
Auth (one of):
- A valid presigned token in the
?token=...query parameter (short-lived JWT bound to thisdocument_id; minted by an authenticated caller). This is the path used by<img src="...">tags in the frontend, which cannot send anAuthorizationheader. - A valid
Authorization: Bearer <jwt>for the document's tenant. Used by JSON-fetch clients (the AI assistant, fetch-based viewers).SYSTEM_ADMINBearer bypasses the tenant check.
A request with neither credential returns 401. A Bearer JWT for a
different tenant returns 404 (no information leak that the row exists
elsewhere).
Trigger Extraction
POST /api/v1/documents/{document_id}/extractResponse: 202 Accepted
Get Extraction Status
GET /api/v1/documents/{document_id}/extract/statusResponse:
{
"status": "completed",
"progress": 100,
"extracted_at": "2024-01-15T11:05:00Z"
}Patient & Observation endpoints
Patient identity and biomarker readings are managed via dedicated domain endpoints that return ORM-shape JSON (snake_case + app fields like
biomarker_id,normalized_value). These are the frontend's primary API surface. For canonical FHIR R4 interop (external systems, export/import, SMART-on-FHIR), see the/api/v1/fhir/R4/*facade at FHIR_R4_FACADE.md.
Create Patient
POST /api/v1/patientsRequest Body: (ORM-shape patient dict)
List Patients
GET /api/v1/patientsQuery Parameters:
limit: Number of results per pageoffset: Pagination offsetuser_id: Filter by linked user
Get Patient
GET /api/v1/patients/{id}Create Observation
POST /api/v1/observationsRequest Body: (ORM-shape observation dict)
List Observations
GET /api/v1/observationsQuery Parameters:
patient_id: Filter by patient IDcode: Filter by LOINC codestart_date/end_date: Filter by date rangelimit/offset: Pagination
Get / Delete Observation
GET /api/v1/observations/{id}
DELETE /api/v1/observations/{id}Clinical Events
List Event Types
GET /api/v1/clinical-events/typesReturns all available event categories (Pregnancy, Pain, Dental, etc.).
List Events
GET /api/v1/clinical-eventsQuery Parameters:
patient_id: Filter by patientexamination_id: Filter by linked examinationstatus: Filter byactive,resolved, etc.
Create Event
POST /api/v1/clinical-eventsRequest Body:
{
"patient_id": "uuid",
"type_id": "uuid",
"title": "Third Pregnancy",
"description": "Routine monitoring",
"onset_date": "2026-01-15T10:00:00Z",
"event_metadata": {
"lmp": "2026-01-01"
},
"examinations": [
{ "examination_id": "uuid", "reason": "Initial scan" }
]
}Link Examination
POST /api/v1/clinical-events/{event_id}/link-examinationAssociate an existing visit with an event.
List Correlated Biomarkers
GET /api/v1/clinical-events/types/{type_id}/biomarkersReturns all biomarkers conceptually linked to a specific event type (e.g., Vision -> Visual Acuity).
Wearable Data
Upload Wearable Data
POST /api/v1/wearable/dataRequest Body:
{
"device_id": "garmin-vivoactive-4",
"data": [
{
"timestamp": "2024-01-15T08:00:00Z",
"heart_rate": 72,
"steps": 1500,
"calories": 250
}
]
}Response: 201 Created
Get Wearable Data
GET /api/v1/wearable/dataQuery Parameters:
device_id: Filter by devicestart_date: Start timestampend_date: End timestampmetrics: Comma-separated metrics
Get Daily Summary
GET /api/v1/wearable/data/summaryQuery Parameters:
date: Datedevice_id: Filter by device
Notifications & Push
Unified multi-source/multi-recipient/multi-channel notification system. All endpoints require authentication and are tenant-scoped via
current_user.tenant_id. Cross-tenant calls return404. Patient-scoped routes additionally callcheck_patient_access. See NOTIFICATION_SYSTEM.md for the full architecture (fan-out model, target resolution, rules engine, real-time WS).
Get VAPID Public Key
GET /api/v1/notifications/vapid-public-keyReturns the public key for Web Push registration. Public (no auth).
Register Push Subscription
POST /api/v1/notifications/subscribeRequest body (SubscribeRequest Pydantic schema — the whole envelope
is parsed; do not pass the subscription as a bare dict with metadata
in query params):
{
"subscription": {
"endpoint": "https://updates.push.services.mozilla.com/wpush/v2/...",
"keys": { "p256dh": "...", "auth": "..." }
},
"device_id": "optional-uuid",
"user_agent": "Mozilla/5.0 ..."
}Personal Inbox
GET /api/v1/notifications/inboxQuery Parameters:
status:unread|read|dismissedcategory: notification categorysource: notification sourcepatient_id: filter to a patient (access-checked)limit(default 50, max 100),offset
Returns {items: NotificationRecipientRead[], total} — per-user inbox
state joined to the notification event.
Unread Count
GET /api/v1/notifications/unread-countReturns {count: int} — for the bell badge.
Mark Read / Dismiss / Read-All
PATCH /api/v1/notifications/{recipient_id}/read
PATCH /api/v1/notifications/{recipient_id}/dismiss
POST /api/v1/notifications/read-allOperate on the user's own inbox rows (tenant-scoped; cross-tenant = 404).
Admin Feed / Stats / Delivery Detail
Role-gated to
ADMIN/MANAGER/SYSTEM_ADMIN.SYSTEM_ADMINsees cross-tenant data and may passtenant_idto target another tenant.
GET /api/v1/notifications/admin
GET /api/v1/notifications/admin/stats
GET /api/v1/notifications/admin/{notification_id}/delivery/admin— tenant-wide (or cross-tenant forSYSTEM_ADMIN) feed of every notification event. Filters:type,source,category,tenant_id./admin/stats— aggregated counts:by_source,by_category,delivery(per-channel × status matrix),recipients(total inbox rows),unique_recipients(distinct user count),total./admin/{id}/delivery— per-recipient breakdown for one notification. Returns the notification, sender (email resolved), and per-recipient inbox status + per-channel delivery state (channel, status, error, timestamps). Used by the admin center's click-to-detail modal.
System Broadcast
POST /api/v1/admin/notifications/broadcastQuery Parameters:
title(required)bodyseverity:info|warning|critical(defaultinfo)scope:tenant(default) |system—systemrequiresSYSTEM_ADMINtenant_id: target another tenant (SYSTEM_ADMINonly)
Emits a SYSTEM/SYSTEM_BROADCAST notification to every user in scope (TENANT target for tenant scope, SYSTEM target = every SYSTEM_ADMIN for system scope).
Scheduled Triggers (reminders)
POST /api/v1/notifications/triggers
GET /api/v1/notifications/triggers
DELETE /api/v1/notifications/triggers/{trigger_id}
POST /api/v1/notifications/triggers/{trigger_id}/testTriggerType: TIME (one-shot at a datetime) or RECURRING (wall-clock
schedule with optional days-of-week). TriggerType.EVENT and the legacy
biomarker_update event hook were removed — biomarker thresholds are now
event-driven via the rules engine below.
GET /triggers patient_id is optional — without it, lists
tenant-wide (used by the global Notification Center "Reminders" tab);
with it, access-checked and patient-scoped.
Biomarker Rules (event-driven alerts)
Replaces the removed /alerts/* endpoints. Rules are evaluated on every
observation ingestion (fhir_service.create_observation →
notification_rule_service.evaluate_and_fire).
GET /api/v1/notification-rules
POST /api/v1/notification-rules
PUT /api/v1/notification-rules/{id}
DELETE /api/v1/notification-rules/{id}
POST /api/v1/notification-rules/{id}/testReal-time WebSocket
GET (WS) /api/v1/ws/notificationsPer-user live stream over Redis pub/sub. Auth via the
["bearer", <jwt>] Sec-WebSocket-Protocol subprotocol (the token stays
out of URL logs). Server-side keepalive ping every 30s. The frontend's
useNotificationStream hook auto-reconnects with 5s backoff and falls
back to a 30s unread-count poll if the socket can't open.
Analytics
Get Dashboard Data
GET /api/v1/analytics/dashboardQuery Parameters:
patient_id: Filter by patientperiod: Time period
Get Biomarker Trends
GET /api/v1/analytics/trendsQuery Parameters:
biomarker_codes: Comma-separated slugs, LOINC codes, or display names. When provided, the filter is expanded against the definition catalog (names, aliases, codes) so both mapped and unmapped observations are included.period: Time period (e.g., 'last-30-days', 'last-12-months', 'all-time')aggregation: Optional resolution bucket for telemetry data (e.g., '15 minutes', '1 day', '1 week')patient_id: Optional override for admin context
Remap Observations to Definition
POST /api/v1/biomarkers/{biomarker_id}/remapRelinks unmapped observations (those with biomarker_id = NULL) to a biomarker definition. Matches observations whose stored code.text equals source_name (case-insensitive). Used by the frontend's "Create biomarker" / "Map to existing" popup on unmapped biomarkers.
Request Body:
{
"source_name": "WBC",
"patient_id": "uuid-of-patient (optional)"
}Response:
{
"status": "success",
"biomarker_id": "3e9e3f7e-42f1-439f-a73c-64fd6234aed5",
"observations_remapped": 3
}Get Analytics Summary
GET /api/v1/analytics/summaryQuery Parameters:
patient_id: Filter by patientperiod: Time period
Get Reference Ranges
GET /api/v1/analytics/reference-rangesIntegrations & Webhooks
The integrations surface lives at /api/v1/integrations/* (see
INTEGRATIONS_FRAMEWORK.md for the discovery
- enable + sync flow). Two tokenless inbound routes are exposed for external systems that don't carry the platform JWT:
Webhook (inbound push from the provider)
POST /api/v1/integrations/{domain}/webhook/{integration_id}HMAC-SHA256 verification when the integration's user_config.webhook_secret
is set. Supported signature headers: X-Webhook-Signature,
X-Webhook-Signature-256, X-Hub-Signature-256 (GitHub; sha256=<hex>
prefix tolerated). Without a configured secret the integration UUID is
the only credential (legacy mode — operators are encouraged to set a
secret).
Generic two-way API proxy
{GET|POST|PUT|DELETE} /api/v1/integrations/{domain}/api/{integration_id}/{path}Forwards to the provider's handle_api_request for headless two-way
integration clients. Auth:
-
HMAC (recommended) — when the integration's
user_config.api_secretis set, the request MUST carry anX-Api-Signatureheader with an HMAC-SHA256 of the canonical request:METHOD\n<path>\n[<timestamp>\n]<raw_body>The optional
X-Api-Timestampheader (epoch seconds) is folded into the signed payload and validated against a ±5-minute skew window to prevent replay. Missing or invalid signature →401. -
Legacy UUID-only — when no
api_secretis configured, the integration UUID acts as the credential. Alogger.warningis emitted on every call so operators notice the gap. This mode is preserved for backward compatibility but is not recommended for new deployments.
See INTEGRATIONS_SDK.md for the webhook_secret /
api_secret configuration flow.
Notification Action (button click handler)
POST /api/v1/integrations/{domain}/notification-action/{integration_id}/{action_id}Server-side handler for clicked action buttons on integration-authored
notifications (the buttons of type="post" in payload.actions[]).
Routes to the provider's handle_notification_action(integration, action_id, payload) and returns an ActionResult-shape dict
({"message": ..., "results": [DisplayBlock...]}) that the frontend
renders in a follow-up modal — same renderer as custom-action results.
Auth: Bearer JWT, tenant-scoped (the caller must own the integration).
The provider must opt in via supports_notifications() → True (see
INTEGRATIONS_SDK.md §3.9).
Request body: arbitrary JSON (passed through as the payload argument
to the provider). Most action buttons send an empty object {}.
Notification Types (per-integration preferences)
GET /api/v1/integrations/notification-types
PUT /api/v1/integrations/{domain}/notification-types/{type_id}GET /integrations/notification-types — aggregates every enabled
integration's declared notification kinds into a single response:
{
"integrations": [
{
"domain": "dev_dummy",
"instance_name": "My test dummy",
"integration_id": "<uuid>",
"types": [
{
"id": "elevated_heart_rate",
"label": "Elevated heart-rate alerts",
"description": "Fires when a synced heart-rate reading exceeds 100 bpm.",
"category": "alert",
"severity": "warning",
"default_enabled": true,
"channels": ["IN_APP", "PUSH"],
"enabled": true
}
]
}
]
}enabled resolves as: USER setting wins, otherwise the provider's
default_enabled, otherwise true. Used by both the per-integration
"Notifications" tab and the central /settings/notifications rollup.
PUT /integrations/{domain}/notification-types/{type_id} — set the
caller's per-type preference. Body: {"enabled": <bool>}. Setting
enabled=true removes the override (revert to default); false writes
user.settings["notifications.integration.{domain}.{type_id}"] = false.
404 if the caller doesn't own an integration of the given domain.
Prefs are keyed by (domain, type_id), NOT by integration instance —
multiple instances of the same domain share prefs. See
INTEGRATIONS_SDK.md §3.9 → Per-type user preferences.
Catalogs & Search
Health Assistant groups its reference vocabularies (anatomy, taxonomy/concepts,
biomarkers, medications, allergies, vaccines) under a uniform Catalog
Registry (app/catalogs/). Every catalog conforms to one
CRUD/access/search/FHIR/edge contract, exposed through a thin registry-driven
meta-layer that complements (does not replace) the domain endpoints
(/biomarkers, /medications, /anatomy, /concepts, /allergies).
Access is ownership-based via scope tiers (Phase A), not pure RBAC: every
item carries a scope of system | tenant | user. Any authenticated user
may create — the scope is derived from the role (SYSTEM_ADMIN→system,
ADMIN/MANAGER→tenant, USER→user). Update/delete: system→SYSTEM_ADMIN,
tenant→ADMIN/MANAGER, user→creator OR ADMIN. CatalogPermissionDenied →
403. Every write appends a CatalogAuditLog row (Phase B).
List registered catalog types
GET /api/v1/catalogs → {types: [{type, ui, has_concept_link, edge_endpoint_type, search_columns}, …]}
(drives the /catalogs?type= workspace left rail).
List / get / create / update / delete one catalog type
GET /api/v1/catalogs/{type}?search=&scope=&class=&kind=&include=relations&limit=&offset= → {"items":[…], "total":n}
GET /api/v1/catalogs/{type}/{id}
POST /api/v1/catalogs/{type} (any role; scope derived from role)
PUT /api/v1/catalogs/{type}/{id} (scope + ownership gated)
DELETE /api/v1/catalogs/{type}/{id} (scope + ownership gated){type} ∈ biomarker | medication | allergy | anatomy | vaccine | concept.
Reads are tenant-scoped (or_(tenant_id == caller, tenant_id IS NULL)).
?scope=system|tenant|usernarrows to a tier.?class=<slug>(comma-list OK) filters any catalog whose items carry aclass_concept_idFK (anatomy, biomarker, medication, allergy, vaccine) by its taxonomy class — the adapter resolves the slug → concept id. Each item is annotated withclass_concept_slug+class_concept_name(e.g. Thyroid →organ/Organ).?kind=<ConceptKind>filters theconceptcatalog byprimary_kind(e.g.anatomy_class,disease).?include=relationsannotates each item withrelation_count+relation_breakdown(per relation type) via a single batched count query.
Scope promotion / demotion
POST /api/v1/catalogs/{type}/{id}/promote body {"scope":"tenant"|"system"|"user"}
— transitions an item's scope. user↔tenant requires ADMIN/MANAGER; any
transition involving system requires SYSTEM_ADMIN. Promote-to-system clears
tenant_id; demote-to-tenant sets it to the actor's tenant.
Audit history
GET /api/v1/catalogs/{type}/{id}/history → {items: [audit_entry, …]} —
newest-first append-only trail (create/update/delete/promote/demote). Each
entry records the operation, who (user_email), when, and any scope transition.
Tenant-scoped (the item must be visible to the caller).
Cross-catalog graph traversal
GET /api/v1/catalogs/{type}/{id}/relations?depth=1-3&relation=&include_proposed=
→ {start, nodes, edges} — the polymorphic concept_edges subgraph reachable
within depth hops. Powers "which organ does this biomarker affect? what treats
that disease?" via a depth-bounded, cycle-safe recursive CTE
(app/services/catalog_graph_service.py).
Whole-catalog ontology graph
GET /api/v1/catalogs/graph?types=&kind=&include_isolated=&limit= → the
entire cross-catalog ontology graph (rootless) as {nodes, edges}, with all
endpoints resolved to display payloads. Filters: types (comma-separated
EdgeEndpointType values), kind (comma-separated ConceptKind values),
include_isolated (bool, default false), limit.
Concept lifecycle — restore
POST /api/v1/concepts/{id}/restore — reverses a retire/soft-delete: sets
status back to active and clears deleted_at.
Read-only concept catalog adapter:
POST/PUT/DELETEon/catalogs/conceptreturn 405 — theConceptCatalogAdapteris read-only. All concept writes go through the/conceptsdomain endpoints (ConceptServiceenforces audit + retire/restore + RBAC).
Unified catalog search
GET /api/v1/catalogs/search?q=&types=&limit= → {results: [{type, id, label}, …]}
— typo-tolerant (pg_trgm) search across all registered catalogs at once,
tenant-scoped. types is an optional comma-separated subset.
Global search
GET /api/v1/search?q= → {results: [{id, type, title, subtitle}, …]} —
patient/examination/document/clinical-event blocks (inline ILIKE) plus the
catalog portion delegated to the registry-driven dispatcher, so anatomy,
concepts, and allergies appear automatically (not just medications + biomarkers).
Vaccines & Immunizations (Phase 5)
Vaccinations are implemented end-to-end, mirroring the medications pattern:
VaccineCatalog is the CVX-coded reference definition (the product);
PatientImmunization is the dose-administered patient-instance record.
# Catalog CRUD (RBAC: USER read-only; ADMIN/MANAGER tenant; SYSTEM_ADMIN global)
GET /api/v1/vaccines/catalog?search= → List[VaccineCatalogResponse]
GET /api/v1/vaccines/catalog/{catalog_id} → VaccineCatalogResponse
POST /api/v1/vaccines/catalog → VaccineCatalogResponse (ADMIN+)
PUT /api/v1/vaccines/catalog/{catalog_id} → VaccineCatalogResponse (global→SYSTEM_ADMIN)
DELETE /api/v1/vaccines/catalog/{catalog_id} (global→SYSTEM_ADMIN)
# Patient immunization instances (patient-access scoped)
GET /api/v1/vaccines/patient/{patient_id} → List[PatientImmunizationResponse]
POST /api/v1/vaccines/patient/{patient_id} → PatientImmunizationResponse
GET /api/v1/vaccines/{immunization_id} → PatientImmunizationResponse
PUT /api/v1/vaccines/{immunization_id} → PatientImmunizationResponse
DELETE /api/v1/vaccines/{immunization_id}FHIR: GET /api/v1/fhir/R4/Immunization?patient={id}&date=&status= returns a
searchset Bundle (the PatientImmunization row projects to a canonical R4
Immunization via to_fhir_dict()). VaccineCatalog projects to Medication.
Vaccines also appear in the unified catalog endpoints (/catalogs/vaccine,
/catalogs/search?q=&types=vaccine) and the cross-catalog graph.
Error Handling
Standard Error Response
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input data",
"details": [
{
"field": "email",
"message": "Invalid email format"
}
]
}
}HTTP Status Codes
| Code | Description |
|---|---|
| 200 | Success |
| 201 | Created |
| 202 | Accepted (async operation) |
| 204 | No Content (delete success) |
| 400 | Bad Request |
| 401 | Unauthorized (missing/invalid token) |
| 403 | Forbidden (role/tenant/patient-access denied) |
| 404 | Not Found (also returned for cross-tenant calls — no leak) |
| 410 | Gone (FHIR R4 facade: tombstone of a soft-deleted resource) |
| 422 | Validation error |
| 429 | Too Many Requests (rate limited — future) |
| 500 | Internal Server Error |
Current API Status
✅ Working Endpoints
Authentication
POST /api/v1/auth/login- Login and get tokensPOST /api/v1/auth/refresh- Refresh access tokenGET /api/v1/auth/validate- Validate current tokenPOST /api/v1/auth/register- Register new user (bootstrap or invite)POST /api/v1/auth/invite- Issue a tenant invite token (ADMIN+)GET /api/v1/users/me- Get current user
Users
GET /api/v1/users/{id}- Get user by IDPUT /api/v1/users/{id}- Update userDELETE /api/v1/users/{id}- Delete user
Tenants
GET /api/v1/tenants/{id}- Get tenantPOST /api/v1/tenants- Create tenantPUT /api/v1/tenants/{id}- Update tenantDELETE /api/v1/tenants/{id}- Delete tenant
Documents
POST /api/v1/documents- Upload documentGET /api/v1/documents/{id}- Get documentGET /api/v1/documents/{id}/download- Download documentGET /api/v1/documents/{id}/preview- Get preview image (presigned token or Bearer JWT; B5)POST /api/v1/documents/{id}/extract- Trigger extractionGET /api/v1/documents/{id}/extract/status- Get extraction status
Patients & Observations (domain endpoints — ORM-shape)
GET /api/v1/patients- List patientsPOST /api/v1/patients- Create patientGET /api/v1/patients/{id}- Get patientPUT /api/v1/patients/{id}- Update patientDELETE /api/v1/patients/{id}- Delete patientGET /api/v1/observations- List observations (patient/code/date filters)POST /api/v1/observations- Create observationGET /api/v1/observations/{id}- Get observationDELETE /api/v1/observations/{id}- Delete observation- For canonical FHIR R4 interop, see
/api/v1/fhir/R4/*(FHIR_R4_FACADE.md)
Wearable Data
POST /api/v1/wearable/data- Upload wearable dataGET /api/v1/wearable/data- Get wearable dataGET /api/v1/wearable/data/summary- Get daily summaryGET /api/v1/wearable/anomalies- Detect anomalies
Alerts (tenant-scoped; B4)
Removed. The legacy
/alerts/*endpoints andAlertModelwere deleted in the unified notification system refactor. Biomarker threshold alerts are now event-driven via thenotification_rulesengine (see Notifications below).
Analytics
GET /api/v1/analytics/dashboard- Get dashboard dataGET /api/v1/analytics/trends- Get biomarker trendsGET /api/v1/analytics/summary- Get analytics summaryGET /api/v1/analytics/reference-ranges- Get reference ranges
Notifications (unified; tenant-scoped)
See NOTIFICATION_SYSTEM.md for the full architecture (fan-out model, target resolution, rules engine).
GET /api/v1/notifications/vapid-public-key- Get VAPID key (public, no auth)POST /api/v1/notifications/subscribe- Register a Web Push subscription (body =SubscribeRequest)GET /api/v1/notifications/inbox- Personal inbox (filters: status/category/source/patient_id/limit/offset)GET /api/v1/notifications/unread-count- Bell badge countPATCH /api/v1/notifications/{recipient_id}/read- Mark inbox row readPATCH /api/v1/notifications/{recipient_id}/dismiss- Dismiss inbox rowPOST /api/v1/notifications/read-all- Mark all unread as readGET /api/v1/notifications/admin- Tenant-wide feed (ADMIN/MANAGER/SYSTEM_ADMIN; cross-tenant for SYSTEM_ADMIN)GET /api/v1/notifications/admin/stats- Aggregate delivery stats (by source/category/channel-status, unique recipients)GET /api/v1/notifications/admin/{notification_id}/delivery- Per-recipient delivery breakdown for one notificationPOST /api/v1/admin/notifications/broadcast- System broadcast (tenant or system scope)POST /api/v1/notifications/triggers- Create scheduled/recurring triggerGET /api/v1/notifications/triggers- List triggers (tenant-wide whenpatient_idomitted)DELETE /api/v1/notifications/triggers/{id}- Delete a triggerPOST /api/v1/notifications/triggers/{id}/test- Fire a trigger immediatelyGET /api/v1/notification-rules- List biomarker rulesPOST /api/v1/notification-rules- Create a biomarker rulePUT /api/v1/notification-rules/{id}- Update a ruleDELETE /api/v1/notification-rules/{id}- Delete a rulePOST /api/v1/notification-rules/{id}/test- Force-fire a ruleGET (WS) /api/v1/ws/notifications- Per-user real-time stream (Bearer subprotocol auth)
Task Monitoring (tenant-scoped except SYSTEM_ADMIN; B1)
GET /api/v1/task-monitor/documents/processing- List stuck processing documentsGET /api/v1/task-monitor/examinations/processing- List stuck processing examinationsPOST /api/v1/task-monitor/documents/retry/{id}- Retry OCR for a documentPOST /api/v1/task-monitor/examinations/retry/{id}- Retry extraction for an examinationGET /api/v1/task-monitor/stats- Aggregate task stats (tenant-scoped; global for SYSTEM_ADMIN)
Integrations & Webhooks
GET /api/v1/integrations- List available integrationsGET /api/v1/integrations/{domain}- Integration metadata / config flowPOST /api/v1/integrations/{domain}/enable- Enable an integration for the current userDELETE /api/v1/integrations/{domain}/disable- Disable an integrationPOST /api/v1/integrations/{domain}/sync/{integration_id}- Trigger a manual syncPOST /api/v1/integrations/{domain}/webhook/{integration_id}- Inbound webhook (HMAC viawebhook_secret); triggers the same notification dispatch asrun_syncANY /api/v1/integrations/{domain}/api/{integration_id}/{path}- Generic two-way API proxy (HMAC viaapi_secret+X-Api-Signature; B8)POST /api/v1/integrations/{domain}/notification-action/{integration_id}/{action_id}- Server-side handler for clicked action buttons on integration-authored notifications (see INTEGRATIONS_SDK.md §3.9)GET /api/v1/integrations/notification-types- Aggregate every enabled integration's declared notification types + the caller's per-type prefsPUT /api/v1/integrations/{domain}/notification-types/{type_id}- Set the caller's per-type preference (body:{enabled: bool})
FHIR R4 Facade (interop surface)
GET /api/v1/fhir/R4/metadata- CapabilityStatementGET/POST /api/v1/fhir/R4/{Resource}- Search / Create (15 resource types)GET/PUT/DELETE /api/v1/fhir/R4/{Resource}/{id}- Read / Update (version-bumps) / Delete (soft-delete → 410 Gone)- See FHIR_R4_FACADE.md
Export & Import (Backup)
See EXPORT_IMPORT.md for the full format/scopes/restore spec.
POST /api/v1/export- Create an export job (body:BackupRequestwithscope∈ {patient, group, system},export_type∈ {fhir_only, full_backup, catalog_only}, optionalpatient_ids). Enqueues a Celery task; returnsExportJobResponse.GET /api/v1/export/jobs- List export jobs for the current tenant.GET /api/v1/export/jobs/{job_id}- Get export job status.GET /api/v1/export/jobs/{job_id}/download- Download the generated file (FHIR Bundle.fhir.json, full-backup.zip, or catalog.catalog.json).POST /api/v1/import/backup- Upload a ZIP or barebundle.json/catalog.json(multipartfile). Enqueues a Celery task; returnsImportJobResponse.GET /api/v1/import/jobs/{job_id}- Get backup-import job status +restore_result(created/updated counts,manifest_verified,fhir_validated).POST /api/v1/import/fhir- Synchronous FHIR Bundle import (smaller bundles).POST /api/v1/import/csv- CSV import (legacy).POST /api/v1/import/ocr- OCR extraction (legacy).
Role gating: patient scope — any role (USER limited to a single own patient); group — MANAGER+; system — ADMIN+. SYSTEM_ADMIN bypasses all checks.
⚠️ Legacy Note
Some internal service functions might still use fallback logic if specific external dependencies (like local OCR engines or AI providers) are not fully configured in the .env file. However, the core database CRUD and multi-tenancy logic are fully implemented.
API Documentation
Once running, visit:
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc