RxRelay Developers

RxRelay API

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.

Agent-assisted integration

Build with an AI coding agent

Works with Claude Code, Codex, OpenCode, Cursor, and other compatible agents. Copy a prompt into your agent of choice; it installs the skill and starts the workflow.

Start a new integration

Choose Full, Standard, or Minimal clinic-scoped integration.

Copy prompt
Install or refresh the latest official RxRelay skill (replaces older copies):
npx skills add https://www.rxrelay.ai/skills/integrate-rxrelay-api/SKILL.md

Then use $integrate-rxrelay-api to implement and verify RxRelay
in this repository. First identify my API key scope (clinic or organization),
then ask me to choose Full, Standard, or Minimal integration, with a concise description of each.
Follow the skill's safety rules and separate optional production hardening.
Finish with the RxRelay readiness reports.
View skill source

Review an existing integration

Find concrete blockers to valid, safe order submission.

Copy prompt
Install or refresh the latest official RxRelay skill (replaces older copies):
npx skills add https://www.rxrelay.ai/skills/review-rxrelay-integration/SKILL.md

Then use $review-rxrelay-integration to audit this repository.
Begin read-only and report what is missing or incompatible.
Focus on concrete blockers to valid, safe order submission.
Separate non-blocking hardening recommendations.
Provide the minimum blocker-fix plan and request permission before editing.
Finish with the RxRelay readiness reports.
View skill source

Update to the latest API

Compare your code with recent releases and plan only the required changes.

Copy prompt
Install or refresh the latest official RxRelay skill (replaces older copies):
npx skills add https://www.rxrelay.ai/skills/rxrelay-api-update/SKILL.md

Then use $rxrelay-api-update to compare this repository
with the latest RxRelay API.
Report required compatibility and safety changes separately from optional hardening.
Provide the minimum required-change plan and request permission before editing.
After approval, implement, test, and refresh the RxRelay readiness report.
View skill source

Rerun any prompt to refresh its skill; each workflow also checks the canonical published copy before acting. Node 22.20+ required. Secrets stay local; sandbox writes require approval, and live orders are never submitted. Readiness schema

Base URL

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 header
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

Using an org key? Start with the organization quickstart and clinic-selection table.

Clinic-scoped keys act on a single clinic and are the default. Organization-scoped keys act on behalf of an organization that owns many clinics — they must name the target clinic on each order via clinicId or externalClinicId, and can invite organization members or admins through the Organizations endpoints when RxRelay enables it.

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.

429 Too Many Requests
A rate-limited response includes Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers.

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.

Problem Details
{
  "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..."
}

Shipping state validation

Send a recognized two-letter code for one of the 50 U.S. states or DC in shippingAddress.state, patient defaultAddress.state, and patient address writes. Full names, territories, and unknown codes such as ZZ are not accepted in /v1 requests. An unrecognized nonblank state returns 400 with code invalid_shipping_state and shippingState containing the supplied value. Missing required address fields return a separate 400 validation error.

When POST /v1/orders uses a saved address, an unrecognized nonblank state returns 422 with code invalid_shipping_state and shippingState. Correct the selected patient address or supply a valid shippingAddress before submitting again. Recognized full names on older saved addresses (for example Pennsylvania) are normalized to codes in the new order's shipping snapshot; this does not update the saved patient address or allow full names in new API address writes.

A recognized state that the selected pharmacy cannot serve returns 422 with code pharmacy_shipping_state_blocked, plus shippingState, pharmacies, and medications. Select an eligible pharmacy or a valid alternative delivery address; do not retry either shipping-state error unchanged. Shipping restrictions are checked again before queued dispatch, approval dispatch, and retries; a blocked existing order fails without a vendor request. No configured restrictions means no state is blocked by that configuration, not a guarantee of pharmacy acceptance.

Invalid state supplied in an address
400
{
  "title": "Invalid shipping address",
  "detail": "shippingAddress.state must be a valid two-letter U.S. state code (e.g. \"TX\").",
  "status": 400,
  "code": "invalid_shipping_state",
  "shippingState": "ZZ"
}
Invalid state on a saved address during order routing
422
{
  "title": "Invalid shipping state",
  "detail": "Shipping state \"ZZ\" is not a recognized U.S. state. Update the patient's shipping address to a two-letter state code (e.g. \"TX\").",
  "status": 422,
  "code": "invalid_shipping_state",
  "shippingState": "ZZ"
}

Idempotency

POST /v1/orders and POST /v1/orders/{orderId}/reorder require 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, operation, or re-order source 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. 1. Create a sandbox read_write key from Settings → API.
  2. 2. Fetch GET /v1/medications and submit representative sandbox orders with required idempotency keys.
  3. 3. Add a webhook endpoint in Settings → API, copy the one-time endpoint secret, and send a test event to verify signature handling.
  4. 4. RxRelay reviews the recorded sandbox orders and webhook deliveries for the clinic.
  5. 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

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.

GET /v1/clinic
Request
cURL
curl https://api.rxrelay.ai/v1/clinic \
  -H "Authorization: Bearer rxrelay_sandbox_your_key"
Response
200
{
  "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

id string · uuid
Your RxRelay clinic id. Not required on clinic-scoped calls (the key implies it), but useful for reference.
name string
Clinic name.
externalClinicId string | null
Your own identifier for the clinic, if one was set when it was created under an organization. null for standalone clinics.
address1 string | null
Street address.
address2 string | null
Apartment, suite, unit, etc.
city string | null
City.
state string | null
Two-letter US state code.
zip string | null
ZIP / postal code.
phoneNumber string | null
Clinic phone number.
contactEmail string | null
Clinic contact email.
isSandboxMode boolean
Whether the clinic is in sandbox mode.
createdAt string · date-time
When the clinic was created.

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

Medication availability can change independently at each pharmacy. Products disabled platform-wide are excluded from GET /v1/medications for every clinic, including sandbox catalogs. New orders recheck current availability: when no permitted route remains, POST /v1/orders returns 400 with title Order routing failed. Refresh the catalog and ask the user to select an available product instead of retrying unchanged. Previously created orders continue through their existing approval, dispatch, and retry flows; completed idempotent replays remain available.

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 List pharmacies).

GET /v1/medications
Request
cURL
curl https://api.rxrelay.ai/v1/medications \
  -H "Authorization: Bearer rxrelay_sandbox_your_key"
Using an organization API key

Organization key: clinicId or externalClinicId is required in the query string, including patient creation, updates, and address writes. Use the clinic UUID from GET /v1/clinics, or its configured externalClinicId. Patient IDs alone do not select a clinic. Organization quickstart and errors

Organization key request
cURL
curl "https://api.rxrelay.ai/v1/medications?clinicId=11111111-1111-1111-1111-111111111111" \
  -H "Authorization: Bearer rxrelay_sandbox_your_org_key"
Filter by pharmacy
cURL
curl "https://api.rxrelay.ai/v1/medications?pharmacyId=Kaduceus:opbrx" \
  -H "Authorization: Bearer rxrelay_sandbox_your_key"
Using an organization API key

Organization key: clinicId or externalClinicId is required in the query string, including patient creation, updates, and address writes. Use the clinic UUID from GET /v1/clinics, or its configured externalClinicId. Patient IDs alone do not select a clinic. Organization quickstart and errors

Organization key request
cURL
curl "https://api.rxrelay.ai/v1/medications?pharmacyId=Kaduceus:opbrx&clinicId=11111111-1111-1111-1111-111111111111" \
  -H "Authorization: Bearer rxrelay_sandbox_your_org_key"
Response
200
[
  {
    "id": "7c9e6a1b-1f4d-4a2e-9c3b-9a1d2e3f4a5b",
    "name": "Semaglutide 2.5mg/mL",
    "strength": "2.5 mg/mL",
    "form": "Injectable",
    "vialSize": null,
    "pharmacyName": "Boothwyn Pharmacy",
    "pharmacySku": "12567",
    "requiredQuantity": null,
    "requiresClinicalJustification": true,
    "ingredients": [],
    "isControlled": false
  },
  {
    "id": "6f667d6d-5f1d-bf89-546f-29405a4393c7",
    "name": "Metoprolol Succinate 25mg ER",
    "strength": null,
    "form": null,
    "vialSize": null,
    "pharmacyName": "Boothwyn Pharmacy",
    "pharmacySku": "13776",
    "requiredQuantity": 6,
    "requiresClinicalJustification": false,
    "ingredients": [],
    "isControlled": false
  },
  {
    "id": "0b8d2f31-6a7c-4d5e-8f90-1a2b3c4d5e6f",
    "name": "RENEW",
    "strength": null,
    "form": "Injectable",
    "vialSize": "5mL",
    "pharmacyName": "Kaduceus Pharmacy",
    "pharmacySku": "11111222219",
    "requiredQuantity": null,
    "requiresClinicalJustification": false,
    "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" }
    ],
    "isControlled": false
  },
  {
    "id": "b33729cb-a9e2-463d-bc89-6c5723ec8d70",
    "name": "Example compounded tablet",
    "strength": "Example strength",
    "form": "Tablet",
    "vialSize": "30 each",
    "pharmacyName": "Emerald",
    "pharmacySku": "12345",
    "requiredQuantity": null,
    "requiresClinicalJustification": false,
    "ingredients": [],
    "isControlled": false,
    "isSupply": false,
    "obsidian": {
      "units": "each",
      "packSize": 30,
      "defaultDirections": null,
      "defaultDaysSupply": null,
      "defaultClinicalDifferenceStatement": null,
      "requiresClinicalDifferenceStatement": false
    }
  }
]

Pharmacy-required quantities

Treat the medication catalog as the source of truth for quantity rules. When a selected medication's requiredQuantity is non-null, convert that integer to a string and send it as the matching items[].quantity in POST /v1/orders. When it is null, collect the quantity from the prescriber using any additional catalog rules below. Do not infer the rule from the medication name or SKU, and do not maintain a partner-side SKU allowlist; pharmacy rules can vary and change independently.

Emerald products include obsidian metadata. For counted products (units: "each"), send quantity: "1" and the exact positive unit count in obsidian.dispenseQuantity. For example, dispenseQuantity: 30 means 30 tablets even for a 100-count SKU. Catalog pack size is not an ordering increment. Days supply applies to the full prescribed quantity. Measured products retain pack counts: three 2 mL vials use quantity: "3" and no dispenseQuantity. Legacy requests without the new field retain pack-count semantics. RxRelay limits expanded orders to 100 prescriptions, not 100 counted units. Collect clinician-approved directions, days supply, and any required clinical difference statement. Refills must be zero.

Apply the catalog rule
TypeScript
const medication = medications.find(
  ({ id }) => id === selectedMedicationId,
);

