Prescription Orchestrator API
7-step prescription approval pipeline coordinating physician registry, pharmacy router, payment, shipping, and notifications.
Overview
The Prescription Orchestrator coordinates the full prescription approval workflow across 5 downstream services. When a prescriber approves (or denies) a prescription via the RxQueue admin portal, the orchestrator runs a deterministic 7-step pipeline that resolves medication config, looks up the patient in Canvas FHIR, selects the correct prescriber, submits to a pharmacy, charges payment, creates a shipment label, and sends patient notifications.
Instead of the RxQueue UI calling each service directly, it makes a single
POST /orchestrator/approve call and the orchestrator handles all
downstream coordination, failure handling, and audit logging.
Base URL
https://api.yourera.com/orchestrator
How It Works
- Prescriber approves a task in the RxQueue admin portal
- Admin portal calls
POST /orchestrator/approvewith taskId, medication, and canvasPatientId - Orchestrator runs the 7-step pipeline (see Pipeline Steps)
- Each step calls a downstream service via HMAC-authenticated HTTP clients
- Every run (success or failure) is persisted to the
orchestration_runstable - The synchronous response includes completed steps, warnings, and any errors
Authentication
All /orchestrator/* endpoints require HMAC-SHA256 authentication. Each API client
is issued an API key (public identifier) and an API secret
(used for signing). Keys are stored in the orchestrator_api_keys table.
Required Headers
| Header | Description |
|---|---|
X-API-Key |
Your public API key identifier |
X-Timestamp |
Current timestamp in ISO 8601 format. Must be within 5 minutes of server time (replay protection). |
X-Signature |
HMAC-SHA256 hex digest of the signing string |
Signature Formula
The signing string is the timestamp, a dot, and the JSON-stringified request body:
HMAC-SHA256(apiSecret, timestamp + '.' + JSON.stringify(body))
The result is encoded as a lowercase hex string.
X-Timestamp header must be within 5 minutes of the server's current time.
Requests with expired timestamps are rejected with 401.
Node.js Example
import crypto from 'node:crypto';
const API_KEY = 'your-api-key';
const API_SECRET = 'your-api-secret';
const BASE_URL = 'https://api.yourera.com/orchestrator';
function signRequest(body) {
const timestamp = new Date().toISOString();
const bodyStr = JSON.stringify(body);
const signature = crypto
.createHmac('sha256', API_SECRET)
.update(timestamp + '.' + bodyStr)
.digest('hex');
return { timestamp, signature };
}
// Approve a prescription
const body = {
taskId: 'task-abc123',
medication: 'semaglutide',
canvasPatientId: 'patient-uuid-here',
dosage: '0.25mg weekly',
};
const { timestamp, signature } = signRequest(body);
const response = await fetch(`${BASE_URL}/approve`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': API_KEY,
'X-Timestamp': timestamp,
'X-Signature': signature,
},
body: JSON.stringify(body),
});
body used for signing must be the exact same string sent as the request body.
Call JSON.stringify() once, use it for both signing and the fetch body.
Approve Prescription
Run the full 7-step approval pipeline. Resolves medication config, fetches the
patient from Canvas FHIR, selects the prescriber, submits to the pharmacy, charges
payment, creates a shipment, and sends notifications. Every run is persisted to
orchestration_runs for audit.
Request Body: ApproveRequest
| Field | Type | Description | |
|---|---|---|---|
taskId |
string |
required | Canvas task ID from the RxQueue. Used to correlate the orchestration run with the original review task. |
medication |
string |
required | Medication key. Must match a key in MEDICATION_CONFIGS: "semaglutide", "tirzepatide", or "nad". |
canvasPatientId |
string |
required | Canvas FHIR Patient resource ID. Used to fetch demographics, address, and contact info. |
dosage |
string |
optional | Dosage override selected by the prescriber (e.g. "0.5mg weekly"). If omitted, the default sig from medication config is used. |
Example Request
{
"taskId": "task-abc123",
"medication": "semaglutide",
"canvasPatientId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"dosage": "0.25mg weekly"
}
Response
Success (200)
{
"success": true,
"result": {
"success": true,
"completedSteps": [
"medication_config",
"patient_details",
"prescriber_resolution",
"pharmacy_submission",
"payment",
"shipment",
"notification"
],
"warnings": [],
"medication": "Semaglutide 5mg/mL",
"patientName": "Jane Smith",
"state": "TX",
"submissionId": "sub-uuid-from-pharmacy-router"
}
}
Success with Warnings (200)
Non-blocking steps (payment, shipment, notification) can fail without failing the pipeline. Failures appear in the warnings array:
{
"success": true,
"result": {
"success": true,
"completedSteps": ["medication_config", "patient_details", "prescriber_resolution", "pharmacy_submission", "payment", "shipment", "notification"],
"warnings": ["payment_failed", "notification_failed"],
"medication": "Semaglutide 5mg/mL",
"patientName": "Jane Smith",
"state": "TX",
"submissionId": "sub-uuid-from-pharmacy-router"
}
}
Pipeline Failure (500)
When a blocking step fails, the pipeline halts and the response includes which step failed:
{
"error": "Pharmacy submission failed",
"failedStep": "pharmacy_submission",
"result": {
"success": false,
"completedSteps": ["medication_config", "patient_details", "prescriber_resolution"],
"failedStep": "pharmacy_submission",
"error": "Pharmacy submission failed",
"warnings": [],
"medication": "Semaglutide 5mg/mL",
"patientName": "Jane Smith",
"state": "TX"
}
}
Error Responses
| Status | Cause | Description |
|---|---|---|
| 400 | Validation | Missing or invalid fields. Response includes Zod details array with per-field issues. |
| 401 | Auth | Missing or invalid HMAC authentication headers, or expired timestamp. |
| 500 | Pipeline failure | A blocking step failed. Response includes failedStep and error message. |
Deny Prescription
Deny a prescription and send a best-effort notification to the patient. The denial
is persisted to orchestration_runs with status "denied".
When canvasPatientId is provided, the orchestrator looks up the patient's
email and phone from the Canvas FHIR API before sending the deny notification (including
the patient's name in the notification variables). If canvasPatientId is not
provided or the FHIR lookup fails, the notification is skipped and a warning is logged.
Request Body: DenyRequest
| Field | Type | Description | |
|---|---|---|---|
taskId |
string |
required | Canvas task ID for the prescription being denied. |
reason |
string |
optional | Human-readable reason for the denial. Included in the patient notification and persisted for audit. |
canvasPatientId |
string |
optional | Canvas FHIR patient ID. When provided, the orchestrator fetches the patient's email, phone, and name from Canvas to send a prescription_denied notification. If omitted or if the lookup fails, the notification is skipped (warning logged). |
Example Request
{
"taskId": "task-def456",
"reason": "BMI does not meet clinical criteria for GLP-1 therapy",
"canvasPatientId": "patient-uuid-abc123"
}
Response
Success (200)
{
"success": true,
"taskId": "task-def456",
"denied": true
}
Error Responses
| Status | Cause | Description |
|---|---|---|
| 400 | Validation | Missing taskId. Response includes Zod details array. |
| 401 | Auth | Missing or invalid HMAC authentication. |
prescription_denied notification via the Notification Service
when canvasPatientId is provided. The patient's email, phone, and name are fetched from
Canvas FHIR and included in the notification variables. If canvasPatientId is omitted
or the FHIR lookup fails, the notification is skipped entirely (a warning is logged). Notification
failure does not affect the deny response -- the deny always succeeds if the request is valid.
Refill Check
Process all due refills. Called by the hisera-refill-cron Lambda via
EventBridge on a daily schedule. Queries the refill_schedules table for
active schedules where nextFillDate <= today, runs the full 7-step
pipeline for each, and updates the schedule on success.
Request Body
No request body required. The endpoint queries due schedules from the database internally.
// Empty body or omit body entirely
{}
Response
Success (200)
{
"processed": 3,
"results": [
{
"scheduleId": "sched-uuid-1",
"canvasPatientId": "patient-uuid-1",
"medication": "semaglutide",
"processed": true
},
{
"scheduleId": "sched-uuid-2",
"canvasPatientId": "patient-uuid-2",
"medication": "tirzepatide",
"processed": true
},
{
"scheduleId": "sched-uuid-3",
"canvasPatientId": "patient-uuid-3",
"medication": "semaglutide",
"processed": false,
"reason": "max_refills_reached"
}
]
}
Refill Decision Reasons
Each schedule is evaluated by shouldProcessRefill(). Skipped schedules include one of these reasons:
| Reason | Description |
|---|---|
cancelled | Schedule has been cancelled by patient or provider |
paused | Schedule is temporarily paused |
completed | All refills have been fulfilled |
max_refills_reached | refillsSent >= totalRefillsAllowed |
not_due | nextFillDate is still in the future |
Error Responses
| Status | Cause | Description |
|---|---|---|
| 401 | Auth | Missing or invalid HMAC authentication. |
| 500 | Database | Failed to query refill_schedules table. |
refillsSent, recalculates nextFillDate, and sets
status = "completed" if all refills have been sent. See Refill Logic.
Get Status
Retrieve all orchestration runs for a given task ID. Returns the full audit trail including completed steps, warnings, and the pipeline result payload. Useful for debugging failed pipelines and confirming successful runs.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
taskId |
string |
The Canvas task ID or refill ID (prefixed with refill- for refill runs) |
Example Request
GET /orchestrator/status/task-abc123
Response
Success (200)
{
"taskId": "task-abc123",
"runs": [
{
"id": "run-uuid-1",
"taskId": "task-abc123",
"medication": "semaglutide",
"canvasPatientId": "patient-uuid",
"status": "completed",
"completedSteps": ["medication_config", "patient_details", "prescriber_resolution", "pharmacy_submission", "payment", "shipment", "notification"],
"failedStep": null,
"error": null,
"warnings": [],
"result": { /* full PipelineResult */ },
"createdAt": "2026-03-20T14:30:00.000Z",
"updatedAt": "2026-03-20T14:30:02.000Z"
}
]
}
Error Responses
| Status | Cause | Description |
|---|---|---|
| 401 | Auth | Missing or invalid HMAC authentication. |
| 404 | Not found | No orchestration runs found for the given taskId. |
| 500 | Database | Failed to query orchestration_runs table. |
Pipeline Steps
The approval pipeline runs 7 steps sequentially. Steps 1–4 are blocking: if any of them fails, the pipeline halts immediately and returns the error. Steps 5–7 are non-blocking: failures are captured as warnings but the pipeline still reports success.
Step 1: Resolve Medication Config
Looks up the medication key ("semaglutide", "tirzepatide", or "nad")
in the MEDICATION_CONFIGS map. Returns the display name, sig (prescriber instructions),
quantity, unit, refill count, Boothwyn SKU, and concentration warning flag.
Blocking: Yes. An unknown medication key halts the pipeline.
Calls: Local config lookup (no network call).
Step 2: Get Patient Details
Fetches the patient record from Canvas FHIR using the canvasPatientId.
Extracts first name, last name, state, email, phone, and full shipping address
(line1, city, state, zip). This data feeds into every subsequent step.
Blocking: Yes. If the Canvas FHIR call fails or the patient is not found, the pipeline halts.
Calls: Canvas FHIR API — GET /Patient/:id
Step 3: Resolve Prescriber
Determines which prescriber to assign based on the patient's state. Ali Nolan FNP-C handles 6 states (AK, CT, HI, LA, NM, NY); Neelima Singh MD is the fallback for all other states. Returns the prescriber's name, suffix, NPI, and Canvas practitioner ID.
Blocking: Yes (always succeeds — every state maps to a prescriber).
Calls: Local routing lookup (no network call). See Prescriber Routing.
Step 4: Submit to Pharmacy
Calls the Integration Service
Pharmacy Router via its HMAC-authenticated
POST /rx/prescriptions/submit endpoint. Passes the patient demographics,
prescriber info, medication config, and task ID. The Pharmacy Router determines the
target pharmacy (GMP/PioneerRx or Boothwyn) based on the patient's state.
Blocking: Yes. If the pharmacy submission fails, the pipeline halts. Payment MUST NOT be charged without a successful pharmacy submission.
Calls: Pharmacy Router — POST /rx/prescriptions/submit
Step 5: Charge Payment
Charges the patient's saved card via Stripe using a PaymentIntent with
off_session = true. The card was saved during intake via a SetupIntent.
The charge amount is determined by the medication and any applied discount codes.
Blocking: No. Payment failure is recorded as a "payment_failed" warning but does not halt the pipeline.
Calls: Payment service (Stripe) — chargePayment(canvasPatientId)
Step 6: Create Shipment
Creates a shipping label via the Shipping Service.
For GMP (PioneerRx) orders, this generates a FedEx label immediately. For Boothwyn orders,
the shipment is created asynchronously: when Boothwyn ships the order, they send a webhook
to the Pharmacy Router, which updates the submission
and sends an HMAC-signed callback to the orchestrator's
POST /api/pharmacy-router/callback endpoint. The callback handler then creates
the shipment record and notifies the patient with tracking information.
Blocking: No. Shipment failure is recorded as a "shipment_failed" warning.
Calls: Shipping Service — createShipment(patient, medication, pharmacy)
Receives (async): Pharmacy Router callback — POST /api/pharmacy-router/callback (when Boothwyn ships)
Step 7: Send Notifications
Dispatches a prescription_approved notification to the patient via email and SMS.
The notification includes the patient name, medication display name, and pharmacy name.
Blocking: No. Notification failure is recorded as a "notification_failed" warning.
Calls: Notification Service — sendNotification(type, recipient, variables)
- Pharmacy sync (step 4) MUST complete successfully before charging payment (step 5). Never charge a patient without a confirmed pharmacy submission.
- Payment failure MUST NOT rollback the pharmacy submission. The prescription is already at the pharmacy.
- Notification failure MUST NOT rollback anything. The patient can still check their portal.
- Steps 5, 6, and 7 run sequentially but are all non-blocking. Each is attempted regardless of the previous non-blocking step's outcome.
Pipeline Failure Modes
| Step | Name | Blocking? | On Failure |
|---|---|---|---|
| 1 | medication_config |
BLOCKING | Pipeline halts. Returns failedStep: "medication_config" and error message. |
| 2 | patient_details |
BLOCKING | Pipeline halts. Returns failedStep: "patient_details" with Canvas FHIR error. |
| 3 | prescriber_resolution |
BLOCKING | Always succeeds (every state has a prescriber). In theory, halts pipeline if lookup throws. |
| 4 | pharmacy_submission |
BLOCKING | Pipeline halts. Returns failedStep: "pharmacy_submission". No payment charged. |
| 5 | payment |
Non-blocking | Warning "payment_failed" added. Pipeline continues. Pharmacy order still active. |
| 6 | shipment |
Non-blocking | Warning "shipment_failed" added. Pipeline continues. Manual label creation needed. |
| 7 | notification |
Non-blocking | Warning "notification_failed" added. Pipeline reports success. Patient not notified. |
orchestration_runs table
with completed steps, failed step, error message, warnings, and the full result payload.
This happens even if the DB insert itself fails (logged to console).
Medication Config
The orchestrator maintains a static medication configuration map. Each entry defines the display name, prescriber instructions (sig), dispensing quantity, Boothwyn SKU, and refill count. The medication key passed in the approve request must match one of these entries exactly.
| Key | Display Name | Quantity | Unit | Sig | Refills | Boothwyn SKU | Conc. Warning |
|---|---|---|---|---|---|---|---|
semaglutide |
Semaglutide 5mg/mL | 2 | mL | inject 10 units (0.25mg) SQ weekly | 3 | 12565 | Yes |
tirzepatide |
Tirzepatide 16.75mg-5mg/mL | 2 | mL | inject 24 units (4mg) SQ weekly | 3 | 12850 | Yes |
nad |
NAD+ 200mg/mL | 5 | mL | inject subcutaneously as directed | 3 | 12832 | No |
MedicationConfig Interface
interface MedicationConfig {
displayName: string;
sig: string;
quantity: string;
unit: string;
refills: number;
boothwynSku?: string;
concentrationWarning?: boolean;
}
Prescriber Routing
The orchestrator routes each prescription to one of two prescribers based on the patient's
state. getPrescriberForState(state) performs this lookup synchronously.
| Prescriber | Suffix | NPI | Canvas Practitioner ID | States |
|---|---|---|---|---|
| Alison Nolan | FNP-C | 1982609764 |
de87795cca7e4c55a816df6938801010 |
AK, CT, HI, LA, NM, NY |
| Neelima Singh | MD | 1164633533 |
474a5e0ac37a4fcd94f0de284874bf1a |
All other states (fallback) |
PrescriberInfo Interface
interface PrescriberInfo {
firstName: string;
lastName: string;
suffix: string;
npi: string;
canvasPractitionerId: string;
}
Refill Logic
Refill scheduling determines when a patient's next prescription fill should be processed. The orchestrator uses a date-based formula with a shipping buffer to ensure medication arrives before the patient runs out.
Next Fill Date Formula
nextFillDate = lastFillDate + daysSupply - SHIPPING_BUFFER_DAYS
SHIPPING_BUFFER_DAYS = 3. For a 30-day supply filled on March 1, the next
fill date would be March 1 + 30 - 3 = March 28, giving 3 days for shipping
before the patient's supply runs out on March 31.
Refill Decision Flow
shouldProcessRefill(schedule, today) evaluates each schedule:
- If status is
"cancelled","paused", or"completed"→ skip - If
refillsSent >= totalRefillsAllowed→ skip (max reached) - If
nextFillDate > today→ skip (not yet due) - Otherwise → process the refill
Schedule Update After Successful Refill
When a refill pipeline completes successfully:
refillsSentis incremented by 1lastFillDateis set to todaynextFillDateis recalculated using the formula above- If
refillsSent >= totalRefillsAllowed, status is set to"completed"
RefillSchedule Interface
interface RefillSchedule {
id: string;
status: 'active' | 'paused' | 'cancelled' | 'completed';
totalRefillsAllowed: number;
refillsSent: number;
daysSupply: number;
lastFillDate: Date;
nextFillDate: Date;
}
refill-{scheduleId}. This distinguishes
them from initial approval runs in the orchestration_runs table and allows
querying by schedule via GET /orchestrator/status/refill-{scheduleId}.
Inter-Service Call Diagram
The orchestrator sits at the center of the prescription workflow, calling 5 downstream services. All inter-service calls use HMAC-SHA256 authentication.
+------------------+
| RxQueue Admin |
| Portal |
+--------+---------+
|
POST /approve
POST /deny
|
+--------v---------+
| Orchestrator |
+--+--+--+--+--+---+
| | | | |
+----------------+ | | | +----------------+
| | | | |
+---------v------+ +---------v--+ | +----------+ +--v-----------+
| Canvas FHIR | | Pharmacy | | | Shipping | | Notification |
| (Patient API) | | Router --+--+ | Service | | Service |
+----------------+ +--+---------+ | +----------+ +--------------+
| ^ |
submit| |callback (async)
v | |
+----------+-+ |
| Boothwyn / | |
| Strive API | |
+-------------+ |
|
+--------v-------+
| Stripe |
| (Payment API) |
+----------------+
Service Endpoints Called
| Step | Service | Endpoint | Auth |
|---|---|---|---|
| 2 | Canvas FHIR | GET /Patient/:canvasPatientId |
Bearer token |
| 4 | Pharmacy Router | POST /rx/prescriptions/submit |
HMAC-SHA256 |
| 4 (async) | Pharmacy Router → Orchestrator | POST /api/pharmacy-router/callback |
HMAC-SHA256 |
| 5 | Stripe | PaymentIntent.create(off_session: true) |
Stripe API key |
| 6 | Shipping Service | POST /shipping/shipments |
HMAC-SHA256 |
| 7 | Notification Service | POST /notifications/send |
HMAC-SHA256 |
Database Schema
The orchestrator uses PostgreSQL (shared RDS instance hisera-db) with
Drizzle ORM. All tables use UUID primary keys and timestamptz columns.
orchestration_runs
Audit log of every pipeline execution (approve, deny, or refill).
| Column | Type | Description |
|---|---|---|
id | uuid PK | Auto-generated |
task_id | varchar(100) | Canvas task ID or refill-{scheduleId} |
medication | varchar(100) | Medication key ("semaglutide", etc.) or "N/A" for denials |
canvas_patient_id | varchar(100) | Canvas FHIR patient ID |
status | varchar(30) | "pending", "completed", "failed", or "denied" |
completed_steps | jsonb | Array of step names that completed |
failed_step | varchar(50) | Name of the step that failed (null on success) |
error | text | Error message (null on success) |
warnings | jsonb | Array of warning strings from non-blocking failures |
result | jsonb | Full PipelineResult payload |
created_at | timestamptz | Run start time |
updated_at | timestamptz | Last update time |
refill_schedules
Tracks recurring prescription refills for each patient.
| Column | Type | Description |
|---|---|---|
id | uuid PK | Auto-generated |
canvas_patient_id | varchar(100) | Canvas FHIR patient ID |
medication | varchar(100) | Medication key |
total_refills_allowed | integer | Max refills authorized |
refills_sent | integer | Refills processed so far (default 0) |
days_supply | integer | Days supply per fill (default 30) |
last_fill_date | timestamptz | Date of the most recent fill |
next_fill_date | timestamptz | Calculated next fill date |
status | varchar(20) | "active", "paused", "cancelled", "completed" |
created_at | timestamptz | Schedule creation time |
updated_at | timestamptz | Last update time |
payment_holds
Tracks Stripe payment state for each patient.
| Column | Type | Description |
|---|---|---|
id | uuid PK | Auto-generated |
canvas_patient_id | varchar(100) | Canvas FHIR patient ID |
stripe_customer_id | varchar(100) | Stripe customer ID |
stripe_payment_intent_id | varchar(100) | Stripe PaymentIntent ID |
amount_cents | integer | Charge amount in cents |
currency | varchar(3) | Currency code (default "usd") |
status | varchar(20) | "pending", "succeeded", "failed" |
created_at | timestamptz | Record creation time |
updated_at | timestamptz | Last update time |
patient_mappings
Maps Canvas patient IDs to pharmacy-specific patient/case IDs.
| Column | Type | Description |
|---|---|---|
canvas_patient_id | varchar(100) PK | Canvas FHIR patient ID |
pioneerrx_patient_id | varchar(100) | PioneerRx patient ID (GMP) |
boothwyn_case_id | varchar(100) | Boothwyn case ID |
created_at | timestamptz | Mapping creation time |
sync_logs
Detailed log of individual API calls to downstream services.
| Column | Type | Description |
|---|---|---|
id | uuid PK | Auto-generated |
sync_type | varchar(50) | Type of sync operation |
canvas_resource_type | varchar(50) | Canvas resource type (e.g. "Patient") |
canvas_resource_id | varchar(100) | Canvas resource ID |
target_method | varchar(100) | Downstream API method called |
response | jsonb | Raw response payload |
success | boolean | Whether the call succeeded |
error_message | text | Error message on failure |
created_at | timestamptz | Log entry time |
medication_configs
Database-backed medication configuration (supplements the in-code MEDICATION_CONFIGS map).
| Column | Type | Description |
|---|---|---|
id | uuid PK | Auto-generated |
medication_type | varchar(50) UNIQUE | Medication key |
drug_name | varchar(255) | Full drug name |
sig | text | Prescriber instructions |
quantity_ml | real | Quantity in mL |
refills | integer | Authorized refills (default 3) |
days_supply | integer | Days supply (default 28) |
indication | text | Clinical indication |
clinical_justification | text | Justification text for pharmacy |
created_at | timestamptz | Record creation time |
updated_at | timestamptz | Last update time |
orchestrator_api_keys
HMAC API keys for authenticating callers.
| Column | Type | Description |
|---|---|---|
id | uuid PK | Auto-generated |
name | varchar(100) | Human-readable key name |
api_key | varchar(100) UNIQUE | Public API key identifier |
api_secret | varchar(100) | Secret used for HMAC signing |
is_active | boolean | Whether the key is active (default true) |
created_at | timestamptz | Key creation time |
Health Check
Returns service health status. No authentication required.
Response (200)
{
"status": "ok",
"service": "prescription-orchestrator",
"timestamp": "2026-03-20T14:30:00.000Z"
}