What is the Practice API?
The Practice API is for provider practices that have their own licensed prescribers on file. Your doctor writes the prescription in your own EMR; VitaRelay records it as an external-source prescription and handles fulfillment, routing, and shipping. You pay VitaRelay; VitaRelay routes to the pharmacy. This is distinct from the VitaClinic API, where prescribing happens through VitaRelay’s Internal Doctor Network (IDN). The Practice API does not use the IDN — you use your own doctors, and your own licensed provider is the prescriber of record.For
POST /orders, your prescriber must already be on your practice roster (NPI, and DEA for controlled substances); the API resolves the prescriber by NPI against your roster. POST /prescriptions takes the prescriber in the request and verifies it (NPI checksum, DEA checksum, and a license in the patient’s state) before recording it on your roster.How it works
1
Pull the catalog
List the unified catalog (
GET /catalog) covering all product types — R&D, 503B, and 503A. Each product says whether it is a controlled substance (controlled_substance). If your pharmacy gave you a price list, each product also carries your own price_cents; no cost or margin figure is ever shown.2
Include prescriber + prescription lines
For any order containing 503A or 503B products, include your prescriber’s NPI (
prescriber_npi) and one prescriptions line per prescription product. The prescriber is resolved against your roster by NPI.3
Submit the order
POST /orders records an external-source signed prescription for each Rx line and creates the order. VitaRelay fulfills it and charges your card on file. Pass external_order_id to make retries idempotent.4
Track order status
Poll
GET /orders/{orderId} for status, payment, and fulfillment/tracking details.5
Cancel before it ships
POST /orders/{orderId}/cancel cancels an order until a package has shipped. A card order is refunded; an invoiced order
is never invoiced. After shipping it answers 409 already_shipped. You get an order.cancelled webhook.Post a prescription in one call
If your system holds the patient and the prescriber, usePOST /prescriptions. It takes the patient, the
prescriber (NPI, DEA, and a license for each state) and the prescription lines together, creates the patient,
records the prescriber on your roster, and places the order for the pharmacy to fulfil. Nothing is created unless
every check passes: a valid NPI, a valid DEA (required for any controlled substance), and a license in the state
the patient lives in. Send external_patient_id and external_order_id so retries never chart a patient or place
an order twice.
When a DEA number is needed
A prescriber’s DEA number is required only when the prescription includes a controlled substance (Schedule II-V).GET /catalog tells you which products those are: each product has controlled_substance: true or false.
A DEA number you do send is always checked, whether the products are controlled or not: 2 letters and 7 digits that
pass the DEA checksum (
invalid_dea otherwise). With POST /prescriptions send it as prescriber.dea_number; with
POST /orders the DEA comes from the prescriber on your roster (POST /prescribers).
Billing and order updates
How an order is billed follows your account setup: your card on file is charged for each order, or orders are added to an invoice and billed together. Register a webhook URL (your pharmacy sets it up for you and gives you the signing secret) or pollGET /orders/{orderId}.
Events
Every order event carries
order_id, order_number, status, occurred_at (ISO 8601, UTC), patient_id and
items[] (product_id, name, quantity). A declined card is not pushed as an event: it shows in the POST
response (payment_message) and as payment_status on GET /orders/{id}.
- Carrier and link.
tracking_carrieris always lower-case (fedex,ups).tracking_urlis an empty string when there is no link. - Several packages. Usually one
order.shippedper order (a repeat within 10 minutes is suppressed); the event carries the order’s tracking.GET /orders/{id}lists every package inshipments[]. - Status values.
status:awaiting_payment,submitted,processing,ready_to_ship,shipped,delivered,cancelled.payment_status:paid,awaiting_payment,not_required,failed,refunded,cancelled,route_hold. Treat an unknown value gracefully.
Delivery
Each delivery carriesX-Webhook-Event, X-Webhook-Delivery (unique per event; a retry reuses it, so ignore ones you have seen),
X-Webhook-Timestamp and X-Webhook-Signature (sha256= plus the hex HMAC-SHA256 of the exact raw body, keyed with your
signing secret). Reply with any 2xx within 8 seconds. Anything else is retried: 6 attempts in all, waiting about 3, 9, 27 and
81 minutes and then about 4 hours, after which the event is dropped. Events can arrive out of order or twice, so use occurred_at
and the status progression, and catch up with GET /orders/{id} (there is no replay). We send from shared infrastructure with no
fixed IP range: rely on the signature, not an IP allow-list. Your pharmacy can rotate the secret or send a test event from the
portal’s Webhooks panel.
Sandbox
A key that starts withvr_test_ is a sandbox key. Every check runs as it does live, but nothing real is created, shipped or
charged. A sandbox order (order_id starting sandbox_ord_) is remembered so you can build the status and tracking side:
GET /orders/{sandbox order id} returns the same shape as a live order, following that timeline. To test the other cases, send
POST /orders/{sandbox order id}/simulate with { "event": "order.shipped" | "order.delivered" | "order.exception" | "order.cancelled" }
(scope orders:write, sandbox keys only). An exception or a cancel stops the automatic timeline. Sandbox events go to the same
webhook endpoint as live ones, with sandbox: true in the body. In the sandbox order_number is empty (the webhook shows the
order id in its place); live orders have a number like VR-20261102-ab12cd.
Product types
Access & scopes
Test keys run in a full sandbox — no real orders or prescriptions are created and nothing is charged. A sandbox order still moves through paid, shipped and delivered so you can build your status handling (see Sandbox above). Live keys create real prescriptions, real orders, and charge your card.
Safety & compliance
- Tenant isolation — you can only reference your own patients and your own prescriber roster. Anything else returns
404. - Server-derived prescriber identity — name, DEA, and license are always resolved from your roster by NPI, never accepted from the request.
- Controlled substances — the resolved prescriber must have a DEA on file for the patient’s state, or the order is rejected with
controlled_requires_deabefore anything is written.