if (!medication) throw new Error("Medication is not available");

// Counted Emerald products use an explicit number of units on one Rx.
const countedEmerald = medication.obsidian?.units === "each";
const quantity = medication.obsidian
  ? countedEmerald ? "1" : String(userSelectedPackCount)
  : medication.requiredQuantity == null
    ? userSelectedQuantity
    : String(medication.requiredQuantity);

// Collect this from the prescriber for this patient; never synthesize rationale.
if (medication.requiresClinicalJustification && !patientSpecificRationale?.trim()) {
  throw new Error("Patient-specific clinical justification is required");
}

const orderItem = {
  medicationId: medication.id,
  quantity,
  directions, // clinician-approved; catalog defaults may be null
  refills: medication.obsidian ? 0 : refills,
  ...(medication.requiresClinicalJustification
    ? { clinicalJustification: patientSpecificRationale.trim() }
    : {}),
  ...(medication.obsidian ? { obsidian: {
    ...(countedEmerald ? { dispenseQuantity: Number(userSelectedQuantity) } : {}),
    daysSupply: clinicianApprovedDaysPerPrescription,
    clinicalDifferenceStatement: clinicianApprovedDifferenceStatement,
  } } : {}),
};

Query parameters

pharmacyId string | null query
Restrict the catalog to a single dispensing pharmacy, using an id from List 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. For clinic-scoped keys, omit this field or match the key’s clinic; a different clinic is rejected.
externalClinicId string | null query
Organization keys only. Your own identifier for the target clinic.

Response

[] object[]
An array of medications available to this clinic.
Show child attributes
id string · uuid
Medication ID. Use this as items[].medicationId when creating an order.
name string
Display name of the medication. May be a human-curated pharmacy-product label; this never changes the downstream product identifier used for ordering.
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.
vialSize string | null
Vendor-supplied package volume (e.g. 2 mL), when available. Used for injectable vials and other measured packages such as creams; null when the pharmacy does not provide it.
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) for this medication, when available.
requiresClinicalJustification boolean
When true, collect patient-specific rationale from the prescriber and send it in items[].clinicalJustification. Applies to Boothwyn GLP-1s, testosterone, and other flagged products. Pharmacy notes do not satisfy this requirement.
requiredQuantity integer | null
The exact dispense quantity required for this pharmacy product. When non-null, stringify this value and send it as items[].quantity; any other value returns 400 Bad Request. When null, the prescriber may choose the quantity.
obsidian object | null
Emerald package and clinical metadata; absent or null for other platforms.
Show child attributes
units string
Dispensing unit such as each, ml, or g.
packSize number | null
Amount in one pack. Order quantity counts packs.
defaultDirections string | null
Optional SIG default for clinician review.
defaultDaysSupply integer | null
Optional days supply per pack, subject to clinician review.
defaultClinicalDifferenceStatement string | null
Optional statement for clinician review and editing.
requiresClinicalDifferenceStatement boolean
Whether this product requires a nonblank clinical difference statement.
isControlled boolean
Whether the pharmacy-specific product is controlled. Controlled products are discoverable, but order creation is blocked until the prescriber completes identity proofing and two-factor authorization.
ingredients object[]
Structured composition, when the pharmacy provides it (empty otherwise). For a multi-active blend, use this to show what's actually in the vial — strength alone can't describe it. Not required to order.
Show child attributes
name string
Ingredient (active) name, e.g. BPC-157.
concentration number | null
Amount per mL (or per vial for mg units).
unit string | null
Concentration unit, e.g. mg/mL.

List pharmacies

Returns the distinct pharmacies the authenticated clinic can dispense from. Each carries a stable, opaque id you pass as the pharmacyId filter on List medications to retrieve that pharmacy's formulary. Its shippingMethods advertise the preferred normalized values to send when creating an order. Use this when you route across multiple pharmacies and need per-pharmacy catalogs or shipping choices.

GET /v1/pharmacies
Request
cURL
curl https://api.rxrelay.ai/v1/pharmacies \
  -H "Authorization: Bearer rxrelay_sandbox_your_key"
Using an organization API key

Organization key: clinicId or externalClinicId is required in the query string, including patient creation, updates, and address writes. Use the clinic UUID from GET /v1/clinics, or its configured externalClinicId. Patient IDs alone do not select a clinic. Organization quickstart and errors

Organization key request
cURL
curl "https://api.rxrelay.ai/v1/pharmacies?clinicId=11111111-1111-1111-1111-111111111111" \
  -H "Authorization: Bearer rxrelay_sandbox_your_org_key"
Response
200
[
  {
    "id": "Boothwyn",
    "name": "Boothwyn Pharmacy",
    "platform": "Boothwyn",
    "shippingMethods": []
  },
  {
    "id": "LifeFile:obprx",
    "name": "Optimal Balance",
    "platform": "LifeFile",
    "shippingMethods": ["Overnight"]
  },
  {
    "id": "Kaduceus:opbrx",
    "name": "Kaduceus Pharmacy",
    "platform": "Kaduceus",
    "shippingMethods": ["2Day", "Overnight"]
  }
]

Response

[] object[]
An array of pharmacies this clinic can dispense from.
Show child attributes
id string
Opaque, stable pharmacy identifier. Treat it as a string and pass it back verbatim as pharmacyId on List medications.
name string | null
Friendly display name of the pharmacy, when one has been assigned.
platform string
The fulfillment network the pharmacy belongs to.
shippingMethods string[]
Preferred normalized request values supported for this pharmacy. 2Day means two-day delivery; legacy Standard remains accepted as an alias. A single value means only that service is advertised—for example, Optimal Balance advertises only Overnight. An empty array means no partner-selectable service is advertised. For Emerald, shipping must first be configured; an empty array does not authorize a default service.

List prescribers

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

GET /v1/prescribers
Request
cURL
curl https://api.rxrelay.ai/v1/prescribers \
  -H "Authorization: Bearer rxrelay_sandbox_your_key"
Using an organization API key

Organization key: clinicId or externalClinicId is required in the query string, including patient creation, updates, and address writes. Use the clinic UUID from GET /v1/clinics, or its configured externalClinicId. Patient IDs alone do not select a clinic. Organization quickstart and errors

Organization key request
cURL
curl "https://api.rxrelay.ai/v1/prescribers?clinicId=11111111-1111-1111-1111-111111111111" \
  -H "Authorization: Bearer rxrelay_sandbox_your_org_key"
Response
200
[
  {
    "userId": "3f1a8c7d-2b4e-4f6a-9c8d-7e6f5a4b3c2d",
    "firstName": "Alex",
    "lastName": "Reed",
    "email": "alex.reed@northsideclinic.com",
    "npi": "1234567890",
    "licenseState": "TX",
    "licenseNumber": "MD-44821"
  }
]

Response

[] object[]
An array of prescribers eligible to be referenced on an order.
Show child attributes
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

Clinic keys only. Organization keys receive 403 even with read/write access. Use organization invitations for org membership; those roles do not grant prescribing authority.

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

