Skip to main content

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, use POST /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 poll GET /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_carrier is always lower-case (fedex, ups). tracking_url is an empty string when there is no link.
  • Several packages. Usually one order.shipped per order (a repeat within 10 minutes is suppressed); the event carries the order’s tracking. GET /orders/{id} lists every package in shipments[].
  • 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 carries X-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 with vr_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_dea before anything is written.