← Developers/RxBridgev1.0.0
RxBridge · REST API

RxBridge API

RxBridge is a real-time e-prescribing bridge between provider (prescriber) organizations and pharmacy organizations. This document describes the public partner API under /api/v1 — the same routes documented at /docs/api. Internal dashboard routes (/api/internal/**) and platform-admin routes (/api/admin/**) are not part of this contract and are not included here. ## Test mode Scripts created with a test-mode API key are test scripts: they are never submitted to the pharmacy integration, generate no billing and send no patient or prescriber emails, and every webhook about them carries `livemode: false`. With a test-mode key, onboarding checks the NPI checksum only (no registry lookup), and it cannot modify a prescriber that has been verified against the NPI registry (409 `live_prescriber_protected`). A test-mode key also cannot update the status or shipment of a live script (403 `live_script_protected`); a live key may act on a test script. ## Error codes Error bodies are `{ error, code?, field? }`. `code` is a stable machine-readable identifier — branch on it, not on the message. | code | HTTP status | Meaning | |---|---|---| | `invalid_json` | 400 | POST /scripts, POST /onboarding/prescriber, PATCH /onboarding/licenses, POST /onboarding/sessions: the request body is not valid JSON (onboarding also requires a JSON object). | | `prescriber_required` | 400 | POST /scripts: the organization has more than one active prescriber and the request named none. Send prescriber_id or prescriber_npi; the response lists active_prescribers. | | `invalid_prescriber_npi` | 400 | POST /scripts: prescriber_npi is not a 10-digit NPI. | | `prescriber_selector_conflict` | 400 | POST /scripts: prescriber_id and prescriber_npi identify different prescribers. | | `prescriber_not_found` | 404 | The prescriber_id / prescriber_npi (POST /scripts) or prescriber_id (PATCH /onboarding/licenses) is not a prescriber in your organization. | | `no_license_for_state` | 422 | POST /scripts: the prescriber has no unexpired license in the patient's state, patientState is not a valid US state or territory code, or patientState is required because the prescriber holds unexpired licenses in several states. | | `no_prescriber` | 403 | POST /scripts: the organization has no prescriber that can sign scripts. | | `prescriber_pending_review` | 403 | POST /scripts: the prescriber was submitted but a platform admin has not approved it yet. | | `prescriber_rejected` | 403 | POST /scripts: the prescriber failed platform review. | | `prescriber_deactivated` | 403 | POST /scripts: the prescriber was approved but has since been deactivated (an admin action, or an expired DEA registration). | | `license_expired` | 403 / 400 | 403 on POST /scripts: all of the prescriber's licenses have expired. 400 on onboarding and license writes: a submitted license expiration_date is in the past. | | `prescriber_npi_unverified` | 403 | POST /scripts with a live key: the prescriber was activated in test mode without NPI registry verification, so it can only sign test scripts. Once its organization has a live key it cannot sign until its NPI is verified. | | `medication_unavailable` | 400 | POST /scripts: medicationId is not an available product at the script's pharmacyOrgId. | | `medication_mismatch` | 400 | POST /scripts: the strength sent does not match the strength of the catalog product named by medicationId. | | `live_prescriber_protected` | 409 | POST /onboarding/prescriber, PATCH /onboarding/licenses: a test-mode API key tried to modify a prescriber that has been verified against the NPI registry. | | `live_script_protected` | 403 | PATCH /scripts/{id}/status, POST /scripts/{id}/shipment: a test-mode API key tried to modify a live script. Test keys can only modify test scripts. | | `invalid_patient_state` | 400 | GET /pharmacies/recommend: patient_state is missing or is not a two-letter US state or territory code. | | `invalid_medication_id` | 400 | GET /pharmacies/recommend: medication_id is not a valid UUID. | | `provider_org_required` | 403 | The onboarding endpoint is only available to provider organizations. | | `full_legal_name_required` | 400 | full_legal_name is missing. | | `mobile_phone_required` | 400 | mobile_phone is missing. | | `invalid_mobile_phone` | 400 | mobile_phone is not a 10-digit US number. | | `professional_email_required` | 400 | professional_email is missing. | | `npi_required` | 400 | npi is missing. | | `invalid_npi` | 400 | npi (body or ?npi= filter) is not exactly 10 digits, or its checksum is invalid. | | `npi_not_found` | 400 | The NPI is not in the NPPES registry (live keys). | | `npi_inactive` | 400 | The NPI is deactivated in the NPPES registry (live keys). | | `npi_not_individual` | 400 | The NPI belongs to an organization (NPI-2); an individual (NPI-1) is required (live keys). | | `npi_lookup_failed` | 502 | The live NPPES registry lookup itself failed; safe to retry. | | `licenses_required` | 400 | licenses must be a non-empty array. | | `invalid_license` | 400 | A licenses[] entry is not an object. | | `license_number_required` | 400 | licenses[].license_number is missing. | | `invalid_license_state` | 400 | licenses[].state is not a 2-letter code for one of the 50 states, DC or PR. | | `invalid_license_type` | 400 | licenses[].license_type is not physician, aprn, pa or pharmd (or an accepted alias). | | `license_expiration_required` | 400 | licenses[].expiration_date is missing. | | `invalid_license_expiration` | 400 | licenses[].expiration_date is not a valid YYYY-MM-DD date. | | `duplicate_license_state` | 400 | Two licenses in the payload have the same state. | | `invalid_date_of_birth` | 400 | date_of_birth is not a valid date, or the prescriber is under 18. | | `invalid_dea_number` | 400 | The DEA number is malformed or fails the checksum / last-name check. | | `invalid_dea_expiration_date` | 400 | The DEA expiration date is not a valid YYYY-MM-DD date. | | `attestations_required` | 400 | The attestations object is missing. | | `attestation_required` | 400 | One of the required attestations is not true. | | `typed_signature_required` | 400 | typed_signature is missing. | | `prescriber_id_required` | 400 | PATCH /onboarding/licenses: prescriber_id is missing. | | `invalid_prescriber_id` | 400 | GET /onboarding/prescriber: the prescriber_id filter is not a UUID. | | `return_url_required` | 400 | POST /onboarding/sessions: return_url is missing. | | `invalid_return_url` | 400 | POST /onboarding/sessions: return_url is not an absolute http(s) URL, or carries credentials, a fragment, or one of the reserved query parameters session_id, state or status. | | `invalid_prefill` | 400 | POST /onboarding/sessions: a prefill value is malformed (wrong type, empty, or over-long) and has no more specific code. The field is named in `field`. | | `prefill_field_not_allowed` | 400 | POST /onboarding/sessions: prefill.attestations or prefill.typed_signature was sent. The prescriber must enter both personally on the hosted page. | | `return_url_not_allowed` | 403 | POST /onboarding/sessions: return_url's origin is not registered for your organization or its partner (an RxBridge admin registers return origins), or it is http / test-only for a live key. | | `invalid_expires_in` | 400 | POST /onboarding/sessions: expires_in is not an integer number of seconds in [300, 604800]. | | `external_ref_required` | 400 | POST /onboarding/sessions: external_ref is missing. | | `invalid_external_ref` | 400 | POST /onboarding/sessions: external_ref is not a non-empty string of at most 200 characters. | | `invalid_state` | 400 | POST /onboarding/sessions: state is not a string of at most 500 characters. | | `session_already_open` | 409 | POST /onboarding/sessions: an open session already exists for this external_ref. The body carries session_id and expires_at; DELETE it or pass replace: true. | | `invalid_session_id` | 400 | GET/DELETE /onboarding/sessions/{id}: the path id is not a UUID. | | `session_not_found` | 404 | GET/DELETE /onboarding/sessions/{id}: no such session for this organization (an unknown id and another organization's id are indistinguishable). | | `session_already_completed` | 409 | DELETE /onboarding/sessions/{id}: the session is already completed and cannot be canceled. | | `mode_mismatch` | 409 | POST /provision: the email's existing organization has API keys in the other mode (live vs test), or test_mode was requested for an organization with no keys that already has live data (a live script or an NPI-verified prescriber), so no key was created. Use a different email for the other mode. | | `org_not_owned_by_partner` | 403 | POST /provision: the email's existing organization was not created by the calling partner (matched by external_source), so no key was created. Only the partner that created an organization can re-provision it; its owner manages keys in Rx-Bridge. | | `source_mismatch` | 403 | POST /provision: the request names a partner (external_source) other than the one the provisioning key belongs to, so nothing was created. | | `partner_key_required` | 403 | POST /provision: external_org_id was sent with the shared provisioning key. It needs a provisioning key issued to your integration only, since the shared key cannot say which partner is calling. | | `partner_not_registered` | 409 | POST /provision: external_org_id was sent but the calling integration is not registered as a partner in Rx-Bridge, so no organization was created. | | `no_active_contract` | 403 | POST /scripts: the provider has no active contract with the pharmacy named in pharmacyOrgId, so nothing was created or sent. Request or accept a contract first. | | `onboarding_failed` | 500 | POST /onboarding/prescriber failed; nothing was saved. | | `update_failed` | 500 | PATCH /onboarding/licenses failed (licenses may be saved when a later DEA write fails; the retry is idempotent). | | `lookup_failed` | 500 | A read of prescribers or licenses failed. |

The RxBridge REST API lets you integrate prescription management into your application. Send scripts, track fulfillment, manage contracts, and receive real-time webhook notifications.

Base URLhttps://rx-bridge.cofabri.com/api/v1

Authentication

All API endpoints require authentication via an API key. Include it in one of these headers:

Authorization: Bearer rxb_your_api_key_here

# or

x-api-key: rxb_your_api_key_here
API keys are shown only once when created. Store them in a secrets manager. Never expose them in client-side code or version control.

API Key Scopes

API keys can be scoped to limit what actions they can perform. An unscoped key (or one with no scopes set) has full access. When creating a key, you can restrict it to specific scopes:

ScopeAllows
scripts:readList and get script details
scripts:writeCreate scripts
pharmacies:readSearch pharmacies
contracts:readList and get contracts
contracts:writeCreate and update contracts
webhooks:manageRegister, update, and delete webhooks
onboarding:manageComplete prescriber onboarding and update licenses
*Full access (all scopes)

If a key with limited scopes attempts a forbidden action, the API returns 403 Insufficient scope.

Sandbox / Test Mode

You can create test API keys for development without affecting production data. Test keys are flagged with isTestKey: true and behave identically to production keys but are visually distinguishable in audit logs and dashboards.

To create a test key, set is_test_key: true when generating a key via the API Keys settings page.

Use test keys during development to verify your integration is working correctly before switching to production keys.

Rate Limits

The API enforces per-organization rate limits using a sliding window. Every response includes rate limit headers so you can monitor your usage: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset.

TierLimitApplies to
Standard100 / minuteGET endpoints (list, search, details)
Write30 / minutePOST/PATCH (create scripts, update status)
Webhook20 / minuteWebhook management endpoints

If you exceed the limit, the API returns 429 Too Many Requests. Use the X-RateLimit-Reset header to know when to retry.

CORS

All /api/v1/* endpoints include CORS headers, allowing browser-based integrations to call the API directly. Preflight OPTIONS requests are handled automatically.

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, x-api-key
Access-Control-Max-Age: 86400
Even though CORS allows any origin, API keys should never be exposed in client-side code. Browser-based integrations should proxy API calls through your own backend server.

Naming Conventions

The API uses two naming conventions depending on the resource:

ResourceRequest fieldsResponse fields
ScriptscamelCase (pharmacyOrgId, patientName)camelCase (medicationName, createdAt)
Contractssnake_case (pharmacy_org_id)snake_case (provider_org_name, created_at)
Webhookssnake_case (is_active)snake_case (created_at, is_active)
Onboardingsnake_case (full_legal_name)snake_case (prescriber_id, is_active)
Pagination—snake_case (total_pages, has_more)

Error Handling

The API uses standard HTTP status codes. Error responses include a JSON body:

{
  "error": "Human-readable error message",
  "details": { "field": ["Validation error"] }
}
StatusMeaning
200Success
201Created
400Bad request / validation error
401Invalid or missing API key
403Forbidden (wrong org type, insufficient scope, expired license)
404Resource not found
429Rate limit exceeded — check X-RateLimit-Reset header
500Server error — retry with backoff
502Upstream lookup failed (e.g. NPI registry unavailable during prescriber onboarding) — safe to retry

Contracts

4 endpoints
post/api/v1/contractsRequest a contract with a pharmacy

Provider organizations only. Creates a pending contract request between the caller's provider org and the given pharmacy org. Requires an organization API key via `Authorization: Bearer <key>` or `x-api-key: <key>`. Callers must be a **provider** organization. Rate limit: 100 requests / 60s per organization (tier: `standard`).

Request body (required)
pharmacy_org_idstringrequired

Organization ID of the pharmacy to request a contract with.

notesstringoptional
Responses
201Contract created.
Contract
400pharmacy_org_id missing.
Error
403Caller is not a provider organization.
Error
500Failed to create contract.
Error
get/api/v1/contractsList contracts for the caller's organization

Returns contracts where the caller's organization is either the provider or the pharmacy party. Requires an organization API key via `Authorization: Bearer <key>` or `x-api-key: <key>`. Rate limit: 100 requests / 60s per organization (tier: `standard`).

Parameters
statusoptional
queryFilter to a single contract status.
Responses
200Array of contracts (not a paginated envelope).
Contract
patch/api/v1/contracts/{id}Accept, reject, or terminate a contract

action determines which lifecycle transition to perform; start_date/end_date/notes are only meaningful for the matching action. Requires an organization API key via `Authorization: Bearer <key>` or `x-api-key: <key>`. Rate limit: 100 requests / 60s per organization (tier: `standard`).

Parameters
idrequired
pathContract ID.
Request body (required)
action"accept" | "reject" | "terminate"required
start_datestringoptional

Used only when action is "accept".

end_datestringoptional

Used only when action is "terminate".

notesstringoptional
Responses
200Updated contract.
Contract
400action missing or not one of accept/reject/terminate.
Error
500Failed to update contract.
Error
get/api/v1/contracts/{id}Get a single contract

Requires an organization API key via `Authorization: Bearer <key>` or `x-api-key: <key>`. Rate limit: 100 requests / 60s per organization (tier: `standard`).

Parameters
idrequired
pathContract ID.
Responses
200The contract.
Contract
404Contract not found (or not visible to the caller's organization).
Error

Organization

1 endpoint
get/api/v1/me/organizationGet the API key's own organization

Returns the organization the calling API key belongs to. Requires an organization API key via `Authorization: Bearer <key>` or `x-api-key: <key>`. Rate limit: 100 requests / 60s per organization (tier: `standard`).

Responses
200The organization.
Organization
404Organization not found.
Error

Medications

1 endpoint
get/api/v1/medicationsList medications available at accessible pharmacies

For pharmacy organizations, returns medications stocked (pharmacy_medications.is_available = true) at their own pharmacy. For provider organizations, returns medications available across every pharmacy they have an active contract with. Results are deduplicated by medication ID; when several pharmacies stock the same product, the ordering limits (quantityOptions, daysSupplyOptions, maxRefills, requiresPriorAuth, shipsToStates, leadTimeDays) come from the first pharmacy returned. Pharmacy-internal fields (LifeFile product id, price, product code) are never exposed. Requires an organization API key via `Authorization: Bearer <key>` or `x-api-key: <key>`. Rate limit: 100 requests / 60s per organization (tier: `standard`).

Parameters
pharmacy_idoptional
queryScope to a single pharmacy's organization ID. Must be a pharmacy the caller has access to, or a 403 is returned.
searchoptional
queryCase-insensitive substring match against medication name or generic name.
pageoptional
queryDefault 1.
limitoptional
queryDefault 25, max 100.
Responses
200Paginated list of medications.
dataMedication[]required
paginationPaginationrequired
403pharmacy_id given but not accessible to the caller.
Error

Onboarding

7 endpoints
patch/api/v1/onboarding/licensesUpdate an existing prescriber's licenses and/or DEA registration

Provider organizations only. Upserts state license rows by (prescriber_id, state) — other states are kept — and upserts the single DEA registration row for the prescriber. Use for renewals or adding a new state license without re-running full onboarding. Every license is validated before anything is written (a bad entry fails the whole request instead of being skipped), and the license upsert is a single atomic statement. Licenses use the same rules as POST /onboarding/prescriber. Requires an organization API key via `Authorization: Bearer <key>` or `x-api-key: <key>`. Callers must be a **provider** organization. Rate limit: 100 requests / 60s per organization (tier: `standard`).

Request body (required)
prescriber_idstringrequired

Must belong to the caller's organization.

licensesobject[]optional

Optional. Each entry is validated; duplicate states are rejected.

deaobjectoptional
dea_numberstringoptional

Validated against the prescriber's last name via validateDEANumber (DEA checksum algorithm).

registration_addressobjectoptional
line1stringoptional
line2stringoptional
citystringoptional
statestringoptional
zipstringoptional
expiration_datestringoptional
schedulesanyoptional

Free-form; an array is stored comma-joined.

Responses
200Update summary.
successbooleanrequired
licenses_updatedintegerrequired
dea_updatedbooleanrequired
400Validation failed; the body carries a stable `code` and the `field`. Codes: invalid_json, prescriber_id_required, licenses_required, invalid_license, license_number_required, invalid_license_state, invalid_license_type, license_expiration_required, invalid_license_expiration, license_expired, duplicate_license_state, invalid_dea_number, invalid_dea_expiration_date.
Error
403Caller is not a provider organization.
Error
404prescriber_id not found in the caller's organization.
Error
409live_prescriber_protected: a test-mode API key tried to modify a prescriber that has been verified against the NPI registry.
Error
500Update failed (code update_failed). Licenses are saved atomically; a DEA write failure is reported after licenses were saved, and a retry is idempotent.
Error
get/api/v1/onboarding/licensesGet a prescriber's licenses and DEA registration

Requires an organization API key via `Authorization: Bearer <key>` or `x-api-key: <key>`. Callers must be a **provider** organization. Rate limit: 100 requests / 60s per organization (tier: `standard`).

Parameters
prescriber_idoptional
queryFilter to one prescriber. Omit to list every prescriber in the caller's organization.
Responses
200Prescribers with their licenses and DEA registrations.
prescribersobject[]required
403Caller is not a provider organization.
Error
500Lookup failed (code lookup_failed).
Error
post/api/v1/onboarding/prescriberOnboard (create or update) a prescriber in one call

Provider organizations only. Upserts a prescriber identified by (organization, npi) with identity, licenses, optional DEA registration, and an attestation, in one atomic request: either everything is saved or the request fails. An organization may hold many prescribers. A new NPI creates an inactive prescriber with review_status "pending" (201); an existing NPI updates that prescriber (200). licenses is the prescriber's complete set: states not listed are removed. On an already-approved prescriber, a substantive change to full_legal_name resets it to pending review and deactivates it until a platform admin re-approves. The comparison ignores case, punctuation, titles and credentials (Dr, MD, DO, NP and similar) and tolerates added name parts such as a middle name or initial, so only dropping or replacing a name part of the stored name triggers re-review. A change in the name the NPI registry holds for the NPI (live keys) is compared the same way. Other field changes never affect activation. New prescribers require platform review before they can be used to create scripts. The NPI must belong to an individual (NPI-1) and is checked against the NPPES registry in live mode (sandbox/test-mode API keys only run the local checksum); a registry name that differs from full_legal_name does not reject the request but flags the prescriber for the reviewer. Test-mode API keys cannot modify a prescriber whose NPI has been verified (409 live_prescriber_protected). Identity verification (Stripe) is not required on this path. Requires an organization API key via `Authorization: Bearer <key>` or `x-api-key: <key>`. Callers must be a **provider** organization. Rate limit: 100 requests / 60s per organization (tier: `standard`).

Request body (required)
full_legal_namestringrequired
date_of_birthstringoptional

Optional, YYYY-MM-DD. If given it must be a valid date and the prescriber at least 18.

mobile_phonestringrequired

A 10-digit US number; formatting characters and a leading country code 1 are ignored.

professional_emailstringrequired
npistringrequired

Exactly 10 digits, no separators. Must be an individual NPI (NPI-1) that passes the NPPES lookup (live keys) or the checksum (test keys).

primary_specialtystringoptional
degreestringoptional
licensesobject[]required

The complete license set (replace semantics). At least one; no duplicate states.

will_prescribe_controlled_substancesbooleanoptionaldefault: false
dea_numberstringoptional

Validated (DEA checksum and last-name letter) only when will_prescribe_controlled_substances is true and dea_number is present.

dea_registration_addressobjectoptional
line1stringoptional
line2stringoptional
citystringoptional
statestringoptional
zipstringoptional
dea_expiration_datestringoptional

YYYY-MM-DD.

dea_schedulesanyoptional

Free-form; an array is stored comma-joined.

attestationsobjectrequired

All four keys must be true.

informationAccuratebooleanrequired
licensedAndInGoodStandingbooleanrequired
willComplyWithLawsbooleanrequired
willPrescribeWithinScopebooleanrequired
typed_signaturestringrequired
Responses
200An existing prescriber (same NPI in this organization) was updated. status shows whether it is active, pending re-review, rejected or deactivated.
prescriber_idstringrequired
onboarding_completedbooleanrequiredconst: true
is_activebooleanrequired

False until a platform admin reviews and activates the prescriber.

status"pending_review" | "active" | "rejected" | "deactivated"required

Derived status: active if is_active; else rejected if review_status is rejected; else deactivated if review_status is approved; else pending_review.

review_status"pending" | "approved" | "rejected"required
messagestringrequired
201A new prescriber was created (inactive, review_status "pending").
prescriber_idstringrequired
onboarding_completedbooleanrequiredconst: true
is_activebooleanrequired

False until a platform admin reviews and activates the prescriber.

status"pending_review" | "active" | "rejected" | "deactivated"required

Derived status: active if is_active; else rejected if review_status is rejected; else deactivated if review_status is approved; else pending_review.

review_status"pending" | "approved" | "rejected"required
messagestringrequired
400Validation failed. The body carries a stable `code` and, where applicable, the `field`. Codes: invalid_json, full_legal_name_required, mobile_phone_required, invalid_mobile_phone, professional_email_required, npi_required, invalid_npi, npi_not_found, npi_inactive, npi_not_individual, licenses_required, invalid_license, license_number_required, invalid_license_state, invalid_license_type, license_expiration_required, invalid_license_expiration, license_expired, duplicate_license_state, invalid_date_of_birth, invalid_dea_number, invalid_dea_expiration_date, attestations_required, attestation_required, typed_signature_required.
Error
403Caller is not a provider organization (code provider_org_required).
Error
409A test-mode API key tried to modify a prescriber whose NPI has been verified (code live_prescriber_protected).
Error
500Onboarding failed and nothing was saved (code onboarding_failed).
Error
502Live NPPES registry lookup itself failed (code npi_lookup_failed); safe to retry. Live keys only.
Error
get/api/v1/onboarding/prescriberGet the organization's prescribers and their onboarding status

Requires an organization API key via `Authorization: Bearer <key>` or `x-api-key: <key>`. Callers must be a **provider** organization. Rate limit: 100 requests / 60s per organization (tier: `standard`).

Parameters
npioptional
queryOnly the prescriber with this NPI (exactly 10 digits).
prescriber_idoptional
queryOnly the prescriber with this id.
Responses
200Every prescriber in the caller's organization (or the filtered subset; an unknown npi or prescriber_id yields an empty list), oldest first.
organization_idstringrequired
prescribersobject[]required
400Malformed filter (codes invalid_npi, invalid_prescriber_id).
Error
403Caller is not a provider organization.
Error
500Lookup failed (code lookup_failed).
Error
post/api/v1/onboarding/sessionsCreate a hosted onboarding session for a prescriber

Provider organizations only. Returns a single-use URL for RxBridge's hosted onboarding page: hand it to the prescriber (RxBridge sends no email). The URL is returned once; treat it as a credential. The prescriber completes identity, NPI and licenses, DEA (if needed), the four attestations and a typed signature on RxBridge; they always enter the attestations and signature themselves, so those cannot be prefilled. `return_url` must match an origin an RxBridge admin registered for your organization or its partner (a partner that provisions organizations through the provisioning API has its origins registered once, and every organization it provisions inherits them); without a registered origin no session can be created. On completion the prescriber is redirected to `return_url` with only `session_id`, `state` and `status`; the authoritative result is the `onboarding_session.completed` webhook and `GET /onboarding/sessions/{id}`. Completion means the prescriber entered pending review, never approval. The session inherits test or live mode from the API key. Prefill values are validated strictly: `null` and empty strings for `npi`, `mobile_phone`, `date_of_birth` and `professional_email` are rejected with `invalid_prefill` (or the field's own code, such as `invalid_npi`); omit a key to skip it. Requires an organization API key via `Authorization: Bearer <key>` or `x-api-key: <key>`. Callers must be a **provider** organization. Rate limit: 30 requests / 60s per organization (tier: `write`).

Request body (required)
return_urlstringrequired

Absolute URL on a registered origin, at most 2000 characters. Must be https, except that http is accepted for loopback origins (localhost, 127.0.0.1, [::1]) registered test-only and only with a test-mode API key. No fragment (not even an empty `#`), credentials, or `session_id` / `state` / `status` query parameters.

external_refstringrequired

Your identifier for this prescriber. At most one open session per external_ref.

statestringoptional

Opaque value echoed back on the redirect.

expires_inintegeroptionaldefault: 1800

Seconds the link stays valid.

replacebooleanoptionaldefault: false

Cancel an open session for the same external_ref and mint a new one.

prefillobjectoptional

Advisory, editable prescriber data. Never verified, never placed in a URL. `attestations` and `typed_signature` are rejected.

full_legal_namestringoptional
professional_emailstringoptional
mobile_phonestringoptional
date_of_birthstringoptional
npistringoptional
degreestringoptional
primary_specialtystringoptional
licensesobject[]optional

May be empty or partial; every entry present is validated in full.

will_prescribe_controlled_substancesbooleanoptional
dea_numberstringoptional
dea_expiration_datestringoptional
dea_registration_addressobjectoptional
dea_schedulesanyoptional

Array or comma-separated string.

Responses
201Session created. `url` is returned once and cannot be recovered.
idstringrequired
urlstringrequired
expires_atstringrequired
400Validation error; see `code` and `field`. Session codes: `invalid_json`, `return_url_required`, `invalid_return_url`, `external_ref_required`, `invalid_external_ref`, `invalid_state`, `invalid_expires_in`, `prefill_field_not_allowed`, `invalid_prefill`; prefill values that fail prescriber validation return that validator's code.
Error
403`provider_org_required` (not a provider organization), or `return_url_not_allowed` (the origin is not registered for your organization or its partner, or it is not allowed for this key).
Error
409`session_already_open`: an open session exists for this external_ref (body has `session_id`, `expires_at`). DELETE it or pass `replace: true`.
Error
500Unexpected server error; the body has only `error`. If a retry returns `session_already_open`, the first attempt created a session whose URL you never received: pass `replace: true`.
Error
get/api/v1/onboarding/sessions/{id}Get a hosted onboarding session

Returns the session state. `status` is evaluated against `expires_at` on every read, so an overdue open session reads `expired`. Once completed, `prescriber_id`, `prescriber_status`, `review_status` and a license summary (no license numbers) are included. Another organization's session id returns 404 like an unknown id. Requires an organization API key via `Authorization: Bearer <key>` or `x-api-key: <key>`. Callers must be a **provider** organization. Rate limit: 100 requests / 60s per organization (tier: `standard`).

Parameters
idrequired
pathSession id.
Responses
200The session.
OnboardingSession
400`invalid_session_id`.
Error
403`provider_org_required`: not a provider organization.
Error
404`session_not_found`.
Error
500Unexpected server error; the body has only `error`. Safe to retry.
Error
delete/api/v1/onboarding/sessions/{id}Cancel a hosted onboarding session

Withdraws an open session (also releases its external_ref). Idempotent on an already-canceled session; an already-expired session is returned as `expired`. A completed session cannot be canceled. Requires an organization API key via `Authorization: Bearer <key>` or `x-api-key: <key>`. Callers must be a **provider** organization. Rate limit: 30 requests / 60s per organization (tier: `write`).

Parameters
idrequired
pathSession id.
Responses
200The session after cancellation (`canceled`), or `expired` if it had already expired.
idstringrequired
status"canceled" | "expired"required
400`invalid_session_id`.
Error
403`provider_org_required`: not a provider organization.
Error
404`session_not_found`.
Error
409`session_already_completed`.
Error
500Unexpected server error; the body has only `error`. Safe to retry: cancelling is idempotent.
Error

Pharmacies

2 endpoints
get/api/v1/pharmaciesSearch pharmacies

Returns every pharmacy on Rx-Bridge (not scoped to the caller's contracts), optionally filtered. Requires an organization API key via `Authorization: Bearer <key>` or `x-api-key: <key>`. Rate limit: 100 requests / 60s per organization (tier: `standard`).

Parameters
cityoptional
queryCase-insensitive substring match.
stateoptional
queryCase-insensitive exact match.
servicesoptional
queryComma-separated list; matches pharmacies offering at least one of the given services.
pageoptional
queryDefault 1.
limitoptional
queryDefault 25, max 100.
Responses
200Paginated list of pharmacies.
dataPharmacy[]required
paginationPaginationrequired
get/api/v1/pharmacies/recommendRecommend a pharmacy for a patient

Provider organizations only. Returns the best pharmacy to route a script to, with a short `reason`, plus up to four `alternatives`. Only active, onboarded pharmacies with an unexpired primary license that serve `patient_state` (states served, or an unexpired pharmacy license or DEA registration there) are considered; when `medication_id` is sent, the pharmacy must also list that medication as available. Ranking: an active contract with your organization first, then more services offered, then name. `id` is the pharmacy organization id, the value `POST /scripts` takes as `pharmacyOrgId`. `recommendation` is null (and `alternatives` empty) when no pharmacy qualifies. Requires an organization API key via `Authorization: Bearer <key>` or `x-api-key: <key>`. Callers must be a **provider** organization. Rate limit: 100 requests / 60s per organization (tier: `standard`).

Parameters
patient_staterequired
queryTwo-letter US state or territory code of the patient (case-insensitive).
medication_idoptional
queryOptional medication ID (from GET /medications). When sent, only pharmacies stocking it are considered.
Responses
200The recommendation and alternatives.
recommendationobjectnullrequired
pharmacyPharmacyRecommendationItemrequired
reasonstringrequired
alternativesPharmacyRecommendationItem[]required
400Error code `invalid_patient_state` (missing or not a two-letter state code) or `invalid_medication_id` (not a UUID).
Error
403Caller is not a provider organization.
Error

Pricing

1 endpoint
post/api/v1/pricing/estimateEstimate provider fee and/or pharmacy commission

Provider organizations get providerFee (and providerFeeBreakdown, if includeBreakdown is true) based on their current-month script volume plus add-ons. Pharmacy organizations get pharmacyCommission when orderValue is given. Fields are populated based on the caller's organization type — a pharmacy org never gets providerFee, a provider org never gets pharmacyCommission. Requires an organization API key via `Authorization: Bearer <key>` or `x-api-key: <key>`. Rate limit: 100 requests / 60s per organization (tier: `standard`).

Request body
addOnsobjectoptional

Provider organizations only. Note: as of this API version every add-on fee is $0 (ADD_ON_FEES in lib/pricing/pharmacy.ts) — pricing is reserved for future use but not yet active.

priorityProcessingbooleanoptional
deliveryTrackingEnabledbooleanoptional
autoRefillRemindersEnabledbooleanoptional
orderValuenumberoptional

Pharmacy organizations only. Required to receive a pharmacyCommission estimate.

includeBreakdownbooleanoptional

Provider organizations only. When true, adds providerFeeBreakdown to the response.

Responses
200Estimate. Any field may be absent depending on organization type and inputs.
providerFeenumberoptional

Volume-tiered base fee plus add-on fees, rounded to 2 decimals. See lib/pricing/pharmacy.ts's VOLUME_TIERS.

providerFeeBreakdownobjectoptional

Only present when includeBreakdown is true.

baseFeenumberoptional
addOnFeenumberoptional
totalFeenumberoptional
pharmacyCommissionnumberoptional

6% of orderValue, clamped to [$1.00, $20.00], rounded to 2 decimals.

Scripts

7 endpoints
post/api/v1/scriptsCreate a script

Provider organizations only. Requires an active, onboarded prescriber: with exactly one it is used automatically; with several, name one with prescriber_id or prescriber_npi (otherwise 400 prescriber_required). The prescriber must hold an unexpired license in the patient's state (patientState); that license is stored on the script and sent on the pharmacy order (422 no_license_for_state). If patientState is omitted the prescriber's only unexpired license is used, and a prescriber licensed in several states must send patientState. Validates NCPDP rules (Schedule II refills/quantity, NDC format, DAW code, ICD-10 diagnosis codes) and that the pharmacy and prescriber licenses are current. On success, asynchronously submits to LifeFile when eligible and fires script.created webhooks (and script.approval_requested, if requiresPrescriberApproval was set) to the provider org and, if set, the pharmacy org. Scripts created with a test-mode API key are never submitted to the pharmacy integration and their webhooks carry livemode: false. Requires an organization API key via `Authorization: Bearer <key>` or `x-api-key: <key>`. Callers must be a **provider** organization. Rate limit: 30 requests / 60s per organization (tier: `write`).

Request body (required)
pharmacyOrgIdstringrequired
patientNamestringrequired
patientPhonestringoptional
patientEmailstringoptional
patientDateOfBirthstringoptional

ISO date. Not required to create a script, but LifeFile order submission requires it — see lib/lifefile/order-mapping.ts.

patientAddressLine1stringoptional
patientAddressLine2stringoptional
patientCitystringoptional
patientStatestringoptional
patientZipstringoptional
patientGenderstringoptional
patientAllergiesstring[]optional
insuranceProviderstringoptional
insurancePolicyIdstringoptional
insuranceGroupNumberstringoptional
insuranceRxBinstringoptional
insuranceRxPCNstringoptional
insuranceCardholderNamestringoptional
insuranceRelationshipstringoptional
medicationNamestringrequired
medicationIdstringoptional

Optional catalog product id (`id` from GET /v1/medications). Must be an available product at the script's pharmacy, else 400. When set, the pharmacy's LifeFile product id is sent with the order.

ndcstringoptional

10 digits (4-4-2) or 11 digits (5-4-2); hyphens optional.

strengthstringoptional
quantitystringoptional
directionsstringoptional
refillsintegeroptionaldefault: 0

Must be 0 for Schedule II (controlledSubstanceSchedule "2"/"II") — NCPDP rule.

prescriptionDatestringoptional
substitutionAllowedbooleanoptionaldefault: true
dawCodenumberoptional

0-9.

priorAuthRequiredbooleanoptionaldefault: false
priorAuthNumberstringoptional
controlledSubstanceSchedulestringoptional
quantityWrittenstringoptional

Required (positive number) for Schedule II — NCPDP rule.

daysSupplynumberoptional
diagnosisCodesstring[]optional

ICD-10-CM codes, e.g. "A00.0".

specialInstructionsstringoptional
pharmacyNotesstringoptional
priorityProcessingbooleanoptionaldefault: false
deliveryTrackingEnabledbooleanoptionaldefault: false
autoRefillRemindersEnabledbooleanoptionaldefault: false
externalSystemstringoptional
externalSystemScriptIdstringoptional
requiresPrescriberApprovalbooleanoptionaldefault: false

Set true when the submitter is not the assigned prescriber, to route the script through prescriber sign-off before pharmacy fulfillment.

submittedByNamestringoptional

Display/audit-only.

submittedByExternalIdstringoptional

Caller's own opaque submitter id. Never resolved to an Rx-Bridge user.

clinicalNotestringoptional

Optional free-text clinical context from the submitter, intended for the approving prescriber. Trimmed; an empty value is stored as null; more than 2000 characters returns 400 with details.clinicalNote. Returned as `clinicalNote` on the script, to the provider organization's keys only (always null for the pharmacy organization).

prescriber_idstringoptional

Which of the organization's prescribers signs this script. Optional when the organization has exactly one active prescriber; required (this or prescriber_npi) when it has several. Send at most one of prescriber_id / prescriber_npi (both is allowed only if they name the same prescriber).

prescriber_npistringoptional

10-digit NPI of the prescriber that signs this script. Alternative to prescriber_id.

Responses
201Script created, status "pending".
Script
400Zod validation failure, an NCPDP rule violation (Schedule II refills/quantity, NDC format, DAW code, ICD-10 format), or a prescriber-selection error with a `code`: invalid_json, prescriber_required (several active prescribers and none named — the body lists active_prescribers), invalid_prescriber_npi, prescriber_selector_conflict.
Error
403Caller is not a provider org, or the prescriber/pharmacy validation failed. A 403 with "code": no_active_contract means the provider has no active contract with the named pharmacy, so nothing was created or sent. A prescriber-caused 403 may carry a "code": no_prescriber (the organization has no prescriber), prescriber_pending_review (a prescriber exists but awaits platform review; also returned for an active prescriber whose onboarding is incomplete), prescriber_rejected (the review was rejected), prescriber_deactivated (previously approved, now deactivated), license_expired (every state license of the prescriber has expired), or prescriber_npi_unverified (live key only: the prescriber was activated in test mode without NPI registry verification and can sign only test scripts). When several prescribers exist and none is usable, the reason follows the priority prescriber_pending_review, then prescriber_deactivated, then prescriber_rejected. Other 403 reasons (no state license on file, a missing or expired DEA registration for a controlled-substance script, an expired pharmacy license, and so on) carry only "error", so treat "code" as optional and fall back to the "error" text.
errorstringrequired
code"no_prescriber" | "prescriber_pending_review" | "prescriber_rejected" | "prescriber_deactivated" | "license_expired" | "prescriber_npi_unverified"optional
404prescriber_not_found: prescriber_id / prescriber_npi does not match a prescriber in the caller's organization.
Error
422no_license_for_state: the selected prescriber has no unexpired license in the patient's state (or patientState is required because the prescriber is licensed in several states).
Error
get/api/v1/scriptsList scripts

Provider organizations see scripts they created; pharmacy organizations see scripts routed to them. Requires an organization API key via `Authorization: Bearer <key>` or `x-api-key: <key>`. Rate limit: 100 requests / 60s per organization (tier: `standard`).

Parameters
statusoptional
queryFilter to a single script status.
start_dateoptional
queryISO date/datetime. Filters createdAt >= start_date.
end_dateoptional
queryISO date/datetime. Filters createdAt <= end_date.
searchoptional
queryCase-insensitive substring match against patient name or medication name.
pageoptional
queryDefault 1.
limitoptional
queryDefault 25, max 100.
Responses
200Paginated list of scripts, newest first.
dataScript[]required
paginationPaginationrequired
get/api/v1/scripts/{id}Get a single script

Caller's organization must be either the providerOrgId or pharmacyOrgId on the script. Requires an organization API key via `Authorization: Bearer <key>` or `x-api-key: <key>`. Rate limit: 100 requests / 60s per organization (tier: `standard`).

Parameters
idrequired
pathScript ID.
Responses
200The script.
Script
404Script not found, or not a script of the caller's organization. A test-mode key also gets 404 for live scripts.
Error
get/api/v1/scripts/{id}/eventsList a script's timeline events

Chronological history of a script, oldest first: submission, prescriber approval request, approval or rejection, status changes and shipment updates. Each event carries a `source` (`medoura` or `praxis` when the action came through that partner's API key, `pharmacy` when the routed pharmacy acted, otherwise `rxbridge`). `actorName` is only set for the submitter (submission, approval request) and the assigned prescriber (approval, rejection); it is null for pharmacy activity. Caller's organization must be either the providerOrgId or pharmacyOrgId on the script. Requires an organization API key via `Authorization: Bearer <key>` or `x-api-key: <key>`. Rate limit: 100 requests / 60s per organization (tier: `standard`).

Parameters
idrequired
pathScript ID.
Responses
200The events, oldest first.
eventsScriptEvent[]required
404Script not found, including a malformed id or a script of another organization. A test-mode key also gets 404 for live scripts.
Error
get/api/v1/scripts/{id}/shipmentGet shipment/tracking info for a script

Caller's organization must be either the providerOrgId or pharmacyOrgId on the script. Requires an organization API key via `Authorization: Bearer <key>` or `x-api-key: <key>`. Rate limit: 100 requests / 60s per organization (tier: `standard`).

Parameters
idrequired
pathScript ID.
Responses
200Shipment info.
carrierstringrequired
trackingNumberstringrequired
shippingStatusstringrequired
404Script not found, or not a script of the caller's organization. A test-mode key also gets 404 for live scripts.
Error
post/api/v1/scripts/{id}/shipmentUpdate shipment/tracking info for a script

Caller's organization must be either the providerOrgId or pharmacyOrgId on the script, same as GET. Any given field replaces the existing value; omitted fields (or empty strings) keep the script's current value. Fires script.shipment_updated webhooks to the provider and pharmacy orgs on the script. Requires an organization API key via `Authorization: Bearer <key>` or `x-api-key: <key>`. Rate limit: 100 requests / 60s per organization (tier: `standard`).

Parameters
idrequired
pathScript ID.
Request body (required)
carrierstringoptional
trackingNumberstringoptional
shippingStatusstringoptional
Responses
200Updated.
successbooleanrequiredconst: true
403A test-mode API key was used on a live script (code live_script_protected; nothing is changed).
Error
404Script not found, or not a script of the caller's organization. A test-mode key also gets 404 for live scripts.
Error
patch/api/v1/scripts/{id}/statusUpdate a script's fulfillment status

Pharmacy organizations only, and only for scripts routed to their own organization. Transitions must be legal per lib/workflow/script-status.ts's ALLOWED_TRANSITIONS (e.g. a script can't jump straight from "pending" to "completed"). Two controlled-substance-only compliance gates apply on the transition that first reaches the target status (not on same-status "re-save" calls): moving into "verified" requires a diversion red-flag review (redFlagReviewConfirmed + reviewedByUserId) when controlledSubstanceSchedule is set; moving into "completed" requires photo-ID verification (idVerificationType + idVerifiedByUserId, optionally idVerificationLast4) when controlledSubstanceSchedule is set. reviewedByUserId/idVerifiedByUserId must be a pharmacist or admin member of the caller's organization. Fires script.status_changed webhooks to the caller's org and the script's provider org. Requires an organization API key via `Authorization: Bearer <key>` or `x-api-key: <key>`. Callers must be a **pharmacy** organization. Rate limit: 100 requests / 60s per organization (tier: `standard`).

Parameters
idrequired
pathScript ID.
Request body (required)
status"pending" | "processing" | "on_hold_pending_pa" | "verified" | "ready" | "completed" | "cancelled" | "rejected"required

Target status. Must be a legal transition from the script's current status.

orderValuenumberoptional

Persisted as the script's orderValue on every call that includes it (used for pharmacy commission once the script reaches "completed").

redFlagReviewConfirmedbooleanoptional

Required with reviewedByUserId when status is "verified" and the script is a controlled substance, on the transition into "verified".

reviewedByUserIdstringoptional

Must be a pharmacist or admin member of the caller's organization.

reviewNotesstringoptional
idVerificationType"drivers_license" | "state_id" | "passport" | "military_id" | "other"optional

Required (with idVerifiedByUserId) when status is "completed" and the script is a controlled substance, on the transition into "completed".

idVerificationLast4stringoptional

Last 4 characters only — never the full ID number.

idVerifiedByUserIdstringoptional

Must be a pharmacist or admin member of the caller's organization.

Responses
200Updated.
successbooleanrequiredconst: true
400Invalid status value, illegal status transition, or a controlled-substance compliance gate (red-flag review / ID verification) not satisfied.
Error
403Caller is not a pharmacy organization, or a test-mode API key was used on a live script (code live_script_protected; nothing is changed).
Error
404Script not found, or not a script of the caller's organization. A test-mode key also gets 404 for live scripts.
Error

Webhooks

5 endpoints
post/api/v1/webhooksRegister a webhook endpoint

Generates an HMAC-SHA256 signing secret ("whsec_..."), returned only in this response — store it to verify the x-rxbridge-signature header on delivered payloads. Requires an organization API key via `Authorization: Bearer <key>` or `x-api-key: <key>`. Rate limit: 100 requests / 60s per organization (tier: `standard`).

Request body (required)
urlstringrequired
events"script.created" | "script.status_changed" | "script.shipment_updated" | "script.approval_requested" | "script.approved" | "script.rejected" | "contract.created" | "contract.accepted" | "contract.rejected" | "contract.terminated" | "prescriber.onboarded" | "prescriber.activated" | "prescriber.rejected" | "prescriber.deactivated" | "onboarding_session.completed" | "*"[]required

Non-empty. "*" subscribes to every event. contract.created, contract.accepted, contract.rejected and contract.terminated are reserved: they can be subscribed to but are not emitted yet. prescriber.* events carry data { prescriber_id, npi, status, reason?, livemode } — reason is present only on prescriber.rejected and prescriber.deactivated. onboarding_session.completed (a prescriber finished a hosted onboarding session) carries data { session_id, external_ref, prescriber_id, npi, status, livemode }; status is the prescriber's status at completion (pending_review, never approval).

descriptionstringoptional
Responses
201Webhook registered.
WebhookCreated
400url missing, not https, embedded credentials, or resolving to a private or internal address; or events missing/empty/containing an unrecognized event name.
Error
get/api/v1/webhooksList webhook endpoints

Signing secrets are never included in list responses — only in the POST /webhooks response, at creation time. Requires an organization API key via `Authorization: Bearer <key>` or `x-api-key: <key>`. Rate limit: 100 requests / 60s per organization (tier: `standard`).

Responses
200Array of webhooks (not a paginated envelope).
Webhook
get/api/v1/webhooks/{id}Get a single webhook endpoint

Requires an organization API key via `Authorization: Bearer <key>` or `x-api-key: <key>`. Rate limit: 100 requests / 60s per organization (tier: `standard`).

Parameters
idrequired
pathWebhook endpoint ID.
Responses
200The webhook (no secret).
Webhook
404Webhook not found (or not owned by the caller's organization).
Error
patch/api/v1/webhooks/{id}Update a webhook endpoint

Every field is optional; only fields present in the body are changed. The signing secret cannot be rotated via this endpoint. Requires an organization API key via `Authorization: Bearer <key>` or `x-api-key: <key>`. Rate limit: 100 requests / 60s per organization (tier: `standard`).

Parameters
idrequired
pathWebhook endpoint ID.
Request body (required)
urlstringoptional
events"script.created" | "script.status_changed" | "script.shipment_updated" | "script.approval_requested" | "script.approved" | "script.rejected" | "contract.created" | "contract.accepted" | "contract.rejected" | "contract.terminated" | "prescriber.onboarded" | "prescriber.activated" | "prescriber.rejected" | "prescriber.deactivated" | "onboarding_session.completed" | "*"[]optional

Non-empty if given.

descriptionstringoptional
is_activebooleanoptional
Responses
200Updated webhook.
Webhook
400url invalid, or events empty/containing an unrecognized event name.
Error
404Webhook not found (or not owned by the caller's organization).
Error
delete/api/v1/webhooks/{id}Delete a webhook endpoint

Requires an organization API key via `Authorization: Bearer <key>` or `x-api-key: <key>`. Rate limit: 100 requests / 60s per organization (tier: `standard`).

Parameters
idrequired
pathWebhook endpoint ID.
Responses
200Deleted.
successbooleanrequiredconst: true
404Webhook not found (or not owned by the caller's organization).
Error