> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tera.health/llms.txt
> Use this file to discover all available pages before exploring further.

# Conventions

> Versioning, formats, errors, idempotency, pagination, rate limits and privacy.

## Base URL and versioning

All paths start with the major version, `/v1`. Within a version, changes are additive only: new endpoints, new optional request fields, new response fields, new enum values and new event types. Build clients that ignore fields and values they do not know.

A breaking change means a new version (`/v2`), announced at least **6 months** ahead. During that time the old version's responses carry `Deprecation` (RFC 9745) and `Sunset` (RFC 8594) headers with the dates, and both versions run side by side.

## Formats

* JSON in and out (`application/json`; `application/merge-patch+json` for `PATCH`). UTF-8.
* Ids are UUID strings. Treat them as opaque.
* Timestamps are ISO 8601 in UTC (`2026-10-26T14:05:00Z`). Log dates (`log_date`) are calendar dates in the patient's time zone.
* Measurements are always metric (kg, cm, ml, g, kcal). `units` on the patient is a display preference only.
* Field names are `snake_case`. Absent and `null` both mean "no value".

## Request ids

Every response has an `X-Request-Id` header, errors included. Ours are `req_` followed by a UUIDv7. Send your own `X-Request-Id` to have it echoed, which makes logs easy to join: a UUID, or `req_` followed by up to 60 letters, digits, `_` or `-`. Any other value is replaced with a new id. Quote it when you contact TeraRx.

## Errors

Every 4xx and 5xx response is RFC 9457 problem details (`application/problem+json`):

```json theme={null}
{
  "type": "about:blank",
  "title": "Unprocessable Content",
  "status": 422,
  "detail": "1 field is invalid.",
  "code": "validation_failed",
  "request_id": "req_019a3b2c-4d5e-7f60-8a1b-2c3d4e5f6a7b",
  "errors": [{ "pointer": "/patient/state_code", "code": "invalid_format", "detail": "Must be a two-letter USPS state code." }]
}
```

Branch on `code`, which is stable; `title` and `detail` may change. Errors never contain patient data: no names, no values you sent, no free text. A resource from another brand or another patient returns `404` exactly like one that does not exist.

| Status | Common `code` values |
| - | - |
| 400 | `malformed_request`, `idempotency_key_missing` |
| 401 | `token_missing`, `token_expired`, `token_invalid`, `client_revoked` |
| 403 | `wrong_caller_type` (a token of another kind, such as a patient token on a brand server endpoint), `insufficient_scope`, `plan_not_eligible` (a coached-plan endpoint on `ai_only`), `treatment_managed_by_brand` |
| 404 | `<resource>_not_found`, for example `enrollment_not_found` |
| 409 | `invalid_status_transition`, `idempotency_key_in_use`, `enrollment_exists`, `external_order_exists`, `flag_closed`, `ai_paused` |
| 410 | `export_expired`, `event_too_old` |
| 422 | `validation_failed`, `idempotency_key_reused`, `plan_not_offered`, `unknown_medication`, `unknown_legal_document` |
| 429 | `rate_limited`, `daily_message_cap_reached` |
| 503 | `ai_unavailable`, `service_unavailable` |

Each operation lists the codes specific to it.

## Idempotency

Every `POST` that creates something requires an `Idempotency-Key` header (a UUID is a good choice); without it the request is refused with `400 idempotency_key_missing`. Other `POST` actions accept one.

* The same key and the same body within **24 hours** returns the first response, with `Idempotent-Replayed: true`, and creates nothing new.
* The same key with a different body is refused with `422 idempotency_key_reused`.
* While the first request with a key is still running, a second one gets `409 idempotency_key_in_use`; retry shortly.

Keys are scoped to your client. Logs your server sends also carry your own `external_id`, which dedupes them permanently.

## Pagination

Lists that can grow are cursor-paginated: `?limit=` (1 to 100, default 25) and `?cursor=`. The response is `{ "data": [...], "next_cursor": "..." }`; pass `next_cursor` back as `cursor` until it is `null`. Cursors are opaque and expire after 24 hours. Short, bounded lists return `{ "data": [...] }` without a cursor.

## Rate limits

Limits are per client (600 requests per minute by default, set per contract) and, for patient tokens, per patient. Every response carries:

| Header | Meaning |
| - | - |
| `RateLimit-Limit` | Requests allowed per window |
| `RateLimit-Remaining` | Requests left |
| `RateLimit-Reset` | Seconds until the window resets |

Over the limit you get `429` with `Retry-After` in seconds. The AI Coach also has a daily message cap per patient, set on the brand's plan (`429 daily_message_cap_reached`).

## Plans

Patient endpoints say which plans they work on (the `x-plans` extension, and "Plans" at the end of each description). `ai_health_fitness` is the coached plan; `ai_only` has the AI Coach and no coaches, and its escalations go to the brand's prescriber as clinical flags. Calling a coached-plan endpoint for an `ai_only` patient returns `403 plan_not_eligible`.

## Privacy

* A patient's records belong to one brand. Nothing is shared, matched or merged across brands.
* Brand servers read status and ids, never clinical content. Reports are aggregates with groups under 10 patients suppressed.
* Webhooks carry ids only. Fetch content through the API.
* Files are uploaded and downloaded through short-lived pre-signed URLs. Never store those URLs.
* Every read or change of a coaching record is audit-logged.

## Emergencies

Coaching is not emergency care. Wherever a patient can type (care messages, the AI Coach, symptom logging), show this guidance: **in an emergency call 911; in a mental health crisis call or text 988.** When a symptom log comes back with `is_red_flag: true`, or an AI Coach reply with `show_emergency_guidance: true`, show it before anything else.

## Environments

| Environment | Base URL | Data |
| - | - | - |
| Local mock | `http://localhost:4010` | The examples in this spec |
| Sandbox (integration) | `https://sandbox.rx.tera.health` | Synthetic only. Until the API is live it is this mock: the examples, no webhooks. Then flags and webhooks fire; nothing is billed |
| Production | `https://api.rx.tera.health` | Real patients |

Each environment has its own clients, keys and webhook secret.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.