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

  1. Prescriber approves a task in the RxQueue admin portal
  2. Admin portal calls POST /orchestrator/approve with taskId, medication, and canvasPatientId
  3. Orchestrator runs the 7-step pipeline (see Pipeline Steps)
  4. Each step calls a downstream service via HMAC-authenticated HTTP clients
  5. Every run (success or failure) is persisted to the orchestration_runs table
  6. 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

HeaderDescription
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.

Replay Protection The 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),
});
Important The 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

POST /orchestrator/approve

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

FieldTypeDescription
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

StatusCauseDescription
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

POST /orchestrator/deny

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

FieldTypeDescription
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

StatusCauseDescription
400 Validation Missing taskId. Response includes Zod details array.
401 Auth Missing or invalid HMAC authentication.
Notification is Best-Effort The deny endpoint sends a 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

POST /orchestrator/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:

ReasonDescription
cancelledSchedule has been cancelled by patient or provider
pausedSchedule is temporarily paused
completedAll refills have been fulfilled
max_refills_reachedrefillsSent >= totalRefillsAllowed
not_duenextFillDate is still in the future

Error Responses

StatusCauseDescription
401 Auth Missing or invalid HMAC authentication.
500 Database Failed to query refill_schedules table.
Schedule Update on Success When a refill pipeline succeeds, the orchestrator atomically updates the schedule: increments refillsSent, recalculates nextFillDate, and sets status = "completed" if all refills have been sent. See Refill Logic.

Get Status

GET /orchestrator/status/:taskId

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

ParameterTypeDescription
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

StatusCauseDescription
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 ServicesendNotification(type, recipient, variables)

Step Ordering Invariants
  • 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

StepNameBlocking?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.
Audit Trail Every pipeline run (success or failure) is persisted to the 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.

PrescriberSuffixNPICanvas Practitioner IDStates
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;
}
Both NPIs Are Registered Both Ali Nolan (1982609764) and Neelima Singh (1164633533) are registered with both PioneerRx (GMP) and Boothwyn. The Pharmacy Router accepts either prescriber for any pharmacy.

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:

  1. If status is "cancelled", "paused", or "completed" → skip
  2. If refillsSent >= totalRefillsAllowed → skip (max reached)
  3. If nextFillDate > today → skip (not yet due)
  4. Otherwise → process the refill

Schedule Update After Successful Refill

When a refill pipeline completes successfully:

  1. refillsSent is incremented by 1
  2. lastFillDate is set to today
  3. nextFillDate is recalculated using the formula above
  4. 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 Task IDs Refill pipeline runs use the task ID format 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

StepServiceEndpointAuth
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).

ColumnTypeDescription
iduuid PKAuto-generated
task_idvarchar(100)Canvas task ID or refill-{scheduleId}
medicationvarchar(100)Medication key ("semaglutide", etc.) or "N/A" for denials
canvas_patient_idvarchar(100)Canvas FHIR patient ID
statusvarchar(30)"pending", "completed", "failed", or "denied"
completed_stepsjsonbArray of step names that completed
failed_stepvarchar(50)Name of the step that failed (null on success)
errortextError message (null on success)
warningsjsonbArray of warning strings from non-blocking failures
resultjsonbFull PipelineResult payload
created_attimestamptzRun start time
updated_attimestamptzLast update time

refill_schedules

Tracks recurring prescription refills for each patient.

ColumnTypeDescription
iduuid PKAuto-generated
canvas_patient_idvarchar(100)Canvas FHIR patient ID
medicationvarchar(100)Medication key
total_refills_allowedintegerMax refills authorized
refills_sentintegerRefills processed so far (default 0)
days_supplyintegerDays supply per fill (default 30)
last_fill_datetimestamptzDate of the most recent fill
next_fill_datetimestamptzCalculated next fill date
statusvarchar(20)"active", "paused", "cancelled", "completed"
created_attimestamptzSchedule creation time
updated_attimestamptzLast update time

payment_holds

Tracks Stripe payment state for each patient.

ColumnTypeDescription
iduuid PKAuto-generated
canvas_patient_idvarchar(100)Canvas FHIR patient ID
stripe_customer_idvarchar(100)Stripe customer ID
stripe_payment_intent_idvarchar(100)Stripe PaymentIntent ID
amount_centsintegerCharge amount in cents
currencyvarchar(3)Currency code (default "usd")
statusvarchar(20)"pending", "succeeded", "failed"
created_attimestamptzRecord creation time
updated_attimestamptzLast update time

patient_mappings

Maps Canvas patient IDs to pharmacy-specific patient/case IDs.

ColumnTypeDescription
canvas_patient_idvarchar(100) PKCanvas FHIR patient ID
pioneerrx_patient_idvarchar(100)PioneerRx patient ID (GMP)
boothwyn_case_idvarchar(100)Boothwyn case ID
created_attimestamptzMapping creation time

sync_logs

Detailed log of individual API calls to downstream services.

ColumnTypeDescription
iduuid PKAuto-generated
sync_typevarchar(50)Type of sync operation
canvas_resource_typevarchar(50)Canvas resource type (e.g. "Patient")
canvas_resource_idvarchar(100)Canvas resource ID
target_methodvarchar(100)Downstream API method called
responsejsonbRaw response payload
successbooleanWhether the call succeeded
error_messagetextError message on failure
created_attimestamptzLog entry time

medication_configs

Database-backed medication configuration (supplements the in-code MEDICATION_CONFIGS map).

ColumnTypeDescription
iduuid PKAuto-generated
medication_typevarchar(50) UNIQUEMedication key
drug_namevarchar(255)Full drug name
sigtextPrescriber instructions
quantity_mlrealQuantity in mL
refillsintegerAuthorized refills (default 3)
days_supplyintegerDays supply (default 28)
indicationtextClinical indication
clinical_justificationtextJustification text for pharmacy
created_attimestamptzRecord creation time
updated_attimestamptzLast update time

orchestrator_api_keys

HMAC API keys for authenticating callers.

ColumnTypeDescription
iduuid PKAuto-generated
namevarchar(100)Human-readable key name
api_keyvarchar(100) UNIQUEPublic API key identifier
api_secretvarchar(100)Secret used for HMAC signing
is_activebooleanWhether the key is active (default true)
created_attimestamptzKey creation time

Health Check

GET /health

Returns service health status. No authentication required.

Response (200)

{
  "status": "ok",
  "service": "prescription-orchestrator",
  "timestamp": "2026-03-20T14:30:00.000Z"
}