POST /v1/users
Request
cURL
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
{
  "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 body required
The user's email. Matches an existing RxRelay user or creates one.
firstName string | null body
User's first name.
lastName string | null body
User's last name.
roles string[] body required
Clinic roles: prescriber, clinic_admin, staff, or owner. platform_admin cannot be assigned via the API.
npi string | null body
Required when roles includes prescriber.
licenseState string | null body
State that issued the prescriber's license.
licenseNumber string | null body
Prescriber's license number.
phone string | null body
User phone number.
deaNumber string | null body
DEA number.

Response

userId string · uuid
The user's RxRelay ID.
email string
The user's email.
firstName string | null
User's first name.
lastName string | null
User's last 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

Organization keys must use GET /v1/clinics/CLINIC_UUID/users instead; GET /v1/users returns 400 for an org key.

Organization key request
cURL
curl https://api.rxrelay.ai/v1/clinics/11111111-1111-1111-1111-111111111111/users \
  -H "Authorization: Bearer rxrelay_sandbox_your_org_key"

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.

GET /v1/users
Request
cURL
curl https://api.rxrelay.ai/v1/users \
  -H "Authorization: Bearer rxrelay_sandbox_your_key"
Response
200
[
  {
    "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

[] object[]
An array of the clinic's users, ordered by name.
Show child attributes
userId string · uuid
The user's RxRelay ID.
email string
The user's email.
firstName string | null
User's first name.
lastName string | null
User's last 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

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. Results are paginated with limit and offset.

GET /v1/patients
Request
cURL
curl "https://api.rxrelay.ai/v1/patients?search=jane&limit=25&offset=0" \
  -H "Authorization: Bearer rxrelay_sandbox_your_key"
Using an organization API key

Organization key: clinicId or externalClinicId is required in the query string, including patient creation, updates, and address writes. Use the clinic UUID from GET /v1/clinics, or its configured externalClinicId. Patient IDs alone do not select a clinic. Organization quickstart and errors

Organization key request
cURL
curl "https://api.rxrelay.ai/v1/patients?search=jane&limit=25&offset=0&clinicId=11111111-1111-1111-1111-111111111111" \
  -H "Authorization: Bearer rxrelay_sandbox_your_org_key"
Response
200
{
  "patients": [
    {
      "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
      "externalPatientId": "patient-456",
      "firstName": "Jane",
      "lastName": "Doe",
      "dateOfBirth": "1980-01-10",
      "sex": "F",
      "phone": "3147501409",
      "email": "jane@example.com",
      "allergyProfile": { "status": "known", "allergies": ["penicillin", "sulfa"] },
      "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 query
Organization keys only. The RxRelay clinic to scope to. Provide this or externalClinicId. For clinic-scoped keys, omit this field or match the key’s clinic; a different clinic is rejected.
externalClinicId string | null query
Organization keys only. Your own identifier for the target clinic.
search string | null query
Case-insensitive substring over name / email / phone, plus an exact date-of-birth match (e.g. 1980-01-10).
email string | null query
Exact (case-insensitive) email match.
externalPatientId string | null query
Exact match on your patient identifier.
limit integer query default: 25
Page size, between 1 and 100.
offset integer query default: 0
Number of records to skip.

Response

patients object[]
The page of matching patients.
Show child attributes
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. New phone writes are normalized to 10 digits, e.g. 3147501409; historical untouched values may still use older formatting.
email string | null
Patient email.
allergyProfile object
Always returned: status is unknown, no_known_allergies, or known; allergies is a string array (empty for unknown/no known allergies).
createdAt string · date-time
When the patient was created.
addresses object[]
The patient's active addresses, default first. Same shape as 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

Fetches a single patient by RxRelay ID. Returns 404 Not Found if the patient does not belong to your clinic.

GET /v1/patients/{patientId}
Request
cURL
curl https://api.rxrelay.ai/v1/patients/b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e \
  -H "Authorization: Bearer rxrelay_sandbox_your_key"
Using an organization API key

Organization key: clinicId or externalClinicId is required in the query string, including patient creation, updates, and address writes. Use the clinic UUID from GET /v1/clinics, or its configured externalClinicId. Patient IDs alone do not select a clinic. Organization quickstart and errors

Organization key request
cURL
curl "https://api.rxrelay.ai/v1/patients/b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e?clinicId=11111111-1111-1111-1111-111111111111" \
  -H "Authorization: Bearer rxrelay_sandbox_your_org_key"
Response
200
{
  "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
  "externalPatientId": "patient-456",
  "firstName": "Jane",
  "lastName": "Doe",
  "dateOfBirth": "1980-01-10",
  "sex": "F",
  "phone": "3147501409",
  "email": "jane@example.com",
  "allergyProfile": { "status": "known", "allergies": ["penicillin", "sulfa"] },
  "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 path required
The RxRelay patient ID.

Returns the patient and its active addresses (default first). Organization-scoped keys must pass clinicId or externalClinicId as a query parameter.

Create a patient

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, dateOfBirth, email, and phone are required to create — email and phone are required to prescribe. Requires a read_write key.

POST /v1/patients
Request
cURL
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": "+1 (314) 750-1409",
  "email": "jane@example.com",
  "allergyProfile": { "status": "known", "allergies": ["penicillin", "sulfa"] },
  "defaultAddress": {
    "address1": "123 Main St",
    "address2": "Apt 4B",
    "city": "Dallas",
    "state": "TX",
    "zip": "75201",
    "isDefault": true
  }
}'
Using an organization API key

Organization key: clinicId or externalClinicId is required in the query string, including patient creation, updates, and address writes. Use the clinic UUID from GET /v1/clinics, or its configured externalClinicId. Patient IDs alone do not select a clinic. Organization quickstart and errors

Organization key request
cURL
curl -X POST "https://api.rxrelay.ai/v1/patients?clinicId=11111111-1111-1111-1111-111111111111" \
  -H "Authorization: Bearer rxrelay_sandbox_your_org_key" \
  -H "Content-Type: application/json" \
  -d '{
  "externalPatientId": "patient-456",
  "firstName": "Jane",
  "lastName": "Doe",
  "dateOfBirth": "1980-01-10",
  "sex": "F",
  "phone": "+1 (314) 750-1409",
  "email": "jane@example.com",
  "allergyProfile": { "status": "known", "allergies": ["penicillin", "sulfa"] },
  "defaultAddress": {
    "address1": "123 Main St",
    "address2": "Apt 4B",
    "city": "Dallas",
    "state": "TX",
    "zip": "75201",
    "isDefault": true
  }
}'
Response
200
{
  "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
  "externalPatientId": "patient-456",
  "firstName": "Jane",
  "lastName": "Doe",
  "dateOfBirth": "1980-01-10",
  "sex": "F",
  "phone": "3147501409",
  "email": "jane@example.com",
  "allergyProfile": { "status": "known", "allergies": ["penicillin", "sulfa"] },
  "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"
    }
  ]
}
Request body
JSON
{
  "externalPatientId": "patient-456",
  "firstName": "Jane",
  "lastName": "Doe",
  "dateOfBirth": "1980-01-10",
  "sex": "F",
  "phone": "+1 (314) 750-1409",
  "email": "jane@example.com",
  "allergyProfile": { "status": "known", "allergies": ["penicillin", "sulfa"] },
  "defaultAddress": {
    "address1": "123 Main St",
    "address2": "Apt 4B",
    "city": "Dallas",
    "state": "TX",
    "zip": "75201",
    "isDefault": true
  }
}

Body

externalPatientId string | null body
Your stable patient identifier. Used for upsert matching within the clinic.
firstName string | null body
Required when creating a new patient.
lastName string | null body
Required when creating a new patient.
dateOfBirth string · date | null body
ISO date, e.g. 1980-01-10. Required when creating a new patient.
sex string | null body
One of M, F, or U.
phone string | null body
U.S./Canada phone: 10 digits, optionally prefixed with 1 or +1. Spaces, hyphens, periods, and parentheses around the area code are accepted. Area code and exchange must start with 2–9. At most 50 input characters after trimming; extensions, letters, and other country codes return 400. Saved as 10 digits, e.g. 3147501409. Required when creating a new patient.
email string | null body
Patient email. Required when creating a new patient, and used as a fallback match when externalPatientId is absent.
allergyProfile object | null body
Optional complete replacement: status must be unknown, no_known_allergies, or known. For known, supply allergies as 1–50 nonblank strings, each at most 500 characters after trimming. For either other status, omit allergies or send an empty array. Null or omitted profile preserves an existing record; new patients default to unknown. Never infer no known allergies from missing data. Invalid profiles return 400. Use status instead of sentinel entries such as “no known allergies”, NKA, or NKDA.
defaultAddress object | null body
An optional default address to store on the new or resolved patient.
Show child attributes
address1 string required
Street address.
address2 string | null
Apartment, suite, unit, etc.
city string required
City.
state string required
Recognized two-letter code for one of the 50 U.S. states or DC, e.g. TX. Full names are rejected. See shipping state errors.
zip string required
ZIP / postal code.
isDefault boolean default: true
Whether to make this the patient's default address.

RxRelay requires a recorded allergy profile for Emerald orders: known with a valid list, or explicit no_known_allergies. This applies immediately to new and unsent orders, including sandbox, partner API, and approval flows. Missing, unknown, or invalid profiles return 400 with code patient_allergies_required on creation. PATCH an existing patient before ordering by patientId, or supply a profile when creating/upserting an inline patient. Other pharmacies remain optional. Emerald receives allergies for its generated prescription PDF only, not its structured pharmacy patient record. Boothwyn receives the list; downstream use and acceptance still need vendor confirmation. Direct LifeFile, Kaduceus, and WellSync do not receive allergies through current adapters. Before Emerald’s first send, RxRelay refreshes only the allergies in any prepared payload from the saved patient. Patient edits never resend an attempted order; completed idempotent replays remain unchanged. This is a RxRelay policy, not automated allergy screening.

Update a patient

Updates a patient's demographics and allergy profile. 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.

PATCH /v1/patients/{patientId}
Request
cURL
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",
  "allergyProfile": { "status": "known", "allergies": ["penicillin", "sulfa"] }
}'
Using an organization API key

Organization key: clinicId or externalClinicId is required in the query string, including patient creation, updates, and address writes. Use the clinic UUID from GET /v1/clinics, or its configured externalClinicId. Patient IDs alone do not select a clinic. Organization quickstart and errors

Organization key request
cURL
curl -X PATCH "https://api.rxrelay.ai/v1/patients/b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e?clinicId=11111111-1111-1111-1111-111111111111" \
  -H "Authorization: Bearer rxrelay_sandbox_your_org_key" \
  -H "Content-Type: application/json" \
  -d '{
  "phone": "5125550199",
  "email": "jane.doe@example.com",
  "externalPatientId": "patient-456",
  "allergyProfile": { "status": "known", "allergies": ["penicillin", "sulfa"] }
}'
Response
200
{
  "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
  "externalPatientId": "patient-456",
  "firstName": "Jane",
  "lastName": "Doe",
  "dateOfBirth": "1980-01-10",
  "sex": "F",
  "phone": "3147501409",
  "email": "jane@example.com",
  "allergyProfile": { "status": "known", "allergies": ["penicillin", "sulfa"] },
  "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"
    }
  ]
}
Request body
JSON
{
  "phone": "5125550199",
  "email": "jane.doe@example.com",
  "externalPatientId": "patient-456",
  "allergyProfile": { "status": "known", "allergies": ["penicillin", "sulfa"] }
}

Body

firstName string | null body
New first name.
lastName string | null body
New last name.
dateOfBirth string · date | null body
New date of birth, ISO date.
sex string | null body
One of M, F, or U.
phone string | null body
U.S./Canada phone: 10 digits, optionally prefixed with 1 or +1. Spaces, hyphens, periods, and parentheses around the area code are accepted. Area code and exchange must start with 2–9. At most 50 input characters after trimming; extensions, letters, and other country codes return 400. Saved as 10 digits, e.g. 3147501409. Omitted, null, or blank phone leaves the saved value unchanged.
email string | null body
New email.
allergyProfile object | null body
Optional complete replacement: status must be unknown, no_known_allergies, or known. For known, supply allergies as 1–50 nonblank strings, each at most 500 characters after trimming. For either other status, omit allergies or send an empty array. Null or omitted profile preserves an existing record; new patients default to unknown. Never infer no known allergies from missing data. Invalid profiles return 400. Use status instead of sentinel entries such as “no known allergies”, NKA, or NKDA.
externalPatientId string | null body
(Re)maps your patient identifier to this patient. 409 Conflict if already mapped to a different patient.

Create an order

Submits a single-pharmacy order with one or more Rx items. Every item must route to the same pharmacy; create separate API orders for products from different pharmacies. The patient is resolved in order of precedence: first by patientId (a direct match to an existing RxRelay patient — when set, demographics are ignored and a non-null allergyProfile is rejected; PATCH allergies first), 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.

POST /v1/orders
Request
cURL
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": "2Day",
  "items": [
    {
      "externalPrescriptionId": "rx-001",
      "medicationId": "7c9e6a1b-1f4d-4a2e-9c3b-9a1d2e3f4a5b",
      "quantity": "1",
      "directions": "[Replace with the prescriber-approved directions]",
      "pharmacyNotes": "Please contact the clinic with fulfillment questions.",
      "clinicalJustification": "[Replace with the prescriber’s patient-specific clinical rationale]",
      "refills": 0
    }
  ]
}'
Using an organization API key

Organization key: include clinicId or externalClinicId at the top level of the order JSON body. Keep the Idempotency-Key header and all normal order fields. Query parameters and X-Clinic-Id do not select the clinic for order creation. Organization quickstart and errors

