Pharmacy Router API

Centralized pharmacy submission API for routing prescriptions to the correct 503B compounding pharmacy.

Overview

The Pharmacy Router is a centralized API that accepts prescription submissions, determines the correct compounding pharmacy based on the patient's state, translates the order into the pharmacy's native format, and tracks the order through fulfillment.

Instead of each service integrating directly with multiple pharmacies, callers submit a single standardized payload and let the router handle pharmacy selection, format translation, submission, and status tracking.

Base URL

https://api.yourera.com/rx

How It Works

  1. Caller submits a PrescriptionSubmission to POST /rx/prescriptions/submit
  2. Router resolves the target pharmacy based on patient state and routing config
  3. Router translates the payload into the pharmacy's native format and submits
  4. Synchronous response includes submissionId, pharmacy, and status
  5. Router sends an HMAC-signed callback to the caller's callbackUrl
  6. When pharmacy webhooks arrive (shipping updates, status changes), router updates the submission and sends another callback

Authentication

All /rx/prescriptions/* endpoints require HMAC-SHA256 authentication. Each API client is issued an API key (public identifier) and an API secret (used for signing).

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';

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 };
}

// Usage
const body = { /* PrescriptionSubmission */ };
const { timestamp, signature } = signRequest(body);

const response = await fetch('https://api.yourera.com/rx/prescriptions/submit', {
  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.

Submit Prescription

POST /rx/prescriptions/submit

Submit a prescription for pharmacy routing. The router resolves the target pharmacy, translates the payload, submits the order, and returns the result synchronously. An HMAC-signed callback is also sent asynchronously to the provided callbackUrl.

Request Headers

HeaderValue
Content-Typeapplication/json
X-API-KeyYour API key
X-TimestampISO 8601 timestamp
X-SignatureHMAC-SHA256 hex signature

Request Body: PrescriptionSubmission

Top-level Fields

FieldTypeDescription
source string required Identifier for the calling system (e.g. "guide-glp", "yourera-orchestrator")
sourceOrderId string required The caller's unique order/reference ID. Used for deduplication (unique per source).
callbackUrl string required URL to receive HMAC-signed status callbacks. Must be a valid URL.
patient object required Patient demographics. See patient object.
shipTo object required Shipping address. See shipTo object.
prescriber object required Prescribing clinician details. See prescriber object.
medication object required Prescription details. See medication object.
routing object optional Routing overrides. See routing object.
test boolean optional When true, submit to pharmacy sandbox/test environments. No real prescriptions are created. See Test Mode.
clinical object optional Clinical data (allergies, conditions, medications). See clinical object.

patient Object

FieldTypeDescription
firstName string required Patient's first name
lastName string required Patient's last name
dob string required Date of birth (e.g. "1990-01-15")
gender "male" | "female" required Patient gender
phone string required Patient phone number
email string optional Patient email address

shipTo Object

FieldTypeDescription
firstName string required Recipient first name
lastName string required Recipient last name
phone string required Recipient phone number
addressLine1 string required Street address line 1
addressLine2 string optional Street address line 2 (apt, suite, etc.)
city string required City
state string required Two-letter state code (e.g. "TX")
zip string required ZIP code (min 5 characters)

prescriber Object

FieldTypeDescription
firstName string required Prescriber first name
lastName string required Prescriber last name
npi string required NPI number (10 digits)
deaNumber string optional DEA registration number
licenseNumber string optional State license number
licenseState string optional State of licensure
phone string optional Prescriber phone
fax string optional Prescriber fax number
email string optional Prescriber email
address object optional Prescriber address: { line1, city, state, zip }
signatureBase64 string optional Base64-encoded prescriber signature image. Used by Strive as signed_prescription_image.

medication Object

FieldTypeDescription
name string required Medication name (e.g. "Semaglutide 2.5mg/mL"). Used for SKU/catalog mapping.
sig string required Prescriber instructions (sig). E.g. "Inject 0.25mL subcutaneously once weekly"
quantity number required Quantity to dispense (must be positive)
daysSupply number required Days supply (must be positive)
refills number required Number of refills authorized (0 or more)
clinicalJustification string optional Clinical justification for the prescription
note string optional Additional notes for the pharmacy

routing Object

FieldTypeDescription
patientState string required Two-letter state code for routing. If omitted, the router falls back to shipTo.state.
preferredPharmacy string optional Force a specific pharmacy (e.g. "boothwyn", "strive"). Bypasses state-based routing.

clinical Object

FieldTypeDescription
allergies object optional { known: boolean, entries?: string[] } - Patient allergies
conditions object optional { known: boolean, entries?: string[] } - Medical conditions
medications object optional { known: boolean, entries?: string[] } - Current medications
billTo "patient" | "practice" optional Who to bill. Defaults to "practice" for Strive orders.

Example Request

{
  "source": "guide-glp",
  "sourceOrderId": "ord_abc123",
  "callbackUrl": "https://api.example.com/webhooks/pharmacy-router",
  "patient": {
    "firstName": "Jane",
    "lastName": "Smith",
    "dob": "1990-03-15",
    "gender": "female",
    "phone": "(555) 123-4567",
    "email": "jane.smith@example.com"
  },
  "shipTo": {
    "firstName": "Jane",
    "lastName": "Smith",
    "phone": "(555) 123-4567",
    "addressLine1": "123 Main St",
    "addressLine2": "Apt 4B",
    "city": "Austin",
    "state": "TX",
    "zip": "78701"
  },
  "prescriber": {
    "firstName": "Ali",
    "lastName": "Nolan",
    "npi": "1982609764",
    "phone": "(555) 987-6543",
    "email": "dr.nolan@example.com",
    "address": {
      "line1": "456 Medical Pkwy",
      "city": "Dallas",
      "state": "TX",
      "zip": "75201"
    }
  },
  "medication": {
    "name": "Semaglutide 2.5mg/mL",
    "sig": "Inject 0.25mL subcutaneously once weekly for 4 weeks",
    "quantity": 1,
    "daysSupply": 28,
    "refills": 0,
    "clinicalJustification": "BMI 32, type 2 diabetes"
  },
  "routing": {
    "patientState": "TX"
  },
  "test": false
}

Response

Success (201)

{
  "submissionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "pharmacy": "strive",
  "status": "submitted",
  "pharmacyOrderId": "STV-ORD-12345"
}

Validation Error (400)

{
  "error": "Validation failed",
  "details": {
    "fieldErrors": {
      "patient.dob": ["Required"]
    },
    "formErrors": []
  }
}

Authentication Error (401)

{
  "error": "Invalid signature"
}

No Route (422)

{
  "error": "No pharmacy route configured for state: MN"
}

Pharmacy Error (502)

{
  "submissionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "pharmacy": "boothwyn",
  "status": "failed",
  "pharmacyOrderId": null,
  "error": "Boothwyn API error 500: Internal Server Error"
}

Get Submission

GET /rx/prescriptions/:id

Retrieve the full submission record by ID. Returns the current status, tracking information, and full request/response payloads. Only the API client that created the submission can access it.

Path Parameters

ParameterTypeDescription
id uuid The submissionId returned from the submit endpoint

Request Headers

Requires the same HMAC authentication headers as the submit endpoint. For GET requests, the body is empty, so the signing string is:

HMAC-SHA256(apiSecret, timestamp + '.' + '{}')
Note For GET requests, sign against an empty object {} since there is no request body. The request body sent to the server should also be empty or {}.

Response (200)

{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "apiKeyId": "key-uuid",
  "source": "guide-glp",
  "sourceOrderId": "ord_abc123",
  "callbackUrl": "https://api.example.com/webhooks/pharmacy-router",
  "patientState": "TX",
  "medicationName": "Semaglutide 2.5mg/mL",
  "pharmacy": "strive",
  "pharmacyOrderId": "STV-ORD-12345",
  "status": "shipped",
  "trackingNumber": "1Z999AA10123456784",
  "carrier": "UPS",
  "errorMessage": null,
  "requestPayload": { /* original PrescriptionSubmission */ },
  "responsePayload": { /* pharmacy raw response */ },
  "submittedAt": "2026-03-19T14:30:00.000Z",
  "createdAt": "2026-03-19T14:30:00.000Z",
  "updatedAt": "2026-03-20T09:15:00.000Z"
}

Error Responses

StatusDescription
401 Missing or invalid HMAC authentication
403 Submission belongs to a different API client
404 Submission not found

Webhook: Boothwyn

POST /rx/webhooks/boothwyn

Receives shipping and status updates from Boothwyn pharmacy. When an update is received, the router updates the submission record and sends an HMAC-signed callback to the original caller.

Authentication

HeaderDescription
x-webhook-secret Shared secret configured with Boothwyn

Request Body

FieldTypeDescription
caseId string required The Boothwyn case ID (matches pharmacyOrderId on the submission)
trackingNumber string optional FedEx tracking number
rxStatus string optional Status update: shipped, delivered, cancelled, processing

Status Mapping

Boothwyn StatusRouter Status
shippedshipped
delivereddelivered
cancelledcancelled
processingprocessing

Response

{ "ok": true }

Webhook: Strive

POST /rx/webhooks/strive

Receives shipping and status updates from Strive (RxVortex) pharmacy. When an update is received, the router updates the submission record and sends an HMAC-signed callback to the original caller.

Authentication

HeaderDescription
x-webhook-secret Shared secret configured with Strive

Request Body

FieldTypeDescription
tracking_id string required Strive order tracking ID (matches pharmacyOrderId on the submission)
trackingnumber string optional Shipping tracking number
rxstatus string optional Status update: shipped, delivered, cancelled, processing, in-transit
shippingcarrier string optional Shipping carrier name

Status Mapping

Strive StatusRouter Status
shippedshipped
delivereddelivered
cancelledcancelled
processingprocessing
in-transitshipped

Response

{ "ok": true }

Health Check

GET /rx/health

Returns service health status. No authentication required.

Response (200)

{
  "status": "ok",
  "service": "pharmacy-router",
  "timestamp": "2026-03-19T14:30:00.000Z"
}

Pharmacy Routing

The router determines which pharmacy receives an order based on the patient's state. Routing rules are stored in the router_pharmacy_config database table, with each state mapped to one or more pharmacies with priority rankings.

Resolution Logic

  1. If routing.preferredPharmacy is set, use it directly (bypass state routing)
  2. Determine state from routing.patientState, falling back to shipTo.state
  3. Query active routing configs for that state, ordered by priority (highest wins)
  4. If no route exists, return 422 with an error

Current Pharmacy Assignments

PharmacyStatesNotes
GMP (via Boothwyn) AL, AZ, CO, CT, FL, GA, IL, IN, KS, KY, LA, NJ, NV, NY, OH, PA, TN, WI 18 states. Uses the Boothwyn B2B adapter internally.
Strive TX Texas-only. Uses the RxVortex/Strive API.
Boothwyn All remaining states (except excluded) Fallback for states not covered by GMP or Strive.

Excluded States

No pharmacy coverage The following states have no active routing configuration. Submissions for these states will receive a 422 error: MN, CA, AR, NC, OK.

Priority System

Each state-pharmacy mapping has a priority integer. When multiple pharmacies are configured for a state, the one with the highest priority (and isActive = true) wins. This allows easy failover by adjusting priorities without removing configurations.

Adapters

Each pharmacy has an adapter that translates the standardized PrescriptionSubmission into the pharmacy's native API format. Adapters handle authentication, format mapping, and error normalization.

Boothwyn Adapter

Submits orders to the Boothwyn Nucleus B2B REST API. Used for both direct Boothwyn orders and GMP-routed orders (GMP fulfillment goes through the same Boothwyn B2B endpoint).

API Details

SKU Mapping

MedicationSKU
Semaglutide12565
Tirzepatide12850
NAD+12832

The adapter matches medication names by checking if the medication.name field contains "semaglutide", "tirzepatide", or "nad" (case-insensitive). If no match is found, the SKU is set to UNKNOWN_SKU and the pharmacy will reject the order.

Gender Mapping

"male" maps to "m", "female" maps to "f".

Strive Adapter

Submits orders to the Strive / RxVortex API. Uses OAuth 2.0 client credentials for authentication.

API Details

Catalog IDs

MedicationCatalog ID
Semaglutide Configured via STRIVE_CATALOG_SEMAGLUTIDE env var
Tirzepatide Configured via STRIVE_CATALOG_TIRZEPATIDE env var

Clinical Data Mapping

The clinical object is mapped to Strive's format: allergies maps directly, conditions maps to diseases, medications maps directly. Each entry in entries[] is wrapped as { description: "..." }.

PioneerRx Adapter

Coming Soon The PioneerRx adapter is planned but not yet implemented in the router. PioneerRx integration currently runs through the legacy orchestrator.

Callbacks

The router sends HMAC-signed HTTP POST callbacks to the callbackUrl you provide in the submission. Callbacks are fired at two points:

  1. After initial submission -- with the pharmacy result (success or failure)
  2. On webhook updates -- when a pharmacy sends a shipping/status update

Callback Authentication

Callbacks include X-Timestamp and X-Signature headers. The signature is computed using the same HMAC-SHA256 formula as request authentication, using your apiSecret:

HMAC-SHA256(apiSecret, timestamp + '.' + body)

You should verify this signature on your end to confirm the callback is authentic.

Callback Payload (Initial Submission)

{
  "submissionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "sourceOrderId": "ord_abc123",
  "pharmacy": "boothwyn",
  "status": "submitted",
  "pharmacyOrderId": "case-uuid-here",
  "error": null
}

Callback Payload (Webhook Update)

{
  "submissionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "sourceOrderId": "ord_abc123",
  "pharmacy": "boothwyn",
  "status": "shipped",
  "pharmacyOrderId": "case-uuid-here",
  "trackingNumber": "794644790132",
  "carrier": "FedEx"
}

Verifying Callbacks (Node.js)

import crypto from 'node:crypto';

function verifyCallback(req, apiSecret) {
  const timestamp = req.headers['x-timestamp'];
  const signature = req.headers['x-signature'];
  const bodyStr = JSON.stringify(req.body);

  const expected = crypto
    .createHmac('sha256', apiSecret)
    .update(timestamp + '.' + bodyStr)
    .digest('hex');

  const sigBuf = Buffer.from(signature, 'hex');
  const expBuf = Buffer.from(expected, 'hex');

  return sigBuf.length === expBuf.length
    && crypto.timingSafeEqual(sigBuf, expBuf);
}
Fire-and-Forget Callbacks are sent asynchronously and do not retry on failure. If your callback endpoint is unreachable, the callback is logged and discarded. Use the GET /rx/prescriptions/:id endpoint to poll for status as a fallback.

Test Mode

Set "test": true in the request body to submit orders to pharmacy sandbox and test environments. No real prescriptions are created in test mode.

Per-Pharmacy Behavior

PharmacyTest Mode Behavior
Boothwyn Appends ?test=true to the B2B API URL. Boothwyn processes the order in their test database -- it will not be fulfilled or shipped.
Strive Routes to https://sandbox.rxvortex.net instead of the production URL. Orders are processed in the Strive sandbox environment.
PioneerRx Planned: Will use the executeMethodTest endpoint (separate test database) instead of the production executeMethod endpoint.
Recommendation Always start integration development with "test": true. The router records test submissions the same way as production ones, so you can verify the full callback and status flow without creating real pharmacy orders.

Example: Test Submission

const body = {
  source: 'guide-glp',
  sourceOrderId: 'test-001',
  callbackUrl: 'https://api.example.com/webhooks/pharmacy-router',
  test: true,
  // ... patient, shipTo, prescriber, medication ...
};

const { timestamp, signature } = signRequest(body);

const res = await fetch('https://api.yourera.com/rx/prescriptions/submit', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-Key': API_KEY,
    'X-Timestamp': timestamp,
    'X-Signature': signature,
  },
  body: JSON.stringify(body),
});

const data = await res.json();
console.log(data);
// { submissionId: "...", pharmacy: "strive", status: "submitted", ... }