API reference
A REST API to provision clinics, manage patients, place lab orders, and pull results. Base URL:
https://api.panelroute.com/v1All requests and responses are JSON. A test key (prk_test_…) runs against an isolated sandbox — no real labs, no charges, no billing — and a live key (prk_live_…) is production. See Sandbox to run a full order end-to-end before you go live.
Import the OpenAPI spec to generate a client, or the Postman collection to try every endpoint — set the apiKey variable to your test key to hit the sandbox.
Sandbox (test mode)
Use a test key to build and test your integration against a fully isolated sandbox:
- Test patients and orders are kept completely separate from live data — a test key only ever sees test data, and vice versa.
- Orders placed with a test key are never charged, never billed, and never sent to a real lab.
/v1/usagestays empty in test mode. - You drive the order lifecycle yourself with
POST /v1/orders/{id}/simulate— advance tocollected,resulted, ordeliveredon demand. On delivery you get canned results (with one out-of-range marker) and the sameorder.status_changed/order.results_readywebhooks you'll get in production.
A full test run — create a patient, order, deliver it, pull results:
# 1. Create a sandbox patient (test key → test data)
curl https://api.panelroute.com/v1/patients -H "Authorization: Bearer prk_test_…" \
-H "Content-Type: application/json" \
-d '{ "provider_id":"…","first_name":"Test","last_name":"Patient",
"dob":"1990-01-01","gender":"female","email":"t@example.com",
"phone":"6025551234","address_line1":"123 Main St",
"address_city":"Phoenix","address_state":"AZ","address_zip":"85004" }'
# 2. Place a sandbox order (no charge, no lab)
curl https://api.panelroute.com/v1/orders -H "Authorization: Bearer prk_test_…" \
-H "Content-Type: application/json" \
-d '{ "provider_id":"…","patient_id":"…","panel_id":"…",
"consents":{ "telehealth":true,"hipaa_authorization":true } }'
# → { "order": { "id":"…","status":"processing" } }
# 3. Drive it to delivered (fires your webhooks, attaches results)
curl https://api.panelroute.com/v1/orders/{id}/simulate -H "Authorization: Bearer prk_test_…" \
-H "Content-Type: application/json" -d '{ "to":"delivered" }'
# 4. Pull the (canned) results
curl https://api.panelroute.com/v1/orders/{id}/results -H "Authorization: Bearer prk_test_…"When your integration works end-to-end in the sandbox, swap in your prk_live_… key — the requests are identical.
Authentication
Send your API key (from your dashboard, once your account has API access) as a Bearer token on every request. Keys are secret — use them server-side only.
Authorization: Bearer prk_live_…Errors
Errors return a consistent shape with an HTTP status. type is one of invalid_request (400), unauthorized (401), forbidden (403), not_found (404), conflict (409), api_error (5xx).
{ "error": { "type": "invalid_request", "message": "provider_id is required." } }Idempotency
Send an Idempotency-Key header (a UUID you generate) on POST /v1/orders. A retry with the same key replays the original response instead of placing a second order. Reusing a key with a different body returns 409.
Idempotency-Key: 8ea40aa2-7727-4172-82e8-a58715fa9b6dConsent
Every order requires the patient's agreement to two consents. Present both to the patient in your flow (link to the current text), then attest on the order:
Send "consents": { "telehealth": true, "hipaa_authorization": true } on POST /v1/orders. Missing or false → 400. Each attestation is recorded in an audit log with a timestamp and the document version.
Pagination
List endpoints return up to ?limit= (default 50, max 100) items plus has_more and next_cursor. Pass ?starting_after=<id> to page.
Clinics
Provision and list your clinics. Each clinic is where patients and orders live.
/v1/providersProvision a clinic. Auto-activated. Idempotent on external_clinic_id (your own id for the clinic).
Request
curl https://api.panelroute.com/v1/providers \
-H "Authorization: Bearer prk_live_…" \
-H "Content-Type: application/json" \
-d '{
"org_name": "Downtown Family Clinic",
"external_clinic_id": "clinic_8842",
"contact_email": "ops@downtownclinic.com",
"npi": "1982749531",
"licensed_states": ["AZ"],
"baa_attested": true
}'Response
{
"provider": {
"id": "e3eb2bdf-f6b2-4f8b-b5cd-66a7e09c63a5",
"org_name": "Downtown Family Clinic",
"status": "active",
"external_clinic_id": "clinic_8842"
}
}/v1/providersList your clinics.
Request
curl https://api.panelroute.com/v1/providers -H "Authorization: Bearer prk_live_…"Response
{
"data": [
{ "id": "e3eb2bdf-…", "org_name": "Downtown Family Clinic",
"status": "active", "external_clinic_id": "clinic_8842",
"npi": "1982749531", "licensed_states": ["AZ"] }
]
}/v1/providers/{id}Retrieve one clinic.
Request
curl https://api.panelroute.com/v1/providers/e3eb2bdf-… -H "Authorization: Bearer prk_live_…"Response
{ "provider": { "id": "e3eb2bdf-…", "org_name": "Downtown Family Clinic",
"status": "active", "contact_email": "ops@downtownclinic.com", … } }/v1/providers/{id}Update a clinic — org_name, contact_email, npi, licensed_states. Status and billing are platform-managed.
Request
curl -X PATCH https://api.panelroute.com/v1/providers/e3eb2bdf-… \
-H "Authorization: Bearer prk_live_…" \
-H "Content-Type: application/json" \
-d '{ "contact_email": "newops@downtownclinic.com" }'Response
{ "provider": { "id": "e3eb2bdf-…", "contact_email": "newops@downtownclinic.com", … } }Patients
Patients belong to a clinic. Email, phone, and full address are required so orders are ready to ship.
/v1/patientsCreate a patient under a clinic.
Request
curl https://api.panelroute.com/v1/patients \
-H "Authorization: Bearer prk_live_…" \
-H "Content-Type: application/json" \
-d '{
"provider_id": "e3eb2bdf-…",
"first_name": "Jane", "last_name": "Doe",
"dob": "1990-04-12", "gender": "female",
"email": "jane@example.com", "phone": "6025551234",
"address_line1": "123 Main St", "address_city": "Phoenix",
"address_state": "AZ", "address_zip": "85004"
}'Response
{
"patient": {
"id": "cb9f568d-…", "provider_id": "e3eb2bdf-…",
"first_name": "Jane", "last_name": "Doe",
"dob": "1990-04-12", "gender": "female"
}
}/v1/patientsList patients. Filter by ?provider_id= and search by ?q=.
Request
curl "https://api.panelroute.com/v1/patients?provider_id=e3eb2bdf-…" \
-H "Authorization: Bearer prk_live_…"Response
{ "data": [ { "id": "cb9f568d-…", "first_name": "Jane", … } ],
"has_more": false, "next_cursor": null }/v1/patients/{id}Retrieve one patient.
Request
curl https://api.panelroute.com/v1/patients/cb9f568d-… -H "Authorization: Bearer prk_live_…"Response
{ "patient": { "id": "cb9f568d-…", "first_name": "Jane", … } }/v1/patients/{id}Update contact, address, or name (partial). dob/gender changes go through support. Required fields can’t be blanked — patients must stay order-ready.
Request
curl -X PATCH https://api.panelroute.com/v1/patients/cb9f568d-… \
-H "Authorization: Bearer prk_live_…" \
-H "Content-Type: application/json" \
-d '{ "phone": "6025559999" }'Response
{ "patient": { "id": "cb9f568d-…", "phone": "6025559999", … } }Panels
What a clinic can order — standard panels plus its own custom panels. wholesale_price is what you pay; retail_price is your marked-up price (set the markup via /v1/markup). Prices are per-panel — every order additionally carries a fixed $15.00 Lab Processing Fee.
/v1/panelsList orderable panels: the standard catalog, your shared panels (marked "shared": true), plus a clinic’s own custom panels with ?provider_id=.
Request
curl "https://api.panelroute.com/v1/panels?provider_id=e3eb2bdf-…" \
-H "Authorization: Bearer prk_live_…"Response
{
"data": [
{ "id": "31407505-…", "type": "custom", "name": "TRT Panel (Male)",
"collection_method": "walk_in_test", "lab_name": "Labcorp", "marker_count": 4,
"markers": [
{ "marker_id": 123, "name": "Testosterone, Total", "slug": "testosterone-total" },
{ "marker_id": 124, "name": "Testosterone, Free", "slug": "testosterone-free" },
{ "marker_id": 201, "name": "Estradiol", "slug": "estradiol" },
{ "marker_id": 305, "name": "Sex Hormone Binding Globulin", "slug": "shbg" }
],
"wholesale_price": "54.00", "retail_price": "70.20" }
]
}/v1/panels/{id}Retrieve one panel with its full biomarker list (markers), priced as on the list. Pass ?provider_id= for a clinic’s own custom panel. Archived custom panels resolve too (orderable: false).
Request
curl "https://api.panelroute.com/v1/panels/31407505-…?provider_id=e3eb2bdf-…" \
-H "Authorization: Bearer prk_live_…"Response
{ "panel": { "id": "31407505-…", "type": "custom", "name": "TRT Panel (Male)",
"lab_name": "Labcorp", "marker_count": 4,
"markers": [ { "marker_id": 123, "name": "Testosterone, Total", "slug": "testosterone-total" }, … ],
"orderable": true, "wholesale_price": "54.00", "retail_price": "70.20" } }/v1/biomarkersSearch the orderable biomarker catalog — or omit q and pass lab to page through that lab’s full catalog (page, size ≤ 100; keep going until page equals pages). Each result’s marker_id + provider_id plug straight into POST /v1/panels — the discovery half of a panel-builder UI. provider_id codes are lab-specific, so pass ?lab= (quest | labcorp | bioreference | sonora_quest | ihd) when building; all of a panel’s markers must come from one lab. Every marker carries its own wholesale_price + retail_price, markup included (add ?provider_id= to price as that clinic’s own panel). Marker prices are estimates: a panel is priced as one lab test when you create it, which can differ from the sum of its markers, and every order adds the flat lab_processing_fee once. price_varies: true means the lab may adjust that marker by condition (e.g. patient state).
Request
curl "https://api.panelroute.com/v1/biomarkers?lab=labcorp&page=1&size=100" \
-H "Authorization: Bearer prk_live_…"Response
{ "data": [ { "marker_id": 123, "name": "Testosterone, Total",
"slug": "testosterone-total", "unit": "ng/dL",
"provider_id": "TESTOSTERONE_TOTAL", "lab": "labcorp",
"wholesale_price": "34.82", "retail_price": "45.27", "price_varies": false } ],
"page": 1, "size": 100, "pages": 11, "lab_processing_fee": "15.00" }/v1/coverageHow many patient service centers (walk-in draw sites) each lab has within range of a zip — check before ordering so the patient goes to a lab that is nearby. zip is required (5 digits; convert a city to a zip first); radius is 10 | 20 | 25 | 50 | 100 miles (default 25); lab (quest | labcorp | bioreference | sonora_quest) may repeat, default all. locations_within_radius is null when that lab is not offered for the area at all (e.g. Quest and Labcorp in NY, NJ and RI); 0 means offered but no locations within the radius.
Request
curl "https://api.panelroute.com/v1/coverage?zip=85004&radius=25" \
-H "Authorization: Bearer prk_live_…"Response
{ "zip": "85004", "radius_miles": 25, "data": [
{ "lab": "quest", "lab_name": "Quest Diagnostics",
"locations_within_radius": 36, "available": true },
{ "lab": "labcorp", "lab_name": "LabCorp",
"locations_within_radius": 16, "available": true } ] }/v1/panelsCreate a custom panel. Pass provider_id for a single clinic, or "shared": true for a partner-wide panel every one of your clinics can order — your standard menu. Auto-approves if the lab activates it immediately; otherwise panel.approved fires once orderable (usually within a day). Live keys only.
Request
curl https://api.panelroute.com/v1/panels \
-H "Authorization: Bearer prk_live_…" \
-H "Content-Type: application/json" \
-d '{
"shared": true,
"name": "TRT Panel (Male)",
"collection_method": "walk_in_test",
"markers": [ { "marker_id": 123, "provider_id": "TESTOSTERONE_TOTAL" } ]
}'Response
{ "panel": { "id": "31407505-…", "type": "custom", "shared": true,
"name": "TRT Panel (Male)", "approval_status": "pending", "orderable": false } }/v1/markupYour retail markup config: the default plus any per-panel overrides.
Request
curl https://api.panelroute.com/v1/markup -H "Authorization: Bearer prk_live_…"Response
{ "default_markup_percent": 30,
"overrides": [ { "panel_id": "31407505-…", "markup_percent": 45 } ] }/v1/markupSet the default retail markup and/or per-panel overrides (markup_percent: null removes one). Applies to future orders — wholesale pricing is not writable.
Request
curl -X PUT https://api.panelroute.com/v1/markup \
-H "Authorization: Bearer prk_live_…" \
-H "Content-Type: application/json" \
-d '{ "default_markup_percent": 30,
"overrides": [ { "panel_id": "31407505-…", "markup_percent": 45 } ] }'Response
{ "default_markup_percent": 30,
"overrides": [ { "panel_id": "31407505-…", "markup_percent": 45 } ] }Orders
Place and track lab orders. No card is charged — usage is billed to you monthly at wholesale. Every order includes a fixed $15.00 Lab Processing Fee on top of the panel’s wholesale price.
/v1/ordersPlace an order for a patient. Requires a consents object — the patient must have agreed to the Telehealth Consent and HIPAA Authorization (present both in your UI; links below). Send an Idempotency-Key header so retries never double-order. Use panel_id (standard) or custom_panel_id. NY, NJ and RI: Quest Diagnostics and Labcorp panels can't be ordered for patients with an address in those states — the request 400s before billing; check lab_name on GET /v1/panels.
Request
curl https://api.panelroute.com/v1/orders \
-H "Authorization: Bearer prk_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 8ea40aa2-7727-4172-82e8-a58715fa9b6d" \
-d '{
"provider_id": "e3eb2bdf-…",
"patient_id": "cb9f568d-…",
"panel_id": "31407505-…",
"consents": { "telehealth": true, "hipaa_authorization": true }
}'Response
{
"order": {
"id": "83914c97-…", "status": "processing",
"amount_charged": "54.00", "collection_method": "walk_in_test"
}
}/v1/ordersList orders. Filter by ?provider_id= and ?status=.
Request
curl "https://api.panelroute.com/v1/orders?provider_id=e3eb2bdf-…" \
-H "Authorization: Bearer prk_live_…"Response
{ "data": [ { "id": "83914c97-…", "status": "processing", … } ],
"has_more": false, "next_cursor": null }/v1/orders/{id}Retrieve one order, with patient + panel summary and current status.
Request
curl https://api.panelroute.com/v1/orders/83914c97-… -H "Authorization: Bearer prk_live_…"Response
{
"order": {
"id": "83914c97-…", "status": "processing", "lab_status": "requisition_created",
"amount_charged": "54.00", "result_available": false, "requisition_available": true,
"patient": { "id": "cb9f568d-…", "first_name": "Jane", "last_name": "Doe" },
"panel": { "id": "31407505-…", "type": "custom", "name": "TRT Panel (Male)" }
}
}/v1/orders/{id}/resultsFetch structured results once available. Returns 409 until the lab reports back (watch the order.results_ready webhook).
Request
curl https://api.panelroute.com/v1/orders/83914c97-…/results -H "Authorization: Bearer prk_live_…"Response
{ "order_id": "83914c97-…", "results": { … }, "pdf_url": "https://…" }/v1/orders/{id}/requisition.pdfDownload the requisition — the lab order form the patient takes to the draw site — as a PDF stream. Ready once requisition_available is true on the order (usually within minutes of placement); 409 before that. Share it with the patient: PanelRoute does not contact patients directly. Test orders return a canned sandbox PDF.
Request
curl https://api.panelroute.com/v1/orders/83914c97-…/requisition.pdf \
-H "Authorization: Bearer prk_live_…" -o requisition.pdfResponse
%PDF-1.4 … (binary application/pdf)/v1/orders/{id}/results.pdfDownload the signed lab report as a PDF stream. 409 until results are ready. Test orders return a canned sandbox PDF.
Request
curl https://api.panelroute.com/v1/orders/83914c97-…/results.pdf \
-H "Authorization: Bearer prk_live_…" -o results.pdfResponse
%PDF-1.4 … (binary application/pdf)/v1/orders/{id}/cancelCancel a submitted/processing order. Live orders can self-cancel within 1 hour of placement (after that, contact support); test orders any time. Charges/usage are reversed and order.cancelled fires.
Request
curl -X POST https://api.panelroute.com/v1/orders/83914c97-…/cancel \
-H "Authorization: Bearer prk_live_…"Response
{ "order_id": "83914c97-…", "status": "cancelled", "cleanup": "complete" }/v1/orders/{id}/eventsThe order’s full status timeline — every transition in sequence. Reconcile against this instead of reconstructing state from webhooks.
Request
curl https://api.panelroute.com/v1/orders/83914c97-…/events -H "Authorization: Bearer prk_live_…"Response
{ "order_id": "83914c97-…", "current_status": "delivered",
"events": [
{ "from_status": null, "to_status": "pending_payment", "occurred_at": "…" },
{ "from_status": "pending_payment", "to_status": "processing", "occurred_at": "…" }
] }/v1/orders/{id}/simulateSANDBOX ONLY (test key). Advance a test order through its lifecycle — or rehearse the unhappy paths with to: "failed" / "cancelled". interpretation ("normal" | "abnormal", default abnormal) shapes the canned results. On "delivered", results attach and order.results_ready fires.
Request
curl https://api.panelroute.com/v1/orders/{id}/simulate \
-H "Authorization: Bearer prk_test_…" \
-H "Content-Type: application/json" \
-d '{ "to": "delivered", "interpretation": "normal" }' # or {} to advance one stepResponse
{ "order_id": "…", "status": "delivered", "result_available": true }Usage
Your billing ledger and monthly invoices. total_wholesale is what you owe for the period.
/v1/usageYour usage and total. Scope to a billing period with ?period=YYYY-MM.
Request
curl "https://api.panelroute.com/v1/usage?period=2026-07" -H "Authorization: Bearer prk_live_…"Response
{
"period": "2026-07", "total_wholesale": "324.00", "order_count": 6,
"events": [
{ "order_id": "83914c97-…", "wholesale": "54.00", "retail": "70.20",
"billing_period": "2026-07", "occurred_at": "2026-07-01T17:45:55Z" }
]
}/v1/invoicesMonthly wholesale invoices, newest first. Each period closes on the 1st and fires invoice.finalized.
Request
curl https://api.panelroute.com/v1/invoices -H "Authorization: Bearer prk_live_…"Response
{ "data": [ { "id": "0b2f6c…", "billing_period": "2026-06",
"total_wholesale": "324.00", "order_count": 6, "status": "finalized" } ],
"has_more": false, "next_cursor": null }/v1/invoices/{id}One invoice with its line items (the usage rows it covers).
Request
curl https://api.panelroute.com/v1/invoices/0b2f6c… -H "Authorization: Bearer prk_live_…"Response
{ "invoice": { "id": "0b2f6c…", "billing_period": "2026-06",
"total_wholesale": "324.00", "status": "finalized",
"lines": [ { "order_id": "83914c97-…", "wholesale": "54.00",
"reversed_after_close": false } ] } }Webhook endpoints
Manage webhook endpoints programmatically — same catalog of events as the webhook portal. Omit event_types to subscribe to every event type your key can receive. The URL isn’t contacted at registration — deliveries start when events fire.
/v1/webhooks/endpointsList your configured endpoints.
Request
curl https://api.panelroute.com/v1/webhooks/endpoints -H "Authorization: Bearer prk_live_…"Response
{ "data": [ { "id": "ep_2x…", "url": "https://example.com/webhooks/panelroute",
"event_types": null, "disabled": false } ] }/v1/webhooks/endpointsAdd an HTTPS endpoint. The signing secret is returned once here (also via /{id}/secret) — use it to verify signatures.
Request
curl https://api.panelroute.com/v1/webhooks/endpoints \
-H "Authorization: Bearer prk_live_…" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/webhooks/panelroute",
"event_types": ["order.results_ready", "order.failed"] }'Response
{ "endpoint": { "id": "ep_2x…", "url": "https://example.com/webhooks/panelroute",
"event_types": ["order.results_ready", "order.failed"] },
"secret": "whsec_…" }/v1/webhooks/endpoints/{id}Remove an endpoint.
Request
curl -X DELETE https://api.panelroute.com/v1/webhooks/endpoints/ep_2x… \
-H "Authorization: Bearer prk_live_…"Response
{ "deleted": true }Webhooks
Register HTTPS endpoints with POST /v1/webhooks/endpoints (below) to receive signed events as your orders progress — no need to poll. Verify each delivery with the endpoint's signing secret (standard webhook-id / webhook-timestamp / webhook-signature headers). Event types:
order.created— an order was acceptedorder.status_changed— the order moved to a new statusorder.results_ready— results are available to fetchorder.failed— the order failed at the lab (statuslab_error, charge reversed) or payment failed (payment_failed)order.cancelled— the order was cancelled
{
"id": "msg_…", "type": "order.results_ready", "created_at": "…",
"data": {
"order_id": "83914c97-…", "status": "resulted",
"result_available": true, "provider_id": "e3eb2bdf-…"
}
}