Organization key request
cURL
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 '{
  "clinicId": "11111111-1111-1111-1111-111111111111",
  "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": "2Day",
  "items": [
    {
      "externalPrescriptionId": "rx-001",
      "medicationId": "7c9e6a1b-1f4d-4a2e-9c3b-9a1d2e3f4a5b",
      "quantity": "1",
      "directions": "[Replace with the prescriber-approved directions]",
      "pharmacyNotes": "Please contact the clinic with fulfillment questions.",
      "clinicalJustification": "[Replace with the prescriber’s patient-specific clinical rationale]",
      "refills": 0
    }
  ]
}'
Response
201
{
  "orderId": "a4d9f0e2-7b1c-4e3a-8f5d-2c6b9a0e1f3d",
  "sourceOrderId": null,
  "externalOrderId": "partner-order-123",
  "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
  "prescriber": {
    "userId": "3f1a8c7d-2b4e-4f6a-9c8d-7e6f5a4b3c2d",
    "firstName": "Alex",
    "lastName": "Reed",
    "npi": "1234567890"
  },
  "status": "SandboxReceived",
  "shippingMethod": "Standard",
  "trackingNumber": null,
  "createdAt": "2026-06-05T12:30:00.0000000+00:00",
  "updatedAt": "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",
      "quantity": "1",
      "refills": 0,
      "directions": "[Replace with the prescriber-approved directions]",
      "externalPrescriptionId": "rx-001",
      "pharmacyNotes": "Please contact the clinic with fulfillment questions.",
      "clinicalJustification": "[Replace with the prescriber’s patient-specific clinical rationale]",
      "isSupply": false,
      "status": "SandboxReceived"
    }
  ],
  "submissions": [
    {
      "submissionId": "d3e4f5a6-b7c8-4d9e-0f1a-2b3c4d5e6f70",
      "environment": "Sandbox",
      "status": "SandboxReceived",
      "orderItemIds": ["c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f"],
      "failureReason": null
    }
  ],
  "approval": null
}
Alternative request body
shippingAddressId
{
  "externalOrderId": "partner-order-124",
  "patient": {
    "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e"
  },
  "shippingAddressId": "9b8a7c6d-5e4f-4321-9a8b-7c6d5e4f3210",
  "prescriberNpi": "1234567890",
  "shippingMethod": "2Day",
  "items": [
    {
      "externalPrescriptionId": "rx-002",
      "medicationId": "7c9e6a1b-1f4d-4a2e-9c3b-9a1d2e3f4a5b",
      "quantity": "1",
      "directions": "[Replace with the prescriber-approved directions]",
      "clinicalJustification": "[Replace with the prescriber’s patient-specific clinical rationale]",
      "refills": 0
    }
  ]
}

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 body
Organization keys only. The RxRelay clinic the order is for. Provide this or externalClinicId. For clinic-scoped keys, omit this field or match the key’s clinic; a different clinic is rejected.
externalClinicId string | null body
Organization keys only. Your own identifier for the target clinic. Provide this or clinicId.
externalOrderId string | null body
Your identifier for the order. Stored and echoed back on the order response.
metadata Record<string, string | number | boolean | null> | null body
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 body required
The patient this order is for. Used to match or create a patient in the clinic.
Show child attributes
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
U.S./Canada phone: 10 digits, optionally prefixed with 1 or +1. Spaces, hyphens, periods, and parentheses around the area code are accepted. Area code and exchange must start with 2–9. At most 50 input characters after trimming; extensions, letters, and other country codes return 400. Saved as 10 digits, e.g. 3147501409. Required when creating a new patient.
email string | null
Patient email. Required when creating a new patient, and used as a fallback match when externalPatientId is absent.
allergyProfile object | null
Optional complete replacement: status must be unknown, no_known_allergies, or known. For known, supply allergies as 1–50 nonblank strings, each at most 500 characters after trimming. For either other status, omit allergies or send an empty array. Null or omitted profile preserves an existing record; new patients default to unknown. Never infer no known allergies from missing data. Invalid profiles return 400. Use status instead of sentinel entries such as “no known allergies”, NKA, or NKDA. Cannot be supplied with patientId; PATCH the existing patient first.
shippingAddress object body
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.
Show child attributes
address1 string required
Street address.
address2 string | null
Apartment, suite, unit, etc.
city string required
City.
state string required
Recognized two-letter code for one of the 50 U.S. states or DC, e.g. TX. Full names are rejected. See shipping state errors.
zip string required
ZIP / postal code.
shippingAddressId string · uuid | null body
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 body
Prescriber's RxRelay user ID. Provide this or prescriberNpi; the prescriber must belong to the clinic.
prescriberNpi string | null body
Prescriber's NPI. Used to resolve the prescriber when prescriberUserId is omitted.
shippingMethod string | null body
Requested normalized delivery speed. Read the fulfilling pharmacy's shippingMethods from List pharmacies and prefer 2Day or Overnight. 2Day, 2-day, two day, and the legacy value Standard are accepted case-insensitively and stored as Standard; that name remains in responses for compatibility. Some pharmacies fix the service regardless of the request—for example, Optimal Balance resolves a submitted 2Day value to Overnight. Emerald accepts only its configured services and rejects unavailable speeds. RxRelay translates the normalized speed to the pharmacy service code; partners never need Emerald-specific codes. When omitted for a pharmacy without a fixed service, the legacy fallback is two-day; Emerald requires two-day to be configured for that fallback. Existing integrations may continue sending Standard.
items object[] body required
At least one Rx item is required.
Show child attributes
medicationId string · uuid required
A medication ID from GET /v1/medications.
medicationName string | null
Overrides the catalog name on this line. Defaults to the medication's catalog name.
quantity string required
Dispense quantity, e.g. 30. For Emerald counted products, use 1 with obsidian.dispenseQuantity for the exact unit count. Otherwise this retains legacy pack-count semantics, with a local limit of 100 expanded prescriptions per order. When the selected medication has a non-null requiredQuantity, this value must match it or the order returns 400 Bad Request.
directions string required
Sig / directions for the patient.
clinicalJustification string | null
Boothwyn-only patient-specific rationale, required when the catalog’s requiresClinicalJustification is true. Up to 4,000 characters (RxRelay’s limit); whitespace alone is invalid when required. Applies in sandbox and production. Sent in Boothwyn’s dedicated prescription field, separately from pharmacy notes. Supplying a nonblank value for another pharmacy returns 400. Example bracketed text must be replaced with prescriber-provided rationale; it is not a clinical default.
pharmacyNotes string | null
Optional note sent to the pharmacy for Kaduceus/Pioneer and Boothwyn medications, limited to 500 characters per item. Boothwyn receives the note on its prescription line. Distinct notes on one Kaduceus submission are combined into its order-level notes field and must also total 500 characters or fewer; supplying this field for another pharmacy returns 400 Bad Request.
refills integer required
Number of refills. Must be zero for Emerald.
obsidian object | null
Emerald-only clinical inputs. Other platforms reject this field when supplied.
Show child attributes
dispenseQuantity integer | null
Exact positive unit count for counted products (units: "each"); requires quantity: "1". Independent of catalog pack size. Omit for measured products. Omission preserves legacy pack-count behavior.
daysSupply integer | null
Positive days supply for the full explicit dispense quantity, or per pack for legacy/measured products; required if the catalog has no default.
clinicalDifferenceStatement string | null
Clinician-approved statement, up to 4,000 characters. Required for products flagged by the catalog, unless a nonblank default exists. An explicit blank overrides the default and fails when required.
externalPrescriptionId string | null
Your identifier for this prescription line. Echoed back on the item.

An Emerald submission with an uncertain outcome is held for reconciliation. Do not create a replacement order or change its idempotency key to bypass that hold. Retrieve the existing order and contact support to reconcile with the pharmacy.

Emerald pharmacy issues may appear as Failed and later recover to Accepted. Keep retrieving the existing order; a pharmacy issue does not authorize a replacement prescription. Shipment tracking uses the existing order.tracking_updated event. Retrieve order status for other lifecycle changes.

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.
prescriber object | null
The prescriber the order was written under, resolved from the prescriberUserId or prescriberNpi you supplied. Null when the order has no prescriber on record.
Show child attributes
userId string · uuid
The prescriber's RxRelay user ID — the same userId returned by List prescribers.
firstName string | null
Prescriber's first name.
lastName string | null
Prescriber's last name.
npi string | null
Prescriber's NPI.
status string
Aggregate order status — one of Queued, SandboxReceived, DemoRecorded, Pending, Sent, Shipped, Delivered, Failed, Cancelled, or PendingApproval. PendingApproval means the order was staged in the app or through the API and is waiting on approval; nothing has been sent to a pharmacy.
shippingMethod string
Effective normalized shipping method stored after routing: Standard (the legacy response name for two-day) or Overnight. It may differ from the requested value when the pharmacy owns service selection; Optimal Balance is always Overnight, while Boothwyn's vendor-native service is configured separately and represented here as Standard.
trackingNumber string | null
The latest tracking number for the order's pharmacy submission. Null until tracking is received.
createdAt string · date-time
When the order was created.
updatedAt string · date-time
The latest partner-visible order activity. Advances for submission attempts, approval or cancellation, and vendor status or tracking events; equals createdAt until the order changes.
metadata Record<string, string | number | boolean | null> | null
The metadata supplied on the order, if any.
items object[]
Per-item status.
Show child attributes
orderItemId string · uuid
RxRelay order item ID.
medicationId string · uuid
Medication on this line.
medicationName string
Resolved medication name.
quantity string
Quantity saved on this order line, preserving the order request's quantity semantics. Included in full order responses and tracking webhooks; this is the ordered quantity, not a confirmation of the amount shipped.
refills integer
Number of refills saved on this order line. Included in full order responses and tracking webhooks.
directions string
Sig / directions stored for this prescription line.
externalPrescriptionId string | null
The identifier you supplied for this line.
clinicalJustification string | null
The patient-specific Boothwyn rationale saved on this prescription. Returned in full order responses and order events that embed that response. Tracking and approval events do not populate this field; retrieve the full order when needed.
pharmacyNotes string | null
The pharmacy note snapshotted on this prescription line, if supplied.
obsidian object | null
Frozen Emerald prescription details: lfProductId, units, packSize, packCount, optional dispenseQuantity, daysSupply for each prescription, dateWritten (YYYY-MM-DD), and clinicalDifferenceStatement. Available on full order responses and order events that embed that response; tracking and approval events do not populate these details.
isSupply boolean
Whether this line is a non-prescription supply item.
status string
Item status, or Queued before dispatch.
submissions object[]
RxRelay submission status and item correlation. Downstream pharmacy routing details are not exposed.
Show child attributes
submissionId string · uuid
RxRelay submission ID.
environment string
Sandbox or Production.
status string
Submission status.
orderItemIds string · uuid[]
Order items included in this submission.
failureReason string | null
Normalized pharmacy error when the submission failed or was rejected; otherwise null.
sourceOrderId string · uuid | null
Immediate source order for API re-orders. Included in full responses, order lists, and order webhooks; null otherwise.
approval object | null
Present only when the order went through the RxRelay prescriber approval queue. Null when approval was not requested.
Show child attributes
status string
One of Pending, Approved, or Rejected.
submittedForApprovalAt string · date-time | null
When the order was staged for approval.
reviewedAt string · date-time | null
When a prescriber or clinic admin reviewed it.
note string | null
Reviewer's note, typically the reason for a rejection.

Errors

