# RxRelay API Reference

Submit clinic orders from an external platform through RxRelay. The v1 API supports
the clinic's routed medication catalog, patient upsert, required idempotency on writes, and
synchronous vendor dispatch once live access is enabled.

## Base URL

```
https://api.rxrelay.ai/v1
```

All endpoints are versioned under `/v1`. Keys issued from the app start in **sandbox**
mode — submissions are recorded and returned for review but are not dispatched to the pharmacy
until a platform admin enables live access.

## Authentication

API keys are clinic-scoped and managed from the RxRelay app under **Settings → API**. Pass
the key as a bearer token (or in the `X-API-Key` header). Sandbox keys are prefixed
`rxrelay_sandbox_`; live keys are prefixed `rxrelay_live_`.

```
Authorization: Bearer rxrelay_sandbox_your_key
```

### Authorizations

- `Authorization` (string, header, **required**) — Bearer token using your clinic API key,
  e.g. `Bearer rxrelay_sandbox_your_key`. A key with the `read_write` scope is required to create orders.

### Clinic vs. organization keys

Keys come in two scopes:

- **Clinic-scoped keys** act on a single clinic. They are the default and cover everything below
  without naming a clinic on each request.
- **Organization-scoped keys** act on behalf of an organization that owns many clinics (e.g. a
  partner managing clinics A, B, C). Org keys **must** name the target clinic on each order via
  `clinicId` (RxRelay's UUID) or `externalClinicId` (your own identifier), and — when enabled by
  RxRelay — can provision clinics and users through the [Organizations](#organizations) endpoints.

## OpenAPI

The public partner API specification is available as OpenAPI 3 at:

```
https://api.rxrelay.ai/openapi/v1.json
```

This spec only includes upstream partner endpoints under `/v1` plus documented webhook payload schemas.
Internal RxRelay app and admin endpoints are not included.

## Rate limits

The v1 partner API starts at **300 requests per minute per API key**. If your clinic needs a higher
limit, contact RxRelay and we can raise it after reviewing expected traffic.

Rate-limited responses return `429 Too Many Requests` and include:

- `Retry-After` — Seconds to wait before retrying.
- `X-RateLimit-Limit` — Current request limit.
- `X-RateLimit-Remaining` — Remaining requests, usually `0` on a rejected request.
- `X-RateLimit-Reset` — Unix timestamp when the limit resets.

## Errors

Errors use the standard Problem Details JSON shape with a stable `code` extension and an
`X-Request-Id` response header. Include the request ID when contacting support.

```json
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "Invalid order request",
  "detail": "At least one order item is required.",
  "status": 400,
  "code": "invalid_order_request",
  "requestId": "req_2c3d4e5f..."
}
```

## Idempotency

`POST /v1/orders` requires an `Idempotency-Key` header. Keys are scoped to the clinic API key.
Retrying the same request with the same key returns the original order response; reusing the same key
with a different body returns `409 Conflict`.

Use a stable value from your system, such as `partner-order-123`.

## Going live

New API keys are created in sandbox mode so RxRelay and the clinic can verify the integration before
any order is dispatched to the pharmacy network. Use this flow for launch:

1. Create a sandbox `read_write` key from **Settings → API**.
2. Fetch `GET /v1/medications` and submit representative sandbox orders with required idempotency keys.
3. Add a webhook endpoint in **Settings → API**, copy the one-time endpoint secret, and send a test event to verify signature handling.
4. RxRelay reviews the recorded sandbox orders and webhook deliveries for the clinic.
5. A platform admin enables live access, which creates a separate `rxrelay_live_` key. Copy that key from **Settings → API** and use it for production order submission.

Sandbox keys remain available for test submissions after live access is enabled. Sandbox orders return
synchronously after validation and recording. Live orders return after synchronous vendor dispatch, and
configured webhooks receive order status events asynchronously after the API response.

## Get clinic

`GET /v1/clinic`

Returns your clinic's record, including its RxRelay `id` and whether it is in sandbox mode. The clinic is
taken from your API key. Organization keys manage many clinics and should use `GET /v1/clinics` instead.

**Request**

```bash
curl https://api.rxrelay.ai/v1/clinic \
  -H "Authorization: Bearer rxrelay_sandbox_your_key"
```

**Response** `200`

```json
{
  "id": "11111111-1111-1111-1111-111111111111",
  "name": "Northside Clinic",
  "externalClinicId": null,
  "address1": "123 Main St",
  "address2": null,
  "city": "Dallas",
  "state": "TX",
  "zip": "75201",
  "phoneNumber": "5555555555",
  "contactEmail": "ops@northsideclinic.com",
  "isSandboxMode": true,
  "createdAt": "2026-06-19T12:30:00.0000000+00:00"
}
```

### Response fields

- `id` (string · uuid) — Your RxRelay clinic id.
- `name` (string) — Clinic name.
- `externalClinicId` (string | null) — Your own identifier for the clinic if set under an organization; `null` for standalone clinics.
- `address1`, `address2`, `city`, `state`, `zip` (string | null) — Clinic address.
- `phoneNumber`, `contactEmail` (string | null) — Clinic contact details.
- `isSandboxMode` (boolean) — Whether the clinic is in sandbox mode.
- `createdAt` (string · date-time) — When the clinic was created.

## List medications

`GET /v1/medications`

Returns the authenticated clinic's routed medication catalog, ordered by name. The list is scoped to medications with a configured route through that clinic's pharmacy access; medications outside this list cannot be submitted.

By default the catalog is unified across all pharmacies the clinic can dispense from. To retrieve a single pharmacy's formulary, pass `pharmacyId` (an id from `GET /v1/pharmacies`).

**Request**

```bash
curl https://api.rxrelay.ai/v1/medications \
  -H "Authorization: Bearer rxrelay_sandbox_your_key"
```

**Filter by pharmacy**

```bash
curl "https://api.rxrelay.ai/v1/medications?pharmacyId=Kaduceus:opbrx" \
  -H "Authorization: Bearer rxrelay_sandbox_your_key"
```

**Response** `200`

```json
[
  {
    "id": "7c9e6a1b-1f4d-4a2e-9c3b-9a1d2e3f4a5b",
    "name": "Semaglutide 2.5mg/mL",
    "strength": "2.5 mg/mL",
    "form": "Injectable",
    "pharmacyName": "Boothwyn Pharmacy",
    "pharmacySku": "SEMA-25-VIAL",
    "ingredients": []
  },
  {
    "id": "0b8d2f31-6a7c-4d5e-8f90-1a2b3c4d5e6f",
    "name": "RENEW",
    "strength": null,
    "form": "Injectable",
    "pharmacyName": "Kaduceus Pharmacy",
    "pharmacySku": "11111222219",
    "ingredients": [
      { "name": "MOTS-C", "concentration": 2, "unit": "mg/mL" },
      { "name": "BPC-157", "concentration": 2, "unit": "mg/mL" },
      { "name": "GHK-Cu", "concentration": 2, "unit": "mg/mL" },
      { "name": "Kisspeptin-10", "concentration": 10, "unit": "mg/mL" }
    ]
  }
]
```

### Query parameters

- `pharmacyId` (string | null, query) — Restrict the catalog to a single dispensing pharmacy, using an `id` from `GET /v1/pharmacies`. Omit for the clinic's full unified catalog. A malformed id returns `400`; a well-formed id the clinic isn't entitled to returns an empty array.
- `clinicId` (string · uuid | null, query) — Organization keys only. The RxRelay clinic to scope to. Provide this or `externalClinicId`. Ignored for clinic-scoped keys.
- `externalClinicId` (string | null, query) — Organization keys only. Your own identifier for the target clinic.

### Response fields

- `[].id` (string · uuid) — Medication ID. Use as `items[].medicationId` when creating an order.
- `[].name` (string) — Display name of the medication.
- `[].strength` (string | null) — Strength / concentration (e.g. `2.5 mg/mL`), when the pharmacy provides it.
- `[].form` (string | null) — Dosage form (e.g. `Injectable`, `Tablet`), when available.
- `[].pharmacyName` (string | null) — Display name of the fulfilling pharmacy for this medication.
- `[].pharmacySku` (string | null) — The fulfilling pharmacy's own product identifier (the downstream SKU), when available.
- `[].ingredients` (object[]) — Structured composition, when the pharmacy provides it (empty otherwise). For a multi-active blend, use this to show what's in the vial — `strength` alone can't describe it. Not required to order.
  - `[].ingredients[].name` (string) — Ingredient (active) name, e.g. `BPC-157`.
  - `[].ingredients[].concentration` (number | null) — Amount per mL (or per vial for `mg` units).
  - `[].ingredients[].unit` (string | null) — Concentration unit, e.g. `mg/mL`.

## List pharmacies

`GET /v1/pharmacies`

Returns the distinct pharmacies the authenticated clinic can dispense from. Each carries a stable, opaque `id` you pass as the `pharmacyId` filter on `GET /v1/medications` to retrieve that pharmacy's formulary. Use this when you route across multiple pharmacies and need per-pharmacy catalogs.

**Request**

```bash
curl https://api.rxrelay.ai/v1/pharmacies \
  -H "Authorization: Bearer rxrelay_sandbox_your_key"
```

**Response** `200`

```json
[
  {
    "id": "Boothwyn",
    "name": "Boothwyn Pharmacy",
    "platform": "Boothwyn"
  },
  {
    "id": "Kaduceus:opbrx",
    "name": "Kaduceus Pharmacy",
    "platform": "Kaduceus"
  }
]
```

### Response fields

- `[].id` (string) — Opaque, stable pharmacy identifier. Treat it as a string and pass it back verbatim as `pharmacyId` on `GET /v1/medications`.
- `[].name` (string | null) — Friendly display name of the pharmacy, when one has been assigned.
- `[].platform` (string) — The fulfillment network the pharmacy belongs to.

## List prescribers

`GET /v1/prescribers`

Returns prescribers in the clinic that have an NPI on file, ordered by name.

**Request**

```bash
curl https://api.rxrelay.ai/v1/prescribers \
  -H "Authorization: Bearer rxrelay_sandbox_your_key"
```

**Response** `200`

```json
[
  {
    "userId": "3f1a8c7d-2b4e-4f6a-9c8d-7e6f5a4b3c2d",
    "firstName": "Alex",
    "lastName": "Reed",
    "email": "alex.reed@northsideclinic.com",
    "npi": "1234567890",
    "licenseState": "TX",
    "licenseNumber": "MD-44821"
  }
]
```

### Response fields

- `[].userId` (string · uuid) — Prescriber's RxRelay user ID. Pass as `prescriberUserId` when creating an order.
- `[].firstName` (string | null) — Prescriber's first name.
- `[].lastName` (string | null) — Prescriber's last name.
- `[].email` (string | null) — Prescriber's email address.
- `[].npi` (string) — National Provider Identifier. Pass as `prescriberNpi` when creating an order.
- `[].licenseState` (string | null) — State that issued the prescriber's license.
- `[].licenseNumber` (string | null) — Prescriber's license number.

## Register a user

`POST /v1/users`

Creates (or updates) a user in your clinic and assigns clinic roles — use it to register prescribers
programmatically instead of the onboarding spreadsheet. The clinic is taken from your API key, so no clinic
reference is needed. A user with the `prescriber` role must include an `npi`, after which it can be
referenced on orders by `prescriberNpi`. Idempotent per email: an existing email links to that user and
merges roles rather than creating a duplicate. Requires a `read_write` key.

**Request**

```bash
curl -X POST https://api.rxrelay.ai/v1/users \
  -H "Authorization: Bearer rxrelay_sandbox_your_key" \
  -H "Content-Type: application/json" \
  -d '{
  "email": "alex.reed@northsideclinic.com",
  "firstName": "Alex",
  "lastName": "Reed",
  "roles": ["prescriber"],
  "npi": "1234567890",
  "licenseState": "TX",
  "licenseNumber": "MD-44821"
}'
```

**Response** `200`

```json
{
  "userId": "3f1a8c7d-2b4e-4f6a-9c8d-7e6f5a4b3c2d",
  "email": "alex.reed@northsideclinic.com",
  "firstName": "Alex",
  "lastName": "Reed",
  "roles": ["prescriber"],
  "npi": "1234567890",
  "joinedAt": "2026-06-19T12:31:00.0000000+00:00"
}
```

### Body

- `email` (string, **required**) — The user's email. Used to match an existing RxRelay user or create one.
- `firstName`, `lastName` (string | null) — User's name.
- `roles` (string[], **required**) — Clinic roles: `prescriber`, `clinic_admin`, `staff`, or `owner`. `platform_admin` cannot be assigned via the API.
- `npi` (string | null) — Required when `roles` includes `prescriber`.
- `licenseState`, `licenseNumber`, `phone`, `deaNumber` (string | null) — Optional professional fields.

### Response fields

- `userId` (string · uuid) — The user's RxRelay ID.
- `email` (string) — The user's email.
- `firstName`, `lastName` (string | null) — User's name.
- `roles` (string[]) — The user's roles in this clinic.
- `npi` (string | null) — National Provider Identifier, if set.
- `joinedAt` (string · date-time) — When the user joined the clinic.

## List users

`GET /v1/users`

Returns every user in your clinic with their roles and NPI. Use it to check whether a prescriber's NPI is
already registered before submitting an order. The clinic is taken from your API key.

**Request**

```bash
curl https://api.rxrelay.ai/v1/users \
  -H "Authorization: Bearer rxrelay_sandbox_your_key"
```

**Response** `200`

```json
[
  {
    "userId": "3f1a8c7d-2b4e-4f6a-9c8d-7e6f5a4b3c2d",
    "email": "alex.reed@northsideclinic.com",
    "firstName": "Alex",
    "lastName": "Reed",
    "roles": ["prescriber"],
    "npi": "1234567890",
    "joinedAt": "2026-06-19T12:31:00.0000000+00:00"
  }
]
```

### Response fields

- `[].userId` (string · uuid) — The user's RxRelay ID.
- `[].email` (string) — The user's email.
- `[].firstName`, `[].lastName` (string | null) — User's name.
- `[].roles` (string[]) — The user's roles in this clinic.
- `[].npi` (string | null) — National Provider Identifier, if set.
- `[].joinedAt` (string · date-time) — When the user joined the clinic.

## List patients

`GET /v1/patients`

Lists or searches patients in the clinic, ordered by name. `search` is a case-insensitive substring match
over name, email, and phone, plus an exact match on date of birth. `email` and `externalPatientId` are
exact-match filters. Paginated with `limit` (default 25, max 100) and `offset`.

**Request**

```bash
curl "https://api.rxrelay.ai/v1/patients?search=jane&limit=25&offset=0" \
  -H "Authorization: Bearer rxrelay_sandbox_your_key"
```

**Response** `200`

```json
{
  "patients": [
    {
      "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
      "externalPatientId": "patient-456",
      "firstName": "Jane",
      "lastName": "Doe",
      "dateOfBirth": "1980-01-10",
      "sex": "F",
      "phone": "5555555555",
      "email": "jane@example.com",
      "createdAt": "2026-06-05T12:15:00.0000000+00:00",
      "addresses": [
        {
          "id": "9b8a7c6d-5e4f-4321-9a8b-7c6d5e4f3210",
          "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
          "address1": "123 Main St",
          "address2": "Apt 4B",
          "city": "Dallas",
          "state": "TX",
          "zip": "75201",
          "isDefault": true,
          "isActive": true,
          "createdAt": "2026-06-05T12:15:00.0000000+00:00"
        }
      ]
    }
  ],
  "total": 1,
  "limit": 25,
  "offset": 0
}
```

### Query parameters

- `clinicId` (string · uuid | null) — **Organization keys only.** The RxRelay clinic to scope to. Provide this or `externalClinicId`.
- `externalClinicId` (string | null) — **Organization keys only.** Your own identifier for the target clinic.
- `search` (string | null) — Case-insensitive substring over name / email / phone, plus an exact date-of-birth match.
- `email` (string | null) — Exact (case-insensitive) email match.
- `externalPatientId` (string | null) — Exact match on your patient identifier.
- `limit` (integer, default `25`) — Page size, between 1 and 100.
- `offset` (integer, default `0`) — Number of records to skip.

### Response

- `patients` (object[]) — The page of matching patients.
  - `patientId` (string · uuid) — RxRelay patient ID.
  - `externalPatientId` (string | null) — Your mapped patient identifier, if one exists.
  - `firstName` (string) — Patient's first name.
  - `lastName` (string) — Patient's last name.
  - `dateOfBirth` (string · date) — ISO date, e.g. `1980-01-10`.
  - `sex` (string | null) — One of `M`, `F`, or `U`.
  - `phone` (string | null) — Patient phone number.
  - `email` (string | null) — Patient email.
  - `createdAt` (string · date-time) — When the patient was created.
  - `addresses` (object[]) — The patient's active addresses, default first. Same shape as [Patient addresses](#patient-addresses).
- `total` (integer) — Total matching patients across all pages.
- `limit` (integer) — The effective page size applied.
- `offset` (integer) — The offset applied.

## Get a patient

`GET /v1/patients/{patientId}`

Fetches a single patient by RxRelay ID, including its active addresses (default first). Returns
`404 Not Found` if the patient does not belong to your clinic.

**Request**

```bash
curl https://api.rxrelay.ai/v1/patients/b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e \
  -H "Authorization: Bearer rxrelay_sandbox_your_key"
```

**Response** `200`

```json
{
  "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
  "externalPatientId": "patient-456",
  "firstName": "Jane",
  "lastName": "Doe",
  "dateOfBirth": "1980-01-10",
  "sex": "F",
  "phone": "5555555555",
  "email": "jane@example.com",
  "createdAt": "2026-06-05T12:15:00.0000000+00:00",
  "addresses": [
    {
      "id": "9b8a7c6d-5e4f-4321-9a8b-7c6d5e4f3210",
      "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
      "address1": "123 Main St",
      "address2": "Apt 4B",
      "city": "Dallas",
      "state": "TX",
      "zip": "75201",
      "isDefault": true,
      "isActive": true,
      "createdAt": "2026-06-05T12:15:00.0000000+00:00"
    }
  ]
}
```

### Path parameters

- `patientId` (string · uuid, **required**) — The RxRelay patient ID.

Organization-scoped keys may pass `clinicId` or `externalClinicId` as a query parameter.

## Create a patient

`POST /v1/patients`

Resolves or creates a patient outside of an order, using the same matching as order creation: first by
`externalPatientId`, then by normalized email; a new patient is created when none match.
`firstName`, `lastName`, and `dateOfBirth` are required to create. Requires a `read_write` key.

**Request**

```bash
curl -X POST https://api.rxrelay.ai/v1/patients \
  -H "Authorization: Bearer rxrelay_sandbox_your_key" \
  -H "Content-Type: application/json" \
  -d '{
  "externalPatientId": "patient-456",
  "firstName": "Jane",
  "lastName": "Doe",
  "dateOfBirth": "1980-01-10",
  "sex": "F",
  "phone": "5555555555",
  "email": "jane@example.com",
  "defaultAddress": {
    "address1": "123 Main St",
    "address2": "Apt 4B",
    "city": "Dallas",
    "state": "TX",
    "zip": "75201",
    "isDefault": true
  }
}'
```

**Request body**

```json
{
  "externalPatientId": "patient-456",
  "firstName": "Jane",
  "lastName": "Doe",
  "dateOfBirth": "1980-01-10",
  "sex": "F",
  "phone": "5555555555",
  "email": "jane@example.com",
  "defaultAddress": {
    "address1": "123 Main St",
    "address2": "Apt 4B",
    "city": "Dallas",
    "state": "TX",
    "zip": "75201",
    "isDefault": true
  }
}
```

**Response** `200`

```json
{
  "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
  "externalPatientId": "patient-456",
  "firstName": "Jane",
  "lastName": "Doe",
  "dateOfBirth": "1980-01-10",
  "sex": "F",
  "phone": "5555555555",
  "email": "jane@example.com",
  "createdAt": "2026-06-05T12:15:00.0000000+00:00",
  "addresses": [
    {
      "id": "9b8a7c6d-5e4f-4321-9a8b-7c6d5e4f3210",
      "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
      "address1": "123 Main St",
      "address2": "Apt 4B",
      "city": "Dallas",
      "state": "TX",
      "zip": "75201",
      "isDefault": true,
      "isActive": true,
      "createdAt": "2026-06-05T12:15:00.0000000+00:00"
    }
  ]
}
```

### Body

- `externalPatientId` (string | null) — Your stable patient identifier. Used for upsert matching within the clinic.
- `firstName` (string | null) — Required when creating a new patient.
- `lastName` (string | null) — Required when creating a new patient.
- `dateOfBirth` (string · date | null) — ISO date, e.g. `1980-01-10`. Required when creating a new patient.
- `sex` (string | null) — One of `M`, `F`, or `U`.
- `phone` (string | null) — Patient phone number.
- `email` (string | null) — Patient email. Used as a fallback match when `externalPatientId` is absent.
- `defaultAddress` (object | null) — An optional default address to store on the new or resolved patient.
  - `address1` (string, **required**) — Street address.
  - `address2` (string | null) — Apartment, suite, unit, etc.
  - `city` (string, **required**) — City.
  - `state` (string, **required**) — Two-letter US state code, e.g. `TX`.
  - `zip` (string, **required**) — ZIP / postal code.
  - `isDefault` (boolean, default `true`) — Whether to make this the patient's default address.

## Update a patient

`PATCH /v1/patients/{patientId}`

Updates a patient's demographics. Supplied (non-null) fields overwrite; omitted or null fields are left
unchanged. Setting `externalPatientId` (re)maps your identifier to this patient, and returns
`409 Conflict` if it is already mapped to a different patient in the clinic. Requires a `read_write` key.

**Request**

```bash
curl -X PATCH https://api.rxrelay.ai/v1/patients/b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e \
  -H "Authorization: Bearer rxrelay_sandbox_your_key" \
  -H "Content-Type: application/json" \
  -d '{
  "phone": "5125550199",
  "email": "jane.doe@example.com",
  "externalPatientId": "patient-456"
}'
```

**Request body**

```json
{
  "phone": "5125550199",
  "email": "jane.doe@example.com",
  "externalPatientId": "patient-456"
}
```

**Response** `200`

```json
{
  "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
  "externalPatientId": "patient-456",
  "firstName": "Jane",
  "lastName": "Doe",
  "dateOfBirth": "1980-01-10",
  "sex": "F",
  "phone": "5555555555",
  "email": "jane@example.com",
  "createdAt": "2026-06-05T12:15:00.0000000+00:00",
  "addresses": [
    {
      "id": "9b8a7c6d-5e4f-4321-9a8b-7c6d5e4f3210",
      "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
      "address1": "123 Main St",
      "address2": "Apt 4B",
      "city": "Dallas",
      "state": "TX",
      "zip": "75201",
      "isDefault": true,
      "isActive": true,
      "createdAt": "2026-06-05T12:15:00.0000000+00:00"
    }
  ]
}
```

### Body

- `firstName` (string | null) — New first name.
- `lastName` (string | null) — New last name.
- `dateOfBirth` (string · date | null) — New date of birth, ISO date.
- `sex` (string | null) — One of `M`, `F`, or `U`.
- `phone` (string | null) — New phone number.
- `email` (string | null) — New email.
- `externalPatientId` (string | null) — (Re)maps your patient identifier to this patient. `409 Conflict` if already mapped to a different patient.

## Create an order

`POST /v1/orders`

Submits an order with one or more Rx items. The patient is resolved in order of precedence:
first by `patientId` (a direct match to an existing RxRelay patient — when set, all other patient
fields are ignored), then by `externalPatientId`, then by normalized email; a new patient is
created when none match. Every item must reference a medication from `GET /v1/medications`.

**Request**

```bash
curl -X POST https://api.rxrelay.ai/v1/orders \
  -H "Authorization: Bearer rxrelay_sandbox_your_key" \
  -H "Idempotency-Key: partner-order-123" \
  -H "Content-Type: application/json" \
  -d '{
  "externalOrderId": "partner-order-123",
  "metadata": {
    "key1": "value1",
    "key2": "value2"
  },
  "patient": {
    "externalPatientId": "patient-456",
    "firstName": "Jane",
    "lastName": "Doe",
    "dateOfBirth": "1980-01-10",
    "sex": "F",
    "email": "jane@example.com",
    "phone": "5555555555"
  },
  "shippingAddress": {
    "address1": "123 Main St",
    "address2": "Apt 4B",
    "city": "Dallas",
    "state": "TX",
    "zip": "75201"
  },
  "prescriberNpi": "1234567890",
  "shippingMethod": "Standard",
  "items": [
    {
      "externalPrescriptionId": "rx-001",
      "medicationId": "7c9e6a1b-1f4d-4a2e-9c3b-9a1d2e3f4a5b",
      "quantity": "30",
      "directions": "Take one capsule daily",
      "refills": 2
    }
  ]
}'
```

**Response** `201`

```json
{
  "orderId": "a4d9f0e2-7b1c-4e3a-8f5d-2c6b9a0e1f3d",
  "externalOrderId": "partner-order-123",
  "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
  "status": "SandboxReceived",
  "createdAt": "2026-06-05T12:30:00.0000000+00:00",
  "metadata": {
    "key1": "value1",
    "key2": "value2"
  },
  "items": [
    {
      "orderItemId": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f",
      "medicationId": "7c9e6a1b-1f4d-4a2e-9c3b-9a1d2e3f4a5b",
      "medicationName": "Semaglutide 2.5mg/mL",
      "externalPrescriptionId": "rx-001",
      "status": "SandboxReceived"
    }
  ],
  "submissions": [
    {
      "submissionId": "d3e4f5a6-b7c8-4d9e-0f1a-2b3c4d5e6f70",
      "environment": "Sandbox",
      "status": "SandboxReceived",
      "orderItemIds": ["c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f"]
    }
  ]
}
```

**Alternative body using `shippingAddressId`**

```json
{
  "externalOrderId": "partner-order-124",
  "patient": {
    "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e"
  },
  "shippingAddressId": "9b8a7c6d-5e4f-4321-9a8b-7c6d5e4f3210",
  "prescriberNpi": "1234567890",
  "shippingMethod": "Standard",
  "items": [
    {
      "externalPrescriptionId": "rx-002",
      "medicationId": "7c9e6a1b-1f4d-4a2e-9c3b-9a1d2e3f4a5b",
      "quantity": "30",
      "directions": "Take one capsule daily",
      "refills": 2
    }
  ]
}
```

### Headers

- `Authorization` (string, header, **required**) — Bearer token using a `read_write` API key.
- `Idempotency-Key` (string, header, **required**) — Unique key for this order. Replaying the same
  key with the same body returns the original order; reusing it with a different body returns `409 Conflict`.

### Body

- `clinicId` (string · uuid | null) — **Organization keys only.** The RxRelay clinic the order is for. Provide this or `externalClinicId`. Ignored for clinic-scoped keys.
- `externalClinicId` (string | null) — **Organization keys only.** Your own identifier for the target clinic (set when the clinic was created). Provide this or `clinicId`.
- `externalOrderId` (string | null) — Your identifier for the order. Stored and echoed back on the response.
- `metadata` (Record<string, string | number | boolean | null> | null) — Optional display and audit context stored with the order, shown in RxRelay, returned by order APIs, and included in order webhooks. Metadata is not used for auth, routing, billing, or fulfillment. Limits: 20 keys, 64 characters per key, 512 characters per value; nested objects and arrays are rejected.
- `patient` (object, **required**) — The patient this order is for. Used to match or create a patient in the clinic.
  - `patientId` (string · uuid | null) — Match an existing RxRelay patient directly. When set, other matching fields are ignored.
  - `externalPatientId` (string | null) — Your stable patient identifier. Primary key used for upsert matching within the clinic.
  - `firstName` (string | null) — Required when creating a new patient.
  - `lastName` (string | null) — Required when creating a new patient.
  - `dateOfBirth` (string · date | null) — ISO date, e.g. `1980-01-10`. Required when creating a new patient.
  - `sex` (string | null) — One of `M`, `F`, or `U`.
  - `phone` (string | null) — Patient phone number.
  - `email` (string | null) — Patient email. Used as a fallback match when `externalPatientId` is absent.
- `shippingAddress` (object | null) — Where the order ships. Provide exactly one of `shippingAddress` or `shippingAddressId`. A supplied address is stored on the patient record, matched against any identical existing address, made default, and sent to the fulfilling pharmacy.
  - `address1` (string, **required**) — Street address.
  - `address2` (string | null) — Apartment, suite, unit, etc.
  - `city` (string, **required**) — City.
  - `state` (string, **required**) — Two-letter US state code, e.g. `TX`.
  - `zip` (string, **required**) — ZIP / postal code.
- `shippingAddressId` (string · uuid | null) — Existing RxRelay patient address ID. Provide exactly one of `shippingAddressId` or `shippingAddress`. The address must belong to the resolved patient and be active.
- `prescriberUserId` (string · uuid | null) — Prescriber's RxRelay user ID. Provide this or `prescriberNpi`; the prescriber must belong to the clinic.
- `prescriberNpi` (string | null) — Prescriber's NPI. Used to resolve the prescriber when `prescriberUserId` is omitted.
- `shippingMethod` (string, default `Standard`) — Shipping method for the order.
- `items` (object[], **required**) — At least one Rx item is required.
  - `medicationId` (string · uuid, **required**) — A medication ID from `GET /v1/medications`.
  - `medicationName` (string | null) — Overrides the catalog name on this line.
  - `quantity` (string, **required**) — Dispense quantity, e.g. `30`.
  - `directions` (string, **required**) — Sig / directions for the patient.
  - `refills` (integer, **required**) — Number of refills.
  - `externalPrescriptionId` (string | null) — Your identifier for this prescription line. Echoed back on the item.

### Response

- `orderId` (string · uuid) — RxRelay order ID.
- `externalOrderId` (string | null) — The `externalOrderId` you supplied, if any.
- `patientId` (string · uuid) — The resolved or newly created patient ID.
- `status` (string) — Aggregate order status: one of `Queued`, `SandboxReceived`, `Pending`, `Sent`, or `Failed`.
- `createdAt` (string · date-time) — When the order was created.
- `metadata` (Record<string, string | number | boolean | null> | null) — The metadata supplied on the order, if any.
- `items` (object[]) — Per-item status.
  - `orderItemId` (string · uuid) — RxRelay order item ID.
  - `medicationId` (string · uuid) — Medication on this line.
  - `medicationName` (string) — Resolved medication name.
  - `externalPrescriptionId` (string | null) — The identifier you supplied for this line.
  - `status` (string) — Item status, or `Queued` before dispatch.
- `submissions` (object[]) — RxRelay submission status and item correlation. Downstream pharmacy routing details are not exposed.
  - `submissionId` (string · uuid) — RxRelay submission ID.
  - `environment` (string) — `Sandbox` or `Live`.
  - `status` (string) — Submission status.
  - `orderItemIds` (string · uuid[]) — Order items included in this submission.

### Errors

- `400` Bad Request — Missing Idempotency-Key, a missing or invalid shipping address, an unknown or unsupported medication, or no resolvable prescriber.
- `402` Payment Required — The clinic has a past-due balance. Settle billing before submitting live orders.
- `403` Forbidden — The API key is read-only. Use a key with the read_write scope to create orders.
- `404` Not Found — A referenced patient or resource does not belong to this clinic.
- `409` Conflict — The Idempotency-Key was reused with a different request body, or the original request is still processing.
- `429` Too Many Requests — The API key exceeded its current rate limit. Retry after the Retry-After header or contact RxRelay to raise clinic limits.

## Retrieve an order

`GET /v1/orders/{orderId}`

Fetches the current status of an order and its items by RxRelay order ID.

**Request**

```bash
curl https://api.rxrelay.ai/v1/orders/a4d9f0e2-7b1c-4e3a-8f5d-2c6b9a0e1f3d \
  -H "Authorization: Bearer rxrelay_sandbox_your_key"
```

**Response** `200`

```json
{
  "orderId": "a4d9f0e2-7b1c-4e3a-8f5d-2c6b9a0e1f3d",
  "externalOrderId": "partner-order-123",
  "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
  "status": "SandboxReceived",
  "createdAt": "2026-06-05T12:30:00.0000000+00:00",
  "metadata": {
    "key1": "value1",
    "key2": "value2"
  },
  "items": [
    {
      "orderItemId": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f",
      "medicationId": "7c9e6a1b-1f4d-4a2e-9c3b-9a1d2e3f4a5b",
      "medicationName": "Semaglutide 2.5mg/mL",
      "externalPrescriptionId": "rx-001",
      "status": "SandboxReceived"
    }
  ],
  "submissions": [
    {
      "submissionId": "d3e4f5a6-b7c8-4d9e-0f1a-2b3c4d5e6f70",
      "environment": "Sandbox",
      "status": "SandboxReceived",
      "orderItemIds": ["c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f"]
    }
  ]
}
```

### Path parameters

- `orderId` (string · uuid, **required**) — The RxRelay order ID returned when the order was created.

Returns the same shape as **Create an order**, or `404 Not Found` if the order does not belong to your clinic.

## List orders

`GET /v1/orders`

Lists or searches the clinic's orders, newest first, with pagination. Each entry is a lightweight summary;
fetch a single order via `GET /v1/orders/{orderId}` for full item and submission detail.

**Request**

```bash
curl "https://api.rxrelay.ai/v1/orders?status=Sent&limit=25&offset=0" \
  -H "Authorization: Bearer rxrelay_sandbox_your_key"
```

**Response** `200`

```json
{
  "orders": [
    {
      "orderId": "a4d9f0e2-7b1c-4e3a-8f5d-2c6b9a0e1f3d",
      "externalOrderId": "partner-order-123",
      "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
      "status": "Sent",
      "createdAt": "2026-06-05T12:30:00.0000000+00:00",
      "metadata": {
        "key1": "value1",
        "key2": "value2"
      }
    }
  ],
  "total": 1,
  "limit": 25,
  "offset": 0
}
```

### Query parameters

- `clinicId` (string · uuid | null) — **Organization keys only.** The RxRelay clinic to scope to. Provide this or `externalClinicId`.
- `externalClinicId` (string | null) — **Organization keys only.** Your own identifier for the target clinic.
- `status` (string | null) — Filter by derived status: one of `Queued`, `Pending`, `Sent`, `Failed`, `Cancelled`, or `SandboxReceived`.
- `patientId` (string · uuid | null) — Only orders for this patient.
- `externalOrderId` (string | null) — Exact match on your order identifier.
- `from` (string · date-time | null) — Only orders created at or after this timestamp.
- `to` (string · date-time | null) — Only orders created at or before this timestamp.
- `limit` (integer, default `25`) — Page size, between 1 and 100.
- `offset` (integer, default `0`) — Number of records to skip.

### Response

- `orders` (object[]) — The page of order summaries, newest first.
  - `orderId` (string · uuid) — RxRelay order ID.
  - `externalOrderId` (string | null) — The identifier you supplied, if any.
  - `patientId` (string · uuid) — The patient on the order.
  - `status` (string) — Derived order status.
  - `createdAt` (string · date-time) — When the order was created.
  - `metadata` (Record<string, string | number | boolean | null> | null) — The metadata supplied on the order, if any.
- `total` (integer) — Total matching orders across all pages.
- `limit` (integer) — The effective page size applied.
- `offset` (integer) — The offset applied.

## Patient orders

`GET /v1/patients/{patientId}/orders`

Lists a single patient's orders, newest first, with pagination. Same summary shape and pagination as
**List orders**. Returns `404 Not Found` if the patient does not belong to your clinic.

**Request**

```bash
curl "https://api.rxrelay.ai/v1/patients/b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e/orders?limit=25&offset=0" \
  -H "Authorization: Bearer rxrelay_sandbox_your_key"
```

**Response** `200`

```json
{
  "orders": [
    {
      "orderId": "a4d9f0e2-7b1c-4e3a-8f5d-2c6b9a0e1f3d",
      "externalOrderId": "partner-order-123",
      "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
      "status": "Sent",
      "createdAt": "2026-06-05T12:30:00.0000000+00:00",
      "metadata": {
        "key1": "value1",
        "key2": "value2"
      }
    }
  ],
  "total": 1,
  "limit": 25,
  "offset": 0
}
```

### Path parameters

- `patientId` (string · uuid, **required**) — The RxRelay patient ID.

## Patient addresses

`GET /v1/patients/{patientId}/addresses`

Returns addresses for an existing RxRelay patient. Store the returned `id` and pass it later as
`shippingAddressId` when creating an order.

**Request**

```bash
curl https://api.rxrelay.ai/v1/patients/b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e/addresses \
  -H "Authorization: Bearer rxrelay_sandbox_your_key"
```

**Response** `200`

```json
[
  {
    "id": "9b8a7c6d-5e4f-4321-9a8b-7c6d5e4f3210",
    "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
    "address1": "123 Main St",
    "address2": "Apt 4B",
    "city": "Dallas",
    "state": "TX",
    "zip": "75201",
    "isDefault": true,
    "isActive": true,
    "createdAt": "2026-06-05T12:15:00.0000000+00:00"
  }
]
```

### Upsert an address

`POST /v1/patients/{patientId}/addresses`

Creates an address or returns the existing matching address when the normalized address already exists
for the patient. Set `isDefault` to `true` to make the address the patient's default.

**Request**

```bash
curl -X POST https://api.rxrelay.ai/v1/patients/b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e/addresses \
  -H "Authorization: Bearer rxrelay_sandbox_your_key" \
  -H "Content-Type: application/json" \
  -d '{
  "address1": "123 Main St",
  "address2": "Apt 4B",
  "city": "Dallas",
  "state": "TX",
  "zip": "75201",
  "isDefault": true
}'
```

**Response** `200`

```json
  {
    "id": "9b8a7c6d-5e4f-4321-9a8b-7c6d5e4f3210",
    "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
    "address1": "123 Main St",
    "address2": "Apt 4B",
    "city": "Dallas",
    "state": "TX",
    "zip": "75201",
    "isDefault": true,
    "isActive": true,
    "createdAt": "2026-06-05T12:15:00.0000000+00:00"
  }
```

### Get one address

`GET /v1/patients/{patientId}/addresses/{addressId}`

Fetches a single address by ID.

**Request**

```bash
curl https://api.rxrelay.ai/v1/patients/b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e/addresses/9b8a7c6d-5e4f-4321-9a8b-7c6d5e4f3210 \
  -H "Authorization: Bearer rxrelay_sandbox_your_key"
```

**Response** `200`

```json
  {
    "id": "9b8a7c6d-5e4f-4321-9a8b-7c6d5e4f3210",
    "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
    "address1": "123 Main St",
    "address2": "Apt 4B",
    "city": "Dallas",
    "state": "TX",
    "zip": "75201",
    "isDefault": true,
    "isActive": true,
    "createdAt": "2026-06-05T12:15:00.0000000+00:00"
  }
```

### Remove an address

`DELETE /v1/patients/{patientId}/addresses/{addressId}`

Soft-deletes (deactivates) an address. If it was the default, the most recently updated remaining active
address is promoted to default so the patient still has one. Returns the deactivated address. Requires a
`read_write` key.

**Request**

```bash
curl -X DELETE https://api.rxrelay.ai/v1/patients/b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e/addresses/9b8a7c6d-5e4f-4321-9a8b-7c6d5e4f3210 \
  -H "Authorization: Bearer rxrelay_sandbox_your_key"
```

### Set the default address

`POST /v1/patients/{patientId}/addresses/{addressId}/default`

Promotes an existing active address to the patient's default. An inactive address returns `400 Bad Request` —
re-add it first. Requires a `read_write` key.

**Request**

```bash
curl -X POST https://api.rxrelay.ai/v1/patients/b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e/addresses/9b8a7c6d-5e4f-4321-9a8b-7c6d5e4f3210/default \
  -H "Authorization: Bearer rxrelay_sandbox_your_key"
```

**Response** `200`

```json
  {
    "id": "9b8a7c6d-5e4f-4321-9a8b-7c6d5e4f3210",
    "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
    "address1": "123 Main St",
    "address2": "Apt 4B",
    "city": "Dallas",
    "state": "TX",
    "zip": "75201",
    "isDefault": true,
    "isActive": true,
    "createdAt": "2026-06-05T12:15:00.0000000+00:00"
  }
```

### Address fields

- `id` (string · uuid) — RxRelay address ID. Pass as `shippingAddressId` when creating an order.
- `patientId` (string · uuid) — The patient this address belongs to.
- `address1` (string) — Street address.
- `address2` (string | null) — Apartment, suite, unit, etc.
- `city` (string) — City.
- `state` (string) — Two-letter US state code.
- `zip` (string) — ZIP / postal code.
- `isDefault` (boolean) — Whether this is the patient's default address.
- `isActive` (boolean) — Whether the address is active. Deactivated addresses are `false`.
- `createdAt` (string · date-time) — When the address was created.

Organization-scoped keys should include `clinicId` or `externalClinicId` as query parameters,
matching the medication and prescriber listing endpoints.

## Organizations

An **organization** is a parent that owns many clinics. A partner that manages multiple clinics is
issued a single **organization-scoped API key** instead of one key per clinic. With it you can:

- Submit orders for any clinic beneath the org by naming the clinic on each request.
- List the clinics under the org.
- Provision new clinics and their users — when RxRelay enables provisioning for your org.

### Ordering on behalf of a clinic

Use the same `POST /v1/orders` endpoint, but include `clinicId` or `externalClinicId` so RxRelay
knows which clinic the order and patient belong to. The named clinic must belong to your organization;
the prescriber and patient are resolved within that clinic exactly as for clinic-scoped keys.

```bash
curl -X POST https://api.rxrelay.ai/v1/orders \
  -H "Authorization: Bearer rxrelay_sandbox_your_org_key" \
  -H "Idempotency-Key: partner-order-123" \
  -H "Content-Type: application/json" \
  -d '{
  "externalClinicId": "clinic-a",
  "externalOrderId": "partner-order-123",
  "metadata": {
    "key1": "value1",
    "key2": "value2"
  },
  "patient": {
    "externalPatientId": "patient-456",
    "firstName": "Jane",
    "lastName": "Doe",
    "dateOfBirth": "1980-01-10",
    "sex": "F"
  },
  "shippingAddress": {
    "address1": "123 Main St",
    "city": "Dallas",
    "state": "TX",
    "zip": "75201"
  },
  "prescriberNpi": "1234567890",
  "items": [
    {
      "medicationId": "7c9e6a1b-1f4d-4a2e-9c3b-9a1d2e3f4a5b",
      "quantity": "30",
      "directions": "Take one capsule daily",
      "refills": 2
    }
  ]
}'
```

If the clinic identifier is missing, RxRelay returns `400 Bad Request`; if it does not match a clinic
in your organization, `404 Not Found`. `GET /v1/medications`, `GET /v1/pharmacies`, and
`GET /v1/prescribers` accept a `clinicId` or `externalClinicId` query parameter to scope their
results when called with an org key.

### List clinics

`GET /v1/clinics`

Returns the clinics under your organization.

**Request**

```bash
curl https://api.rxrelay.ai/v1/clinics \
  -H "Authorization: Bearer rxrelay_sandbox_your_org_key"
```

**Response** `200`

```json
[
  {
    "id": "11111111-1111-1111-1111-111111111111",
    "name": "Northside Clinic",
    "externalClinicId": "clinic-a",
    "address1": "123 Main St",
    "address2": null,
    "city": "Dallas",
    "state": "TX",
    "zip": "75201",
    "phoneNumber": "5555555555",
    "contactEmail": "ops@northsideclinic.com",
    "isSandboxMode": true,
    "createdAt": "2026-06-19T12:30:00.0000000+00:00"
  }
]
```

### Create a clinic

`POST /v1/clinics`

Creates a clinic under your organization. Requires that RxRelay has enabled **clinic provisioning**
for your organization, otherwise `403 Forbidden`. Supplying `externalClinicId` makes the call
idempotent — re-posting the same `externalClinicId` returns the existing clinic instead of creating a
duplicate.

**Request**

```bash
curl -X POST https://api.rxrelay.ai/v1/clinics \
  -H "Authorization: Bearer rxrelay_sandbox_your_org_key" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Northside Clinic",
  "externalClinicId": "clinic-a",
  "address1": "123 Main St",
  "city": "Dallas",
  "state": "TX",
  "zip": "75201",
  "phoneNumber": "5555555555",
  "contactEmail": "ops@northsideclinic.com"
}'
```

**Response** `201`

```json
{
  "id": "11111111-1111-1111-1111-111111111111",
  "name": "Northside Clinic",
  "externalClinicId": "clinic-a",
  "address1": "123 Main St",
  "address2": null,
  "city": "Dallas",
  "state": "TX",
  "zip": "75201",
  "phoneNumber": "5555555555",
  "contactEmail": "ops@northsideclinic.com",
  "isSandboxMode": true,
  "createdAt": "2026-06-19T12:30:00.0000000+00:00"
}
```

#### Body

- `name` (string, **required**) — Clinic name.
- `externalClinicId` (string | null) — Your own identifier for the clinic, unique within the org. Use it as the idempotency key and to reference the clinic on later calls.
- `address1`, `address2`, `city`, `state`, `zip`, `phoneNumber`, `contactEmail` (string | null) — Optional clinic profile fields.

### Create a clinic user

`POST /v1/clinics/{clinicRef}/users`

Creates (or updates) a user in a clinic under your organization and assigns clinic roles.
`{clinicRef}` is the RxRelay clinic id (a UUID) of a clinic in your organization. Requires provisioning to be
enabled. A user with the `prescriber` role must include an `npi` so orders can resolve them by
`prescriberNpi`. The call is idempotent per email within the clinic. A single clinic managing its own roster
with a clinic-scoped key should use `POST /v1/users` instead.

**Request**

```bash
curl -X POST https://api.rxrelay.ai/v1/clinics/11111111-1111-1111-1111-111111111111/users \
  -H "Authorization: Bearer rxrelay_sandbox_your_org_key" \
  -H "Content-Type: application/json" \
  -d '{
  "email": "alex.reed@northsideclinic.com",
  "firstName": "Alex",
  "lastName": "Reed",
  "roles": ["prescriber"],
  "npi": "1234567890",
  "licenseState": "TX",
  "licenseNumber": "MD-44821"
}'
```

**Response** `200`

```json
{
  "userId": "3f1a8c7d-2b4e-4f6a-9c8d-7e6f5a4b3c2d",
  "email": "alex.reed@northsideclinic.com",
  "firstName": "Alex",
  "lastName": "Reed",
  "roles": ["prescriber"],
  "npi": "1234567890",
  "joinedAt": "2026-06-19T12:31:00.0000000+00:00"
}
```

#### Body

- `email` (string, **required**) — The user's email. Used to match an existing RxRelay user or create one.
- `firstName`, `lastName` (string | null) — User's name.
- `roles` (string[], **required**) — Clinic roles: `prescriber`, `clinic_admin`, `staff`, or `owner`. `platform_admin` cannot be assigned via the API.
- `npi` (string | null) — Required when `roles` includes `prescriber`.
- `licenseState`, `licenseNumber`, `phone`, `deaNumber` (string | null) — Optional professional fields.

### List clinic users

`GET /v1/clinics/{clinicRef}/users`

Returns every user in the named clinic with their roles and NPI. `{clinicRef}` is the RxRelay clinic id
(a UUID) of a clinic in your organization. The response shape matches `GET /v1/users`.

**Response** `200`

```json
[
  {
    "userId": "3f1a8c7d-2b4e-4f6a-9c8d-7e6f5a4b3c2d",
    "email": "alex.reed@northsideclinic.com",
    "firstName": "Alex",
    "lastName": "Reed",
    "roles": ["prescriber"],
    "npi": "1234567890",
    "joinedAt": "2026-06-19T12:31:00.0000000+00:00"
  }
]
```

### Errors

- `400` Bad Request — A required field is missing, a prescriber is missing an NPI, or `{clinicRef}` is not a valid clinic id.
- `403` Forbidden — The key lacks `read_write` scope, or provisioning is not enabled for the organization.
- `404` Not Found — The named clinic does not belong to the organization.

## Webhooks

Configure webhook endpoints from **Settings → API** to receive order events. RxRelay signs every
delivery with a per-endpoint secret and records delivery attempts in the webhook event log for auditability.
Outbound webhooks are delivered asynchronously from order submission and pharmacy status processing,
so your endpoint response time does not delay RxRelay API responses. RxRelay attempts each delivery
once; failed deliveries can be replayed from the event log after your endpoint is healthy again.

Treat webhooks as asynchronous, at-least-once notifications and deduplicate by `RxRelay-Event-Id`
or `RxRelay-Delivery-Id`.

**Create endpoint**

```bash
curl -X POST https://api.rxrelay.ai/clinics/{clinicId}/webhooks/endpoints \
  -H "Authorization: Bearer your_app_session_token" \
  -H "X-Clinic-Id: {clinicId}" \
  -H "Content-Type: application/json" \
  -d '{
  "url": "https://example.com/rxrelay/webhooks",
  "environment": "both",
  "events": [
    "order.sandbox_received",
    "order.sent",
    "order.failed",
    "order.tracking_updated"
  ]
}'
```

**Response** `200`

```json
{
  "endpoint": {
    "id": "8f76c5b4-3a21-4c90-93a6-7a8b9c0d1e2f",
    "clinicId": "11111111-1111-1111-1111-111111111111",
    "url": "https://example.com/rxrelay/webhooks",
    "secretPrefix": "whsec_4f8b2c91",
    "environment": "both",
    "events": [
      "order.sandbox_received",
      "order.sent",
      "order.failed",
      "order.tracking_updated"
    ],
    "isActive": true,
    "createdAt": "2026-06-05T12:30:00.0000000+00:00",
    "updatedAt": "2026-06-05T12:30:00.0000000+00:00",
    "secretRotatedAt": "2026-06-05T12:30:00.0000000+00:00"
  },
  "secret": "whsec_4f8b2c91..."
}
```

### Endpoint fields

- `url` (string, **required**) — Absolute HTTP or HTTPS URL that receives webhook POST requests.
- `environment` (string, default `both`) — One of `sandbox`, `live`, or `both`.
- `events` (string[]) — Event types the endpoint should receive.

### Delivery headers

```http
RxRelay-Event-Id: evt_f2a4c6e8d0b14d09a7c1e3f5b9a2c4d6
RxRelay-Delivery-Id: 9b8a7c6d-5e4f-3210-9876-abcdef123456
RxRelay-Timestamp: 1780691400
RxRelay-Signature: t=1780691400,v1=<hmac_sha256>
```

### Order event payload

```json
{
  "id": "evt_f2a4c6e8d0b14d09a7c1e3f5b9a2c4d6",
  "type": "order.sandbox_received",
  "environment": "sandbox",
  "createdAt": "2026-06-05T12:30:00.0000000+00:00",
  "data": {
    "order": {
      "orderId": "a4d9f0e2-7b1c-4e3a-8f5d-2c6b9a0e1f3d",
      "externalOrderId": "partner-order-123",
      "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
      "status": "SandboxReceived",
      "createdAt": "2026-06-05T12:30:00.0000000+00:00",
      "metadata": {
        "key1": "value1",
        "key2": "value2"
      },
      "items": [
        {
          "orderItemId": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f",
          "medicationId": "7c9e6a1b-1f4d-4a2e-9c3b-9a1d2e3f4a5b",
          "medicationName": "Semaglutide 2.5mg/mL",
          "externalPrescriptionId": "rx-001",
          "status": "SandboxReceived"
        }
      ],
      "submissions": [
        {
          "submissionId": "d3e4f5a6-b7c8-4d9e-0f1a-2b3c4d5e6f70",
          "environment": "Sandbox",
          "status": "SandboxReceived",
          "orderItemIds": ["c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f"]
        }
      ]
    }
  }
}
```

### Tracking update payload

```json
{
  "id": "evt_8d2f8a7e6c5b4a3d9e0f1a2b3c4d5e6f",
  "type": "order.tracking_updated",
  "environment": "live",
  "createdAt": "2026-06-05T13:45:00.0000000+00:00",
  "data": {
    "order": {
      "orderId": "a4d9f0e2-7b1c-4e3a-8f5d-2c6b9a0e1f3d",
      "externalOrderId": "partner-order-123",
      "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
      "status": "Shipped",
      "createdAt": "2026-06-05T12:30:00.0000000+00:00",
      "metadata": {
        "key1": "value1",
        "key2": "value2"
      },
      "items": [
        {
          "orderItemId": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f",
          "medicationId": "7c9e6a1b-1f4d-4a2e-9c3b-9a1d2e3f4a5b",
          "medicationName": "Semaglutide 2.5mg/mL",
          "externalPrescriptionId": "rx-001",
          "status": "Shipped"
        }
      ],
      "shipments": [
        {
          "shipmentId": "shp_d3e4f5a6b7c84d9e0f1a2b3c4d5e6f70",
          "carrier": "UPS",
          "trackingNumber": "1Z999AA10123456784",
          "trackingUrl": "https://www.ups.com/track?tracknum=1Z999AA10123456784",
          "orderItemIds": ["c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f"]
        }
      ]
    }
  }
}
```

### Signature verification

Verify the `RxRelay-Signature` header before trusting a webhook. Compute HMAC-SHA256 using
your endpoint secret over `{timestamp}.{raw_body}` and compare it to the `v1` value.

```js
const signedPayload = timestamp + "." + rawRequestBody;
const expected = hmacSha256(endpointSecret, signedPayload);
// compare expected to the v1 value from RxRelay-Signature
```

### Events

- `order.sandbox_received` — A sandbox API order was validated and recorded without vendor dispatch.
- `order.sent` — A live order was submitted and did not end in a failed aggregate state. Delivered asynchronously after the order API response.
- `order.failed` — A live order completed with a failed aggregate state. Delivered asynchronously after the order API response.
- `order.tracking_updated` — Tracking information was received from the fulfillment network and forwarded asynchronously without exposing downstream pharmacy routing.
- `webhook.test` — A test event sent from Settings / API. Test sends are attempted immediately so you can verify endpoint handling.
