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.
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. Review an existing integration
Find concrete blockers to valid, safe order submission.
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. Update to the latest API
Compare your code with recent releases and plan only the required changes.
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. 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
https://api.rxrelay.ai/v1
All endpoints are versioned under /v1.
Keys issued from the app start in sandbox mode — submissions are
recorded and returned for review but are not dispatched to the pharmacy until a platform admin enables live access.
Authentication
API keys are clinic-scoped and managed from the RxRelay app under
Settings → API. Pass the key as a bearer token
(or in the X-API-Key header).
Sandbox keys are prefixed rxrelay_sandbox_;
live keys are prefixed rxrelay_live_.
Authorization: Bearer rxrelay_sandbox_your_key Authorizations
Authorization required 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 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.
{
"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.
{
"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"
} {
"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. Create a sandbox
read_writekey from Settings → API. - 2. Fetch
GET /v1/medicationsand submit representative sandbox orders with required idempotency keys. - 3. Add a webhook endpoint in Settings → API, copy the one-time endpoint secret, and send a test event to verify signature handling.
- 4. RxRelay reviews the recorded sandbox orders and webhook deliveries for the clinic.
- 5. A platform admin enables live access, which creates a separate
rxrelay_live_key. Copy that key from Settings → API and use it for production order submission.
Sandbox keys remain available for test submissions after live access is enabled. Sandbox orders return synchronously after validation and recording. Live orders return after synchronous vendor dispatch, and configured webhooks receive order status events asynchronously after the API response.
Get clinic
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.
/v1/clinic curl https://api.rxrelay.ai/v1/clinic \
-H "Authorization: Bearer rxrelay_sandbox_your_key" {
"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 name externalClinicId null for standalone clinics. address1 address2 city state zip phoneNumber contactEmail isSandboxMode createdAt 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).
/v1/medications 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
curl "https://api.rxrelay.ai/v1/medications?clinicId=11111111-1111-1111-1111-111111111111" \
-H "Authorization: Bearer rxrelay_sandbox_your_org_key" 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
curl "https://api.rxrelay.ai/v1/medications?pharmacyId=Kaduceus:opbrx&clinicId=11111111-1111-1111-1111-111111111111" \
-H "Authorization: Bearer rxrelay_sandbox_your_org_key" [
{
"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.
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 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 externalClinicId. For clinic-scoped keys, omit this field or match the key’s clinic; a different clinic is rejected. externalClinicId Response
[] Show child attributes
id items[].medicationId when creating an order. name strength 2.5 mg/mL), when the pharmacy provides it. form Injectable, Tablet), when available. vialSize 2 mL), when available. Used for injectable vials and other measured packages such as creams; null when the pharmacy does not provide it. pharmacyName pharmacySku requiresClinicalJustification items[].clinicalJustification. Applies to Boothwyn GLP-1s, testosterone, and other flagged products. Pharmacy notes do not satisfy this requirement. requiredQuantity items[].quantity; any other value returns 400 Bad Request. When null, the prescriber may choose the quantity. obsidian Show child attributes
units each, ml, or g. packSize defaultDirections defaultDaysSupply defaultClinicalDifferenceStatement requiresClinicalDifferenceStatement isControlled ingredients strength alone can't describe it. Not required to order.
Show child attributes
name BPC-157. concentration mg units). unit 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.
/v1/pharmacies 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
curl "https://api.rxrelay.ai/v1/pharmacies?clinicId=11111111-1111-1111-1111-111111111111" \
-H "Authorization: Bearer rxrelay_sandbox_your_org_key" [
{
"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
[] Show child attributes
id pharmacyId on List medications. name platform shippingMethods 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.
/v1/prescribers 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
curl "https://api.rxrelay.ai/v1/prescribers?clinicId=11111111-1111-1111-1111-111111111111" \
-H "Authorization: Bearer rxrelay_sandbox_your_org_key" [
{
"userId": "3f1a8c7d-2b4e-4f6a-9c8d-7e6f5a4b3c2d",
"firstName": "Alex",
"lastName": "Reed",
"email": "alex.reed@northsideclinic.com",
"npi": "1234567890",
"licenseState": "TX",
"licenseNumber": "MD-44821"
}
] Response
[] Show child attributes
userId prescriberUserId when creating an order. firstName lastName email npi prescriberNpi when creating an order. licenseState licenseNumber 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.
/v1/users 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"
}' {
"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 required firstName lastName roles required prescriber, clinic_admin, staff, or owner. platform_admin cannot be assigned via the API. npi roles includes prescriber. licenseState licenseNumber phone deaNumber Response
userId email firstName lastName roles npi joinedAt List users
Organization keys must use GET /v1/clinics/CLINIC_UUID/users instead; GET /v1/users returns 400 for an org key.
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.
/v1/users curl https://api.rxrelay.ai/v1/users \
-H "Authorization: Bearer rxrelay_sandbox_your_key" [
{
"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
[] Show child attributes
userId email firstName lastName roles npi joinedAt 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.
/v1/patients 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
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" {
"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 externalClinicId. For clinic-scoped keys, omit this field or match the key’s clinic; a different clinic is rejected. externalClinicId search 1980-01-10). email externalPatientId limit offset Response
patients Show child attributes
patientId externalPatientId firstName lastName dateOfBirth 1980-01-10. sex M, F, or U. phone 3147501409; historical untouched values may still use older formatting. email allergyProfile status is unknown, no_known_allergies, or known; allergies is a string array (empty for unknown/no known allergies). createdAt addresses Patient addresses. total limit offset Get a patient
Fetches a single patient by RxRelay ID. Returns 404 Not Found if the patient does not belong to your clinic.
/v1/patients/{patientId} 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
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" {
"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 required
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.
/v1/patients 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
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
}
}' {
"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"
}
]
} {
"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 firstName lastName dateOfBirth 1980-01-10. Required when creating a new patient. sex M, F, or U. phone email externalPatientId is absent. allergyProfile 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 Show child attributes
address1 required address2 city required state required TX. Full names are rejected. See shipping state errors. zip required isDefault 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.
/v1/patients/{patientId} 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
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"] }
}' {
"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"
}
]
} {
"phone": "5125550199",
"email": "jane.doe@example.com",
"externalPatientId": "patient-456",
"allergyProfile": { "status": "known", "allergies": ["penicillin", "sulfa"] }
} Body
firstName lastName dateOfBirth sex M, F, or U. phone email allergyProfile 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 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.
/v1/orders 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
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
}
]
}' {
"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
} {
"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 required read_write API key. Idempotency-Key required 409 Conflict.
Body
clinicId externalClinicId. For clinic-scoped keys, omit this field or match the key’s clinic; a different clinic is rejected. externalClinicId clinicId. externalOrderId metadata patient required Show child attributes
patientId externalPatientId firstName lastName dateOfBirth 1980-01-10. Required when creating a new patient. sex M, F, or U. phone email externalPatientId is absent. allergyProfile 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 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 required address2 city required state required TX. Full names are rejected. See shipping state errors. zip required shippingAddressId shippingAddressId or shippingAddress. The address must belong to the resolved patient and be active.
prescriberUserId prescriberNpi; the prescriber must belong to the clinic.
prescriberNpi prescriberUserId is omitted. shippingMethod 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 required Show child attributes
medicationId required GET /v1/medications. medicationName quantity required 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 required clinicalJustification 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 notes field and must also total 500 characters or fewer; supplying this field for another pharmacy returns 400 Bad Request. refills required obsidian Show child attributes
dispenseQuantity units: "each"); requires quantity: "1". Independent of catalog pack size. Omit for measured products. Omission preserves legacy pack-count behavior. daysSupply clinicalDifferenceStatement externalPrescriptionId 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 externalOrderId externalOrderId you supplied, if any. patientId prescriber prescriberUserId or prescriberNpi you supplied. Null when the order has no prescriber on record.
Show child attributes
userId userId returned by List prescribers. firstName lastName npi status 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 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 createdAt updatedAt createdAt until the order changes. metadata items Show child attributes
orderItemId medicationId medicationName quantity refills directions externalPrescriptionId clinicalJustification pharmacyNotes obsidian 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 status Queued before dispatch. submissions Show child attributes
submissionId environment Sandbox or Production. status orderItemIds failureReason sourceOrderId approval Show child attributes
status Pending, Approved, or Rejected. submittedForApprovalAt reviewedAt note 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
/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.
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
}' 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.
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
}' 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.
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 required Idempotency-Key required submitForApproval externalOrderId shippingAddressId shippingAddress shippingMethod prescriberUserId prescriberNpi refills metadata stagedByUserId clinicId / externalClinicId 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.
{
"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.
/v1/orders/{orderId} 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
curl https://api.rxrelay.ai/v1/orders/a4d9f0e2-7b1c-4e3a-8f5d-2c6b9a0e1f3d \
-H "Authorization: Bearer rxrelay_sandbox_your_org_key" {
"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 required
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.
/v1/orders/{orderId}/approval 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
}
]
}' 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
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"
}' {
"approve": false,
"approverUserId": "3f1a8c7d-2b4e-4f6a-9c8d-7e6f5a4b3c2d",
"note": "Dose needs review before this goes out."
} Body parameters
approve 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 required 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 note 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 stagedByUserId 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}.
/v1/orders 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
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" {
"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 externalClinicId. externalClinicId status Queued, Pending, Sent, Shipped, Delivered, Failed, Cancelled, SandboxReceived, DemoRecorded, or PendingApproval. patientId externalOrderId from to limit offset Response
orders Show child attributes
orderId externalOrderId patientId prescriber userId, firstName, lastName, and npi. Null when the order has no prescriber on record.
status shippingMethod Standard (two-day) or Overnight. Pharmacy-owned service selection may override the requested value. trackingNumber createdAt updatedAt createdAt until the order changes. metadata approval total limit offset 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.
/v1/patients/{patientId}/orders 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
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" {
"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 required 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.
/v1/patients/{patientId}/addresses 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
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" [
{
"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
/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.
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
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
}' {
"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
/v1/patients/{patientId}/addresses/{addressId} 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
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" {
"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.
/v1/patients/{patientId}/addresses/{addressId} 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
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.
/v1/patients/{patientId}/addresses/{addressId}/default 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
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" {
"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 shippingAddressId when creating an order. patientId address1 address2 city state zip isDefault isActive false. createdAt
Organization-scoped keys must include clinicId
or externalClinicId
as query parameters, matching the medication and prescriber listing endpoints.
Organizations
Start with an organization key
- Use your sandbox organization key as the Bearer token. Call
GET /v1/clinicsand choose an enabled clinic'sid. AnexternalClinicIdworks only if that alias is already configured. - 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. - Submit the normal
POST /v1/orderspayload withclinicIdin its JSON body and anIdempotency-Keyheader. Use IDs from the selected clinic. - 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
| Endpoint | Organization-key behavior |
|---|---|
| GET /v1/clinics | No selector: discover enabled clinics belonging to the key's organization. |
| POST /v1/orders; POST /v1/orders/{orderId}/reorder | clinicId or externalClinicId in the JSON body. |
| GET /v1/medications, /v1/pharmacies, /v1/prescribers | clinicId or externalClinicId in the query string. |
| GET /v1/patients; GET/PATCH /v1/patients/{patientId}; POST /v1/patients | clinicId or externalClinicId in the query string, including writes and single-patient lookup. |
| All /v1/patients/{patientId}/addresses endpoints | clinicId or externalClinicId in the query string for list, get, upsert, delete, and set-default. |
| GET /v1/orders; GET /v1/patients/{patientId}/orders | clinicId or externalClinicId in the query string. Lists cover one clinic, not the whole organization. |
| GET /v1/orders/{orderId}; POST /v1/orders/{orderId}/approval | The order determines its clinic; no selector needed. Approval still requires read_write and an eligible approver. |
| GET /v1/clinics/{clinicRef}/users | RxRelay clinic UUID in the path; externalClinicId aliases are not supported here. |
| POST /v1/organization/invites | The key determines the organization. Requires read_write and invitations enabled; no clinic selector. |
| GET /v1/clinic; GET/POST /v1/users | Clinic-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
| HTTP | Failure | What to check |
|---|---|---|
| 401 | Authentication failed | Send Authorization: Bearer with the organization key. Check that the key is valid, not expired or revoked. An explanatory JSON body is not guaranteed. |
| 400 | Clinic is required | Supply clinicId or externalClinicId in the body for order creation, or in the query for clinic-scoped patient, catalog, and list requests. |
| 400 | Invalid clinicId format | clinicId must be a UUID. Use externalClinicId for your configured string alias. Model-validation error wording may vary. |
| 404 | Clinic not found | Check GET /v1/clinics. The clinic must belong to the key's organization. If both identifiers are supplied, they must identify the same clinic. |
| 403 | Clinic disabled | The selected clinic is disabled; contact RxRelay. Resource-based order lookups and clinic-user discovery instead conceal inaccessible clinics with 404. |
| 404 | Patient, address, or order not found | Confirm 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. |
| 403 | Write forbidden | Use a read_write key for writes. Some scope failures have no explanatory body. Org keys cannot register clinic users even with read_write. |
| 400 | Missing Idempotency-Key | Order creation requires this header after clinic selection succeeds. Keep the same key and identical body when retrying the same order. |
{
"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.
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.
/v1/clinics curl https://api.rxrelay.ai/v1/clinics \
-H "Authorization: Bearer rxrelay_sandbox_your_org_key" [
{
"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.
/v1/organization/invites 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"
}' {
"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 required role required 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
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.
/v1/clinics/{clinicRef}/users [
{
"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.
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"
]
}' {
"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 required environment sandbox, live, or both. events 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
RxRelay-Event-Id: evt_f2a4c6e8d0b14d09a7c1e3f5b9a2c4d6
RxRelay-Delivery-Id: 9b8a7c6d-5e4f-3210-9876-abcdef123456
RxRelay-Timestamp: 1780691400
RxRelay-Signature: t=1780691400,v1=<hmac_sha256> {
"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
}
}
} {
"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"]
}
]
}
}
} {
"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.
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.