400

Bad Request

Missing Idempotency-Key, an Emerald patient without a valid recorded allergy profile (code patient_allergies_required), an invalid allergyProfile or a non-null patient.allergyProfile supplied with patientId, a missing or invalid shipping address (an unrecognized supplied state uses code invalid_shipping_state), a patient missing a required email or phone, an invalid supplied patient phone (use a 10-digit U.S./Canada number, optionally prefixed with 1 or +1; extensions are unsupported), an unknown or unsupported medication, no resolvable prescriber, a prescriber with no NPI on file, an item whose quantity does not match the medication catalog's requiredQuantity, invalid pharmacyNotes for the selected pharmacy or length limit, missing required Boothwyn clinicalJustification or a value over RxRelay’s 4,000-character limit, clinicalJustification supplied for another pharmacy, or invalid Emerald pack count, days supply, clinical difference statement, refills, or unconfigured shipping service.

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.

422

Unprocessable Entity

A saved shipping address has an unrecognized state (code invalid_shipping_state), the selected pharmacy cannot ship to a valid state (code pharmacy_shipping_state_blocked), or items require different pharmacies (code multiple_pharmacies_not_supported). Correct the address or pharmacy selection, or split items into separate pharmacy orders; do not retry unchanged.

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.

Re-order

POST /v1/orders/{orderId}/reorder

Creates a new whole order from an existing order in the selected clinic. This is a new prescription, not a refill dispense under an existing prescription. Omit the body or send {} for clinic keys; organization keys must select clinicId or externalClinicId in the body. A read_write key and Idempotency-Key are required. Approval defaults to true; explicitly set submitForApproval: false to use normal direct submission. Sandbox requests remain record-only and non-billable, including after approval; demo and other existing dispatch restrictions still apply.

Inherits the patient, original prescriber, every medication, quantities, directions, pharmacy notes, clinical statements, Emerald prescription details, shipping method, and saved shipping destination. Current catalog, prescriber, shipping, clinical, routing, and billing rules are revalidated. No items are silently dropped and no prescriber is substituted. Missing shipping snapshots require an explicit destination. A re-order does not switch to or replace the patient's current default address. Changed Emerald product/pack details require a new order through POST /v1/orders.

Re-order into the approval queue
curl -X POST https://api.rxrelay.ai/v1/orders/a4d9f0e2-7b1c-4e3a-8f5d-2c6b9a0e1f3d/reorder \
  -H "Authorization: Bearer rxrelay_sandbox_your_key" \
  -H "Idempotency-Key: partner-order-124" \
  -H "Content-Type: application/json" \
  -d '{
  "externalOrderId": "partner-order-124",
  "submitForApproval": true
}'
Organization key: select the source clinic
curl -X POST https://api.rxrelay.ai/v1/orders/a4d9f0e2-7b1c-4e3a-8f5d-2c6b9a0e1f3d/reorder \
  -H "Authorization: Bearer rxrelay_sandbox_your_key" \
  -H "Idempotency-Key: partner-order-124" \
  -H "Content-Type: application/json" \
  -d '{
  "clinicId": "11111111-1111-1111-1111-111111111111",
  "externalOrderId": "partner-order-124",
  "submitForApproval": true
}'

Review first, then dispatch

For review before dispatch, send submitForApproval: true (or omit it). The new order returns PendingApproval and approval.status Pending. No pharmacy dispatch or billable usage occurs before approval. After an authorized reviewer signs off, approve that NEW returned orderId in the RxRelay app or through POST /v1/orders/{newOrderId}/approval with approve: true and approverUserId. The approver must be a prescriber, owner, or clinic_admin in that clinic. The live-key examples below show staging and then releasing the new order; replace the approval URL's example ID with the orderId returned by staging.

1. Stage a live re-order for review
curl -X POST https://api.rxrelay.ai/v1/orders/a4d9f0e2-7b1c-4e3a-8f5d-2c6b9a0e1f3d/reorder \
  -H "Authorization: Bearer rxrelay_live_your_key" \
  -H "Idempotency-Key: partner-order-124" \
  -H "Content-Type: application/json" \
  -d '{
  "externalOrderId": "partner-order-124",
  "submitForApproval": true
}'
2. Approve the NEW order to release it
curl -X POST https://api.rxrelay.ai/v1/orders/e5a0f1b3-8c2d-4f4b-9a6e-3d7c0b1f2a4e/approval \
  -H "Authorization: Bearer rxrelay_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "approve": true,
    "approverUserId": "3f1a8c7d-2b4e-4f6a-9c8d-7e6f5a4b3c2d"
  }'

Direct pharmacy submission without the review queue

For direct pharmacy submission without the RxRelay review queue, explicitly send submitForApproval: false with a live read_write key once your own prescribing/authorization process is complete. The new order is validated and enters normal pharmacy dispatch without a separate approval call; approval is null. Existing demo-mode and dispatch restrictions still apply. 201 means a new order was created, not that the pharmacy accepted or fulfilled it: inspect status, submissions[].status, and submissions[].failureReason and follow order retrieval/webhooks. Using a sandbox key instead records SandboxReceived without dispatch or billing.

Live re-order with submitForApproval: false
curl -X POST https://api.rxrelay.ai/v1/orders/a4d9f0e2-7b1c-4e3a-8f5d-2c6b9a0e1f3d/reorder \
  -H "Authorization: Bearer rxrelay_live_your_key" \
  -H "Idempotency-Key: partner-order-126" \
  -H "Content-Type: application/json" \
  -d '{
  "externalOrderId": "partner-order-126",
  "submitForApproval": false
}'

To release an already queued order, approve its new orderId. Do not re-post the source re-order with submitForApproval: false: changing the body with the same idempotency key returns 409, and using a new key creates another order. On the approval endpoint, approve: false REJECTS the order; it does not mean skip review or dispatch directly. Organization keys select clinicId or externalClinicId in the re-order body for either mode; the later approval endpoint derives clinic access from the new order ID.

Safety modes also apply when submitForApproval is false. Sandbox API keys never call a pharmacy or accrue billable usage, including after approval with a live key. A live key in a sandbox-mode clinic can send only to a pharmacy test connection; missing test configuration never authorizes a production send. Live-key orders created or approved in demo mode stay record-only and non-billable after the clinic returns to live mode: direct submission or approval results in DemoRecorded. Clinic modes are checked again at dispatch, including queued work and retries. If a clinic switches to sandbox after a production route was saved, that route is blocked with Failed and an explanatory failureReason rather than sent or silently rerouted. Changing a clinic mode does not undo a pharmacy request already sent.

Default behavior differs by endpoint: POST /v1/orders defaults submitForApproval to false; POST /v1/orders/{orderId}/reorder defaults it to true. Both accept explicit true to stage for approval or false to request normal direct submission. These flags never turn sandbox API orders into pharmacy submissions.

Request

orderId string · uuid path required
Existing RxRelay order in the selected clinic.
Idempotency-Key string header required
Unique key for this intended new order; reuse it for retries.
submitForApproval boolean
Defaults to true. False explicitly requests normal direct submission after validation; pharmacy acceptance is reported in the resulting status.
externalOrderId string | null
Optional new partner order ID. Never inherited from the original.
shippingAddressId string · uuid | null
Optional active address belonging to the same patient. Mutually exclusive with shippingAddress.
shippingAddress object | null
Optional destination with address1, address2, city, state, and zip, using normal order address rules. If neither address override is supplied, uses the original shipping snapshot.
shippingMethod string | null
Optional override using normal shipping-method rules; otherwise inherits the original effective method.
prescriberUserId string · uuid | null
Optional current clinic prescriber. Mutually exclusive with prescriberNpi.
prescriberNpi string | null
Optional current clinic prescriber NPI. If neither prescriber override is supplied, revalidates the original prescriber.
refills integer
Defaults to zero; must be non-negative. Explicitly authorizes this many refills on every new item, subject to pharmacy rules. Emerald requires zero. Use ordinary creation for different refill counts per item.
metadata object | null
Optional fresh scalar metadata, using normal order limits. Not inherited.
stagedByUserId string · uuid | null
Optional preparation attribution to a member of this clinic. Otherwise attributed to the API key.
clinicId / externalClinicId string | null
Organization keys must select the source order's clinic in the body, using its UUID or external alias. Clinic keys imply their clinic. Query parameters and X-Clinic-Id do not select it.

Response and retries

Returns 201 Created using the normal new-order response shape, with sourceOrderId identifying the immediate original order. All other identifiers, statuses, approvals, tracking, pricing, and history belong to the new order; the source is unchanged. External order/prescription IDs and metadata are not copied. sourceOrderId also appears in order retrieval, lists, and order webhooks (including approval and tracking); it is null for orders without API re-order history. Existing webhook event names are reused.

New order
201
{
  "orderId": "e5a0f1b3-8c2d-4f4b-9a6e-3d7c0b1f2a4e",
  "sourceOrderId": "a4d9f0e2-7b1c-4e3a-8f5d-2c6b9a0e1f3d",
  "externalOrderId": "partner-order-124",
  "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
  "prescriber": {
    "userId": "3f1a8c7d-2b4e-4f6a-9c8d-7e6f5a4b3c2d",
    "firstName": "Alex",
    "lastName": "Reed",
    "npi": "1234567890"
  },
  "status": "PendingApproval",
  "shippingMethod": "Standard",
  "trackingNumber": null,
  "createdAt": "2026-10-07T12:30:00.0000000+00:00",
  "updatedAt": "2026-10-07T12:30:00.0000000+00:00",
  "metadata": null,
  "items": [{
    "orderItemId": "f6b1a2c4-9d3e-405c-8b7f-4e8d1c2a3b5f",
    "medicationId": "7c9e6a1b-1f4d-4a2e-9c3b-9a1d2e3f4a5b",
    "medicationName": "Semaglutide 2.5mg/mL",
    "quantity": "1",
    "refills": 0,
    "directions": "[The original prescriber-approved directions]",
    "externalPrescriptionId": null,
    "pharmacyNotes": null,
    "clinicalJustification": "[The original patient-specific clinical rationale]",
    "isSupply": false,
    "status": "PendingApproval"
  }],
  "submissions": [{
    "submissionId": "07c2b3d5-ae4f-416d-9c80-5f9e2d3b4c60",
    "environment": "Sandbox",
    "status": "PendingApproval",
    "orderItemIds": ["f6b1a2c4-9d3e-405c-8b7f-4e8d1c2a3b5f"],
    "failureReason": null
  }],
  "approval": {
    "status": "Pending",
    "submittedForApprovalAt": "2026-10-07T12:30:00.0000000+00:00",
    "reviewedAt": null,
    "note": null
  }
}

