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

# Authentication

> Client credentials for brand servers and prescriber systems, token exchange for patients.

Three kinds of caller use this API, each with its own token. The tokens are not interchangeable: a brand server token cannot call patient endpoints, and a patient token cannot call brand server or clinical endpoints. Coaches use the TeraRx portal, which is not part of this contract.

| Caller | Security scheme | How it gets a token | Lifetime |
| - | - | - | - |
| Brand server | `brandServer` | Client credentials with its own `client_id` and secret | 1 hour |
| Patient (brand app, or our own app's backend as TeraRx Direct) | `patientToken` | Token exchange of an assertion the brand's server signed | 15 minutes |
| Prescriber system (brand clinician tool or Beluga) | `clinicalClient` | Client credentials with a separate clinical client | 1 hour |

Each operation lists the scheme and the scope it needs. All tokens go in `Authorization: Bearer <token>`.

A platform partner that hosts several brands holds one client for all of them and names the brand on every token request, so each token is still for one brand. See [Platform partners](https://docs.tera.health/guides/partners.md).

## Brand servers: client credentials

TeraRx issues one OAuth client per brand and environment (integration, production). The secret is shown once; TeraRx keeps only its hash. Keep it on your server, never in an app. The integration client works against the sandbox and test environments only; the production API answers it with `unauthorized_client`.

| Problem | `code` | HTTP |
| - | - | - |
| Unknown client or wrong secret (the same answer for both) | `invalid_client` | 401 |
| The client was revoked | `client_revoked` | 401, also for tokens it already holds, within a minute |
| A `scope` the client does not hold | `invalid_scope` | 400 |
| The integration client on the production API | `unauthorized_client` | 400 |
| A partner client sent no `tenant_id` | `tenant_required` | 400 |
| A `tenant_id` this client may not act for, or a brand that is not live | `tenant_not_permitted` | 400 |

```http theme={null}
POST /v1/oauth/token
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&scope=enrollments:read%20enrollments:write%20logs:write
```

| Scope | Allows |
| - | - |
| `enrollments:read` | Read enrollment status, the treatment mirror and consent records |
| `enrollments:write` | Enroll, change plan, pause, resume, cancel, update the treatment mirror, record consent proof |
| `logs:write` | Send logs your app already recorded |
| `events:read` | Replay events |
| `reports:read` | The aggregate brand report |
| `data_requests:read`, `data_requests:write` | Read and create data requests; decide amendments |
| `patients:token_exchange` | Your signing keys may mint patient tokens (see below) |
| `tenants:read` | List the brands your client may act for |

A brand server reads status and ids only. It never reads clinical content: messages, form answers, protocol text and log values stay with the patient and the care team.

## Patients: token exchange

There are no patient passwords in the API. Your server vouches for the patient by signing a short-lived JWT with **your own private key**; TeraRx checks it against the public keys you registered (a JWKS URL or a JWKS document) and returns a patient token. TeraRx never holds a key that can mint patient sessions, and you can rotate keys yourself by publishing a new key in your JWKS.

1. Your app signs the patient in with your own login.
2. Your server signs a patient assertion (below) and hands it to the app.
3. The app (or your server) exchanges it:

```http theme={null}
POST /v1/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=<assertion>
&subject_token_type=urn:ietf:params:oauth:token-type:jwt
```

4. The response's `access_token` is valid for 15 minutes for that one patient and enrollment. There is no refresh token: sign a new assertion when it expires.

### Patient assertion

A JWT (RFC 7519) signed with `ES256`, `RS256` or `EdDSA`. The header names the key with `kid`, which must be in your registered JWKS.

| Claim | Required | Value |
| - | - | - |
| `iss` | Yes | Your issuer, exactly as registered with TeraRx at onboarding |
| `sub` | Yes | Your patient id: the `external_patient_id` you used on `POST /v1/enrollments` |
| `aud` | Yes | The TeraRx API audience for the environment, given at onboarding |
| `iat` | Yes | Issued at, in Unix seconds |
| `exp` | Yes | Expiry, at most 5 minutes after `iat` |
| `jti` | Yes | A unique id. Each assertion can be exchanged once |
| `tenant_id` | Platform partners only | The brand the patient is enrolled with. A brand's own assertions may omit it |

TeraRx allows 30 seconds of clock skew on `iat` and `exp`. The assertion carries no names or health data.

If you registered a JWKS URL, TeraRx caches it for 10 minutes and fetches it again as soon as an assertion names a `kid` it has not seen (at most every 30 seconds). To rotate, publish the new key next to the old one, start signing with it, then remove the old one.

| Problem | `code` | HTTP |
| - | - | - |
| Signature does not match a registered key | `assertion_signature_invalid` | 400 |
| `exp` has passed | `assertion_expired` | 400 |
| `exp` is more than 5 minutes after `iat` | `assertion_lifetime_too_long` | 400 |
| `jti` already used | `assertion_replayed` | 400 |
| Wrong `aud` | `assertion_audience_invalid` | 400 |
| `iss` not registered | `assertion_issuer_unknown` | 400 |
| A required claim is missing or malformed, or `iat` is in the future | `assertion_invalid` | 400 |
| A partner's assertion names a brand the partner may not act for, or one that is not live | `tenant_not_permitted` | 400 |
| No open enrollment for `sub` | `patient_not_found` | 400 |
| The enrollment is cancelled, completed or ineligible | `enrollment_not_active` | 400 |
| The client lacks `patients:token_exchange` | `token_exchange_not_allowed` | 400 |
| The client was revoked | `client_revoked` | 401, also for the patient tokens it already exchanged, within a minute |
| Your client's exchanges passed its per-minute limit | `rate_limited` | 429 |
| TeraRx could not fetch your JWKS URL, or could not check the `jti` | `service_unavailable` | 503 |

Each problem also carries the RFC 6749 `error` field (`invalid_grant`, `invalid_client`, `unauthorized_client` ...) for OAuth libraries.

## Prescriber systems: clinical clients

Your prescriber system gets its own OAuth client, separate from the brand server's, with `clinical:read` (read flags) and `clinical:write` (acknowledge and resolve). It authenticates with client credentials like a brand server.

A clinical client acts for many prescribers, so every clinical write names the individual in the request body:

```json theme={null}
{
  "on_behalf_of": { "npi": "1234567893", "prescriber_id": "dr-00417", "name": "Dr. Alex Morgan, NP" }
}
```

`name` is required, plus at least one of `npi` or `prescriber_id`. A write without `on_behalf_of`, or with an NPI whose check digit is wrong, is refused with `422 validation_failed` and the field's `pointer`. TeraRx stores the prescriber and the client that sent it on the flag, shows the name to coaches, and records it in the audit log.


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