Use a fresh idempotency key per intended new order, and keep that key, source order ID, and request body unchanged on ambiguous retries. A completed replay returns 200 with the same new order's current details, even if the source or catalog has since changed. Reusing a key with a different source, body, or between create and reorder returns 409. An in-progress request also returns 409; retry the identical request shortly. Re-ordering is not a way to retry an uncertain pharmacy submission.

Errors

Normal order errors apply. Unknown or cross-clinic source orders return 404. Invalid overrides, a missing shipping snapshot, a departed prescriber, an unavailable/controlled medication, or changed Emerald product/pack details return 400. Pharmacy shipping restrictions return 422. The whole request fails validation rather than silently omitting lines. Correct the indicated details or create a fresh order with the current catalog; do not blindly retry validation failures.

Retrieve an order

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

GET /v1/orders/{orderId}
Request
cURL
curl https://api.rxrelay.ai/v1/orders/a4d9f0e2-7b1c-4e3a-8f5d-2c6b9a0e1f3d \
  -H "Authorization: Bearer rxrelay_sandbox_your_key"
Using an organization API key

Organization key: no clinic selector is required here. RxRelay reads the order's clinic and checks that it is an enabled clinic in your organization. Unknown and unauthorized orders return 404. Organization quickstart and errors

Organization key request
cURL
curl https://api.rxrelay.ai/v1/orders/a4d9f0e2-7b1c-4e3a-8f5d-2c6b9a0e1f3d \
  -H "Authorization: Bearer rxrelay_sandbox_your_org_key"
Response
200
{
  "orderId": "a4d9f0e2-7b1c-4e3a-8f5d-2c6b9a0e1f3d",
  "sourceOrderId": null,
  "externalOrderId": "partner-order-123",
  "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
  "prescriber": {
    "userId": "3f1a8c7d-2b4e-4f6a-9c8d-7e6f5a4b3c2d",
    "firstName": "Alex",
    "lastName": "Reed",
    "npi": "1234567890"
  },
  "status": "Shipped",
  "shippingMethod": "Standard",
  "trackingNumber": "1Z999AA10123456784",
  "createdAt": "2026-06-05T12:30:00.0000000+00:00",
  "updatedAt": "2026-06-08T16:45:12.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",
      "quantity": "1",
      "refills": 0,
      "directions": "[Replace with the prescriber-approved directions]",
      "externalPrescriptionId": "rx-001",
      "pharmacyNotes": "Please contact the clinic with fulfillment questions.",
      "clinicalJustification": "[Replace with the prescriber’s patient-specific clinical rationale]",
      "isSupply": false,
      "status": "Shipped"
    }
  ],
  "submissions": [
    {
      "submissionId": "d3e4f5a6-b7c8-4d9e-0f1a-2b3c4d5e6f70",
      "environment": "Production",
      "status": "Shipped",
      "orderItemIds": ["c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f"],
      "failureReason": null
    }
  ],
  "approval": null
}

Path parameters

orderId string · uuid path 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.

Approve or reject an order

Optional two-step ordering. Send submitForApproval: true on create to hold an order in the RxRelay approval queue instead of dispatching it, then release it here. Nothing reaches a pharmacy and nothing is billed before approval. Orders created with a sandbox API key remain non-dispatching and non-billable after approval: submissions stay Sandbox, status becomes SandboxReceived, and approval.status becomes Approved. This also applies when approval happens in the app or through a live key. Sandbox keys can approve or reject only sandbox-origin API orders; other orders return 403 with code sandbox_order_required.

Use this when your own product distinguishes staff who prepare orders from clinicians who sign off on them. You decide who may do what in your app; RxRelay independently verifies that the user you name is authorized here.

POST /v1/orders/{orderId}/approval
1. Stage an order for approval
cURL
curl -X POST https://api.rxrelay.ai/v1/orders \
  -H "Authorization: Bearer rxrelay_sandbox_your_key" \
  -H "Idempotency-Key: partner-order-125" \
  -H "Content-Type: application/json" \
  -d '{
  "externalOrderId": "partner-order-125",
  "submitForApproval": true,
  "stagedByUserId": "5c4b3a29-8d7e-4f61-b0a2-3c4d5e6f7a8b",
  "patient": {
    "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e"
  },
  "shippingAddressId": "9b8a7c6d-5e4f-4321-9a8b-7c6d5e4f3210",
  "prescriberNpi": "1234567890",
  "shippingMethod": "Standard",
  "items": [
    {
      "externalPrescriptionId": "rx-003",
      "medicationId": "7c9e6a1b-1f4d-4a2e-9c3b-9a1d2e3f4a5b",
      "quantity": "1",
      "directions": "[Replace with the prescriber-approved directions]",
      "pharmacyNotes": "Please contact the clinic with fulfillment questions.",
      "clinicalJustification": "[Replace with the prescriber’s patient-specific clinical rationale]",
      "refills": 0
    }
  ]
}'
2. Approve it
cURL
curl -X POST https://api.rxrelay.ai/v1/orders/a4d9f0e2-7b1c-4e3a-8f5d-2c6b9a0e1f3d/approval \
  -H "Authorization: Bearer rxrelay_sandbox_your_key" \
  -H "Content-Type: application/json" \
  -d '{
  "approve": true,
  "approverUserId": "3f1a8c7d-2b4e-4f6a-9c8d-7e6f5a4b3c2d"
}'
Using an organization API key

Organization key: no clinic selector is required here. RxRelay reads the order's clinic and checks that it is an enabled clinic in your organization. Unknown and unauthorized orders return 404. Organization quickstart and errors

Organization key request
cURL
curl -X POST https://api.rxrelay.ai/v1/orders/a4d9f0e2-7b1c-4e3a-8f5d-2c6b9a0e1f3d/approval \
  -H "Authorization: Bearer rxrelay_sandbox_your_org_key" \
  -H "Content-Type: application/json" \
  -d '{
  "approve": true,
  "approverUserId": "3f1a8c7d-2b4e-4f6a-9c8d-7e6f5a4b3c2d"
}'
Rejecting instead
{
  "approve": false,
  "approverUserId": "3f1a8c7d-2b4e-4f6a-9c8d-7e6f5a4b3c2d",
  "note": "Dose needs review before this goes out."
}

Body parameters

approve boolean required
true approves the order; live orders are released to the pharmacy, while sandbox API orders are recorded without dispatch. false rejects it permanently — a rejected order is never sent and cannot be un-rejected.
approverUserId string · uuid required
The RxRelay user approving. Must be a member of the order's clinic holding prescriber, owner, or clinic_admin. Use GET /v1/users to look up ids and roles. This user is recorded as the approver on the order.
prescriberUserId string · uuid | null
Optionally reassign the prescriber of record while approving. Ignored when rejecting. Must be a prescriber in the clinic with an NPI on file.
note string | null
Free text kept with the order. Typically the reason for a rejection.

Default behavior differs by endpoint: POST /v1/orders defaults submitForApproval to false; POST /v1/orders/{orderId}/reorder defaults it to true. Both accept explicit true to stage for approval or false to request normal direct submission. These flags never turn sandbox API orders into pharmacy submissions.

Create-order fields

submitForApproval boolean default: false
Hold the order for approval instead of dispatching it. Works with sandbox and live keys alike, so the whole flow can be exercised in sandbox.
stagedByUserId string · uuid | null
Who prepared the order, for attribution. Optional — omit it and the order is attributed to the API key instead, so you need not onboard your back-office staff as RxRelay users to adopt the queue.

Identity

RxRelay validates that approverUserId is authorized to approve in that clinic, but cannot observe that the person acted — the same trust already placed in the prescriber you name when creating an order. The originating API key is recorded alongside the approval, so an approval made through this API stays distinguishable from one made in the RxRelay app.

Errors

400

Bad Request

approverUserId is missing, is not a member of the order's clinic, or is not a prescriber, owner, or clinic admin there. Also returned when an overriding prescriberUserId is not a prescriber in the clinic or has no NPI on file.

402

Payment Required

Approving would dispatch the order, but the clinic is past due or has no card on file.

403

Forbidden

The API key is read-only, ordering is disabled for the clinic, or a sandbox key attempted to approve/reject an order not created with a sandbox API key (code sandbox_order_required).

404

Not Found

No order with that id is reachable by this API key.

409

Conflict

The order was already approved or rejected, was cancelled, or never required approval.

List orders

Lists or searches the clinic's orders, newest first, with pagination. Each summary includes the latest tracking number and an update timestamp, so you can identify changed orders before fetching full item and submission detail via GET /v1/orders/{orderId}.

GET /v1/orders
Request
cURL
curl "https://api.rxrelay.ai/v1/orders?status=Shipped&limit=25&offset=0" \
  -H "Authorization: Bearer rxrelay_sandbox_your_key"
Using an organization API key

Organization key: clinicId or externalClinicId is required in the query string, including patient creation, updates, and address writes. Use the clinic UUID from GET /v1/clinics, or its configured externalClinicId. Patient IDs alone do not select a clinic. Organization quickstart and errors

Organization key request
cURL
curl "https://api.rxrelay.ai/v1/orders?status=Shipped&limit=25&offset=0&clinicId=11111111-1111-1111-1111-111111111111" \
  -H "Authorization: Bearer rxrelay_sandbox_your_org_key"
Response
200
{
  "orders": [
    {
      "orderId": "a4d9f0e2-7b1c-4e3a-8f5d-2c6b9a0e1f3d",
      "sourceOrderId": null,
      "externalOrderId": "partner-order-123",
      "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
      "prescriber": {
        "userId": "3f1a8c7d-2b4e-4f6a-9c8d-7e6f5a4b3c2d",
        "firstName": "Alex",
        "lastName": "Reed",
        "npi": "1234567890"
      },
      "status": "Shipped",
      "shippingMethod": "Standard",
      "trackingNumber": "1Z999AA10123456784",
      "createdAt": "2026-07-24T16:35:42.5802830+00:00",
      "updatedAt": "2026-08-05T14:54:29.0444470+00:00",
      "metadata": {
        "key1": "value1",
        "key2": "value2"
      },
      "approval": null
    }
  ],
  "total": 1,
  "limit": 25,
  "offset": 0
}

Query parameters

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

Response

orders object[]
The page of order summaries, newest first.
Show child attributes
orderId string · uuid
RxRelay order ID.
externalOrderId string | null
The identifier you supplied, if any.
patientId string · uuid
The patient on the order.
prescriber object | null
The prescriber the order was written under — userId, firstName, lastName, and npi. Null when the order has no prescriber on record.
status string
Derived order status.
shippingMethod string
Effective normalized shipping method stored after routing: Standard (two-day) or Overnight. Pharmacy-owned service selection may override the requested value.
trackingNumber string | null
The latest tracking number for the order's pharmacy submission. Null until tracking is received.
createdAt string · date-time
When the order was created.
updatedAt string · date-time
The latest partner-visible order activity. Advances for submission attempts, approval or cancellation, and vendor status or tracking events; equals createdAt until the order changes.
metadata Record<string, string | number | boolean | null> | null
The metadata supplied on the order, if any.
approval object | null
Approval status and timestamps when the order went through the RxRelay prescriber approval queue; otherwise null.
total integer
Total matching orders across all pages.
limit integer
The effective page size applied.
offset integer
The offset applied.

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

GET /v1/patients/{patientId}/orders
Request
cURL
curl "https://api.rxrelay.ai/v1/patients/b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e/orders?limit=25&offset=0" \
  -H "Authorization: Bearer rxrelay_sandbox_your_key"
Using an organization API key

Organization key: clinicId or externalClinicId is required in the query string, including patient creation, updates, and address writes. Use the clinic UUID from GET /v1/clinics, or its configured externalClinicId. Patient IDs alone do not select a clinic. Organization quickstart and errors

Organization key request
cURL
curl "https://api.rxrelay.ai/v1/patients/b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e/orders?limit=25&offset=0&clinicId=11111111-1111-1111-1111-111111111111" \
  -H "Authorization: Bearer rxrelay_sandbox_your_org_key"
Response
200
{
  "orders": [
    {
      "orderId": "a4d9f0e2-7b1c-4e3a-8f5d-2c6b9a0e1f3d",
      "sourceOrderId": null,
      "externalOrderId": "partner-order-123",
      "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
      "prescriber": {
        "userId": "3f1a8c7d-2b4e-4f6a-9c8d-7e6f5a4b3c2d",
        "firstName": "Alex",
        "lastName": "Reed",
        "npi": "1234567890"
      },
      "status": "Shipped",
      "shippingMethod": "Standard",
      "trackingNumber": "1Z999AA10123456784",
      "createdAt": "2026-07-24T16:35:42.5802830+00:00",
      "updatedAt": "2026-08-05T14:54:29.0444470+00:00",
      "metadata": {
        "key1": "value1",
        "key2": "value2"
      },
      "approval": null
    }
  ],
  "total": 1,
  "limit": 25,
  "offset": 0
}

Path parameters

patientId string · uuid path required
The RxRelay patient ID.

Patient addresses

Retrieve or upsert addresses for an existing RxRelay patient. Upserts return the existing address when the normalized address already matches, so partners can safely store the returned address ID and later pass it as shippingAddressId.

GET /v1/patients/{patientId}/addresses
Request
cURL
curl https://api.rxrelay.ai/v1/patients/b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e/addresses \
  -H "Authorization: Bearer rxrelay_sandbox_your_key"
Using an organization API key

Organization key: clinicId or externalClinicId is required in the query string, including patient creation, updates, and address writes. Use the clinic UUID from GET /v1/clinics, or its configured externalClinicId. Patient IDs alone do not select a clinic. Organization quickstart and errors

Organization key request
cURL
curl "https://api.rxrelay.ai/v1/patients/b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e/addresses?clinicId=11111111-1111-1111-1111-111111111111" \
  -H "Authorization: Bearer rxrelay_sandbox_your_org_key"
Response
200
[
  {
    "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

Send a recognized two-letter code for one of the 50 U.S. states or DC in shippingAddress.state, patient defaultAddress.state, and patient address writes. Full names, territories, and unknown codes such as ZZ are not accepted in /v1 requests. An unrecognized nonblank state returns 400 with code invalid_shipping_state and shippingState containing the supplied value. Missing required address fields return a separate 400 validation error.

Request
cURL
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
}'
Using an organization API key

Organization key: clinicId or externalClinicId is required in the query string, including patient creation, updates, and address writes. Use the clinic UUID from GET /v1/clinics, or its configured externalClinicId. Patient IDs alone do not select a clinic. Organization quickstart and errors

Organization key request
cURL
curl -X POST "https://api.rxrelay.ai/v1/patients/b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e/addresses?clinicId=11111111-1111-1111-1111-111111111111" \
  -H "Authorization: Bearer rxrelay_sandbox_your_org_key" \
  -H "Content-Type: application/json" \
  -d '{
  "address1": "123 Main St",
  "address2": "Apt 4B",
  "city": "Dallas",
  "state": "TX",
  "zip": "75201",
  "isDefault": true
}'
Response
200
  {
    "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}
Request
cURL
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"
Using an organization API key

Organization key: clinicId or externalClinicId is required in the query string, including patient creation, updates, and address writes. Use the clinic UUID from GET /v1/clinics, or its configured externalClinicId. Patient IDs alone do not select a clinic. Organization quickstart and errors

Organization key request
cURL
curl "https://api.rxrelay.ai/v1/patients/b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e/addresses/9b8a7c6d-5e4f-4321-9a8b-7c6d5e4f3210?clinicId=11111111-1111-1111-1111-111111111111" \
  -H "Authorization: Bearer rxrelay_sandbox_your_org_key"
Response
200
  {
    "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

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.

DELETE /v1/patients/{patientId}/addresses/{addressId}
Request
cURL
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"
Using an organization API key

Organization key: clinicId or externalClinicId is required in the query string, including patient creation, updates, and address writes. Use the clinic UUID from GET /v1/clinics, or its configured externalClinicId. Patient IDs alone do not select a clinic. Organization quickstart and errors

Organization key request
cURL
curl -X DELETE "https://api.rxrelay.ai/v1/patients/b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e/addresses/9b8a7c6d-5e4f-4321-9a8b-7c6d5e4f3210?clinicId=11111111-1111-1111-1111-111111111111" \
  -H "Authorization: Bearer rxrelay_sandbox_your_org_key"

Set the default address

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.

POST /v1/patients/{patientId}/addresses/{addressId}/default
Request
cURL
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"
Using an organization API key

Organization key: clinicId or externalClinicId is required in the query string, including patient creation, updates, and address writes. Use the clinic UUID from GET /v1/clinics, or its configured externalClinicId. Patient IDs alone do not select a clinic. Organization quickstart and errors

Organization key request
cURL
curl -X POST "https://api.rxrelay.ai/v1/patients/b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e/addresses/9b8a7c6d-5e4f-4321-9a8b-7c6d5e4f3210/default?clinicId=11111111-1111-1111-1111-111111111111" \
  -H "Authorization: Bearer rxrelay_sandbox_your_org_key"
Response
200
  {
    "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 must include clinicId or externalClinicId as query parameters, matching the medication and prescriber listing endpoints.

Organizations

Start with an organization key

  1. Use your sandbox organization key as the Bearer token. Call GET /v1/clinics and choose an enabled clinic's id. An externalClinicId works only if that alias is already configured.
  2. Fetch medications and prescribers with ?clinicId=CLINIC_UUID. Fetch or create the patient in that same clinic. Catalog inheritance can make clinics' available products different.
  3. Submit the normal POST /v1/orders payload with clinicId in its JSON body and an Idempotency-Key header. Use IDs from the selected clinic.
  4. Retrieve the result with GET /v1/orders/ORDER_UUID; no clinic selector is needed for this lookup. Sandbox API orders are record-only; switching to a live key changes dispatch behavior.

The same public endpoints support both key types. The organization comes from the key; do not send an organization ID. X-Clinic-Id does not select a clinic for these partner endpoints. Clinic keys already identify their clinic and need no selector. For organization keys, use either clinic selector; if both are present, both must match.

Where to select the clinic

EndpointOrganization-key behavior
GET /v1/clinicsNo selector: discover enabled clinics belonging to the key's organization.
POST /v1/orders; POST /v1/orders/{orderId}/reorderclinicId or externalClinicId in the JSON body.
GET /v1/medications, /v1/pharmacies, /v1/prescribersclinicId or externalClinicId in the query string.
GET /v1/patients; GET/PATCH /v1/patients/{patientId}; POST /v1/patientsclinicId or externalClinicId in the query string, including writes and single-patient lookup.
All /v1/patients/{patientId}/addresses endpointsclinicId or externalClinicId in the query string for list, get, upsert, delete, and set-default.
GET /v1/orders; GET /v1/patients/{patientId}/ordersclinicId or externalClinicId in the query string. Lists cover one clinic, not the whole organization.
GET /v1/orders/{orderId}; POST /v1/orders/{orderId}/approvalThe order determines its clinic; no selector needed. Approval still requires read_write and an eligible approver.
GET /v1/clinics/{clinicRef}/usersRxRelay clinic UUID in the path; externalClinicId aliases are not supported here.
POST /v1/organization/invitesThe key determines the organization. Requires read_write and invitations enabled; no clinic selector.
GET /v1/clinic; GET/POST /v1/usersClinic-key routes. With an org key, use GET /v1/clinics and GET /v1/clinics/{clinicId}/users. Org keys cannot register clinic users/prescribers.

There is no organization-wide patient or order list. Iterate over GET /v1/clinics, request each clinic's list, and paginate each independently. Patient lookup requires a clinic selector even when you know the patient UUID; order lookup derives it from the order.

Troubleshooting organization requests

HTTPFailureWhat to check
401Authentication failedSend Authorization: Bearer with the organization key. Check that the key is valid, not expired or revoked. An explanatory JSON body is not guaranteed.
400Clinic is requiredSupply clinicId or externalClinicId in the body for order creation, or in the query for clinic-scoped patient, catalog, and list requests.
400Invalid clinicId formatclinicId must be a UUID. Use externalClinicId for your configured string alias. Model-validation error wording may vary.
404Clinic not foundCheck GET /v1/clinics. The clinic must belong to the key's organization. If both identifiers are supplied, they must identify the same clinic.
403Clinic disabledThe selected clinic is disabled; contact RxRelay. Resource-based order lookups and clinic-user discovery instead conceal inaccessible clinics with 404.
404Patient, address, or order not foundConfirm the resource belongs to the selected clinic (or an enabled org clinic for order lookup). The response may have no explanatory detail; 404 also protects resources outside your access.
403Write forbiddenUse a read_write key for writes. Some scope failures have no explanatory body. Org keys cannot register clinic users even with read_write.
400Missing Idempotency-KeyOrder creation requires this header after clinic selection succeeds. Keep the same key and identical body when retrying the same order.
Missing clinic selector
400
{
  "title": "Clinic is required",
  "status": 400,
  "detail": "Organization API keys must specify clinicId or externalClinicId."
}

Structured errors use ProblemDetails with title, status, and detail; additional framework fields may appear. Clinic-selection errors currently have no stable code. Handle HTTP status first and display detail when present; do not require every 403/404 to contain JSON or branch on exact title text. A 404 may deliberately conceal a resource outside your access. Fix clinic selection or permissions before retrying these requests.

An organization is a parent that owns many clinics. A partner managing 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, list those clinics, and — when RxRelay enables member invitations — invite organization members or admins. Clinic creation, clinic attachment, and organization configuration are managed by RxRelay.

When both clinic identifiers are supplied, they must identify the same clinic; otherwise the request returns 404. Organization access does not grant prescribing authority: assign each prescriber to the target clinic. Patient identifiers and idempotency records remain clinic-specific; idempotency records are also specific to the API key.

Clinics can inherit an organization catalog or use a clinic override. Fetch the target clinic’s catalog before ordering. RxRelay configures inheritance during clinic onboarding; payment methods and agreement acceptance remain separate. Organization webhook inheritance replaces local destinations instead of sending to both scopes. Pausing an organization endpoint does not activate a local fallback. A detached clinic’s queued events are not sent to its former organization.

Webhook envelopes identify the originating clinic with top-level clinicId, externalClinicId, and organizationId; the latter two may be null. Use these fields to route shared organization events. Existing event IDs, payload fields and signing rules are unchanged.

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. A missing identifier returns 400; one that does not match a clinic in your organization returns 404. GET /v1/medications, GET /v1/pharmacies, and GET /v1/prescribers accept a clinicId or externalClinicId query parameter to scope their results.

Request
cURL
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 '{
  "clinicId": "11111111-1111-1111-1111-111111111111",
  "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": "2Day",
  "items": [
    {
      "externalPrescriptionId": "rx-001",
      "medicationId": "7c9e6a1b-1f4d-4a2e-9c3b-9a1d2e3f4a5b",
      "quantity": "1",
      "directions": "[Replace with the prescriber-approved directions]",
      "pharmacyNotes": "Please contact the clinic with fulfillment questions.",
      "clinicalJustification": "[Replace with the prescriber’s patient-specific clinical rationale]",
      "refills": 0
    }
  ]
}'

List clinics

Returns the enabled clinics under your organization. Read-only organization keys are supported; member invitations do not need to be enabled. Listing users in a clinic likewise requires no invitation permission.

GET /v1/clinics
Request
cURL
curl https://api.rxrelay.ai/v1/clinics \
  -H "Authorization: Bearer rxrelay_sandbox_your_org_key"
Response
200
[
  {
    "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"
  }
]

Invite an organization user

Requires a read/write organization API key and the organization member invitation permission enabled by RxRelay. The organization is determined by the key; no organization or clinic ID is accepted as a target. Both org_member and org_admin grant access across the organization's clinics. Neither assigns a prescribing role.

POST /v1/organization/invites
Request
cURL
curl -X POST https://api.rxrelay.ai/v1/organization/invites \
  -H "Authorization: Bearer rxrelay_sandbox_your_org_key" \
  -H "Content-Type: application/json" \
  -d '{
  "email": "alex.reed@example.com",
  "role": "org_admin"
}'
Response
200
{
  "organizationId": "22222222-2222-4222-8222-222222222222",
  "email": "alex.reed@example.com",
  "role": "org_admin",
  "status": "invited",
  "userId": null,
  "inviteId": "33333333-3333-4333-8333-333333333333",
  "expiresAt": "2026-10-12T12:00:00Z"
}
email string body required
Email of the person to invite.
role string body required
Exactly one of org_member or org_admin.

New users receive a seven-day registration invitation; the response status is invited with inviteId and expiresAt. Existing accounts join immediately: member_added, member_updated when promoted to admin, or already_member, with userId and no invitation. Invitations never downgrade existing admins. Both sandbox and live read/write keys can perform this action when enabled; sandbox order mode does not suppress invitation emails.

Repeating the same pending invitation returns the same invite without another email. A different role for a pending invitation returns 409; ask RxRelay to revoke it first. Invalid email/role returns 400, the wrong key scope or a disabled invitation permission returns 403, and a missing organization returns 404. Invitation codes are not returned by this API.

Partner clinic creation is retired: POST /v1/clinics returns 403 with code clinic_provisioning_removed. Organization keys cannot write clinic users through POST /v1/clinics/{clinicRef}/users. Clinic-specific user and prescriber provisioning remains available with a clinic-scoped key at POST /v1/users.

List clinic users

Organization key request
cURL
curl https://api.rxrelay.ai/v1/clinics/11111111-1111-1111-1111-111111111111/users \
  -H "Authorization: Bearer rxrelay_sandbox_your_org_key"

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.

GET /v1/clinics/{clinicRef}/users
Response
200
[
  {
    "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"
  }
]

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
cURL
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",
    "order.pending_approval",
    "order.approved",
    "order.rejected"
  ]
}'
Response
200
{
  "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",
      "order.pending_approval",
      "order.approved",
      "order.rejected"
    ],
    "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
Public HTTPS URL that receives webhook POST requests directly, without redirects.
environment string default: both
One of sandbox, live, or both.
events string[] default: default order events
Event types the endpoint should receive.

Clinic and organization destinations must use a public HTTPS URL. HTTP, localhost, private/reserved IP addresses, URL credentials, and fragments are rejected. Hostnames are checked again when connecting; all resolved addresses must be public. Redirects are not followed: configure the final receiving URL. Existing unsafe destinations fail delivery until corrected. Use a public HTTPS tunnel for local testing.

Queued deliveries and replays honor the endpoint's current environment and event subscriptions. A live event cannot be replayed to a sandbox-only endpoint, and unsubscribed events are not sent. Explicit synthetic webhook tests remain available on any active, unpaused endpoint, including live-only endpoints; they contain no order data and retain the sandbox test envelope.

Delivery payload

Headers
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
{
  "id": "evt_f2a4c6e8d0b14d09a7c1e3f5b9a2c4d6",
  "type": "order.sandbox_received",
  "environment": "sandbox",
  "createdAt": "2026-06-05T12:30:00.0000000+00:00",
  "clinicId": "11111111-1111-4111-8111-111111111111",
  "externalClinicId": "clinic-a",
  "organizationId": "22222222-2222-4222-8222-222222222222",
  "data": {
    "order": {
      "orderId": "a4d9f0e2-7b1c-4e3a-8f5d-2c6b9a0e1f3d",
      "sourceOrderId": null,
      "externalOrderId": "partner-order-123",
      "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
      "prescriber": {
        "userId": "3f1a8c7d-2b4e-4f6a-9c8d-7e6f5a4b3c2d",
        "firstName": "Alex",
        "lastName": "Reed",
        "npi": "1234567890"
      },
      "status": "SandboxReceived",
      "shippingMethod": "Standard",
      "trackingNumber": null,
      "createdAt": "2026-06-05T12:30:00.0000000+00:00",
      "updatedAt": "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",
          "quantity": "1",
          "refills": 0,
          "directions": "[Replace with the prescriber-approved directions]",
          "externalPrescriptionId": "rx-001",
          "pharmacyNotes": "Please contact the clinic with fulfillment questions.",
          "clinicalJustification": "[Replace with the prescriber’s patient-specific clinical rationale]",
          "isSupply": false,
          "status": "SandboxReceived"
        }
      ],
      "submissions": [
        {
          "submissionId": "d3e4f5a6-b7c8-4d9e-0f1a-2b3c4d5e6f70",
          "environment": "Sandbox",
          "status": "SandboxReceived",
          "orderItemIds": ["c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f"],
          "failureReason": null
        }
      ],
      "approval": null
    }
  }
}
Tracking update payload
{
  "id": "evt_8d2f8a7e6c5b4a3d9e0f1a2b3c4d5e6f",
  "type": "order.tracking_updated",
  "environment": "live",
  "createdAt": "2026-06-05T13:45:00.0000000+00:00",
  "clinicId": "11111111-1111-4111-8111-111111111111",
  "externalClinicId": "clinic-a",
  "organizationId": "22222222-2222-4222-8222-222222222222",
  "data": {
    "order": {
      "orderId": "a4d9f0e2-7b1c-4e3a-8f5d-2c6b9a0e1f3d",
      "sourceOrderId": null,
      "externalOrderId": "partner-order-123",
      "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
      "prescriber": {
        "userId": "3f1a8c7d-2b4e-4f6a-9c8d-7e6f5a4b3c2d",
        "firstName": "Alex",
        "lastName": "Reed",
        "npi": "1234567890"
      },
      "status": "Shipped",
      "shippingMethod": "Standard",
      "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",
          "quantity": "1",
          "refills": 0,
          "directions": "Take one capsule daily",
          "externalPrescriptionId": "rx-001",
          "pharmacyNotes": "Please contact the clinic with fulfillment questions.",
          "isSupply": false,
          "status": "Shipped"
        }
      ],
      "shipments": [
        {
          "shipmentId": "shp_d3e4f5a6b7c84d9e0f1a2b3c4d5e6f70",
          "carrier": "UPS",
          "trackingNumber": "1Z999AA10123456784",
          "trackingUrl": "https://www.ups.com/track?tracknum=1Z999AA10123456784",
          "orderItemIds": ["c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f"]
        }
      ]
    }
  }
}
Approval event payload
{
  "id": "evt_1b3d5f7a9c2e4068b1d3f5a7c9e0b2d4",
  "type": "order.approved",
  "environment": "live",
  "createdAt": "2026-06-05T14:10:00.0000000+00:00",
  "clinicId": "11111111-1111-4111-8111-111111111111",
  "externalClinicId": "clinic-a",
  "organizationId": "22222222-2222-4222-8222-222222222222",
  "data": {
    "order": {
      "orderId": "a4d9f0e2-7b1c-4e3a-8f5d-2c6b9a0e1f3d",
      "sourceOrderId": null,
      "externalOrderId": null,
      "patientId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
      "prescriberUserId": "3f1a8c7d-2b4e-4f6a-9c8d-7e6f5a4b3c2d",
      "status": "Pending",
      "approval": {
        "status": "Approved",
        "submittedForApprovalAt": "2026-06-05T13:55:00.0000000+00:00",
        "reviewedAt": "2026-06-05T14:10:00.0000000+00:00",
        "note": null
      }
    }
  }
}

Webhook signatures

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.

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

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

order.pending_approval

An order was staged inside RxRelay and is awaiting prescriber approval. Nothing has been sent to a pharmacy yet. Fires for app orders, API orders created with submitForApproval: true, and API re-orders using the default approval behavior.

order.approved

A prescriber or clinic admin approved a staged order. Sandbox API orders remain record-only with SandboxReceived status and sandbox webhook environment. Live orders are released for dispatch; order.sent follows the pharmacy hand-off.

order.rejected

A prescriber or clinic admin declined a staged order. It is never sent to a pharmacy and is not billed. Terminal — the order cannot be un-rejected.

webhook.test

A test event sent from Settings / API. Test sends are attempted immediately so you can verify endpoint handling.