Skip to main content

Prescriber-Partner Integration Guide

📄 This guide is also available as a PDF download.

Version: 0.1 (Draft) Date: March 2, 2026 Base URL: https://api.stealth.health (production) | https://sandbox.stealth.health (development) Status: Proposal / Scoping

This guide describes a Prescriber Partner integration where the partner submits patient intake data via API, our licensed physicians review and prescribe, and the partner receives the full prescription outcome. The partner owns the patient relationship, intake experience, and (optionally) fulfillment.

1. Overview​

This integration is designed for partners who already have their own patient intake experience and need access to a licensed physician network for prescription review and signing. Rather than directing patients to our enrollment pages, the partner collects patient information and questionnaire responses in their own application, then submits the data to our API for clinical review.

How it works:

  1. Partner collects patient demographics and medical intake data through their own UI.
  2. Partner submits the appointment via API with structured patient and intake data.
  3. Our physicians review the intake and either approve (sign a prescription) or deny the case.
  4. Partner receives the outcome via webhook and/or polling — including full prescription details (medications, dosages, prescriber information).
  5. Partner either routes the prescription to their own pharmacy or requests that we route it to a pharmacy in our network.

The partner has full visibility into the clinical outcome, including prescription details, prescriber information, and denial reasons.


2. How This Differs from Other Integration Tiers​

Referral TierClinical TierPrescriber Tier (this guide)
Patient intakeOur enrollment pagesOur enrollment pagesPartner's own UI
Who submits dataPatient directlyPatient directlyPartner via API
Doctor reviewOur physiciansOur physiciansOur physicians
Prescription visibilityStatus only (no PHI)Full detailsFull details + prescriber info
Pharmacy routingWe handleWe handlePartner's choice (self or us)
Fulfillment & shippingWe handleWe handlePartner handles (or us, optionally)
Payment modelPatient pays at enrollmentPatient pays at enrollmentPer-review fee billed to partner
PHI exposureNoneFullFull
BAA requiredLimited (referral source)FullFull

3. HIPAA & Compliance Requirements​

Because this integration involves the partner sending and receiving protected health information (PHI), the following requirements apply:

3.1 Business Associate Agreement (BAA)​

A fully executed BAA is required before API credentials are issued. The BAA covers:

  • Partner's obligations for storing, transmitting, and disposing of PHI.
  • Breach notification procedures and timelines.
  • Audit and inspection rights.
  • Minimum necessary standard for data access.

3.2 Technical Requirements​

RequirementDetails
Transport encryptionTLS 1.2+ required for all API traffic
At-rest encryptionAES-256 (or equivalent) for any PHI stored on partner systems
Access controlsRole-based access; PHI accessible only to authorized personnel
Audit loggingAll PHI access must be logged with user, timestamp, and action
RetentionAudit logs retained for a minimum of 6 years
DisposalSecure deletion of PHI when no longer needed

3.3 Compliance Review​

Before going live, partners must:

  1. Complete a security questionnaire.
  2. Pass a compliance review.
  3. Agree to annual re-certification.

4. Authentication & Security​

4.1 API Keys​

Each partner receives a pair of credentials:

CredentialPurpose
X-Partner-IDIdentifies the partner (safe to log)
X-Api-KeyAuthenticates the request (secret — never log or expose client-side)

Both headers are required on every request:

POST /partner/appointments HTTP/1.1
Host: api.stealth.health
X-Partner-ID: ptr_acme_health
X-Api-Key: sk_live_7f3a...redacted
Content-Type: application/json

4.2 Key Rotation​

Partners can request key rotation at any time. When a new key is issued, the previous key remains valid for 72 hours to allow migration.

4.3 Webhook Signature Verification​

All webhook payloads include an X-Stealth-Signature header containing an HMAC-SHA256 signature of the request body, signed with the partner's webhook secret (provided during onboarding, separate from the API key).

X-Stealth-Signature: sha256=a1b2c3d4e5f6...

Verification example (Node.js):

const crypto = require("crypto");

function verifySignature(body, signature, secret) {
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(JSON.stringify(body))
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}

4.4 Transport Security​

  • All API traffic must use TLS 1.2+.
  • IP allowlisting is available on request.
  • All requests are logged with partner ID, endpoint, timestamp, and response code.

5. Integration Flow​

5.1 Sequence Diagram​

Prescriber Partner Integration Sequence Diagram

5.2 Lifecycle Summary​

  1. Partner submits appointment → pending
  2. Doctor begins review → in_review
  3. Doctor approves → doctor_reviewed (prescription details available)
  4. Doctor denies → rejected (denial reason available)
  5. (Optional) Partner requests pharmacy routing → routed → filled

6. API Reference — Submit Appointment​

6.1 POST /partner/appointments​

Submit a new appointment for physician review.

Request body:

{
"patient": {
"email": "jane.doe@example.com",
"first_name": "Jane",
"last_name": "Doe",
"date_of_birth": "1990-05-15",
"gender": "female",
"phone": "+15551234567",
"address": {
"line1": "123 Main St",
"line2": "Apt 4B",
"city": "Miami",
"state": "FL",
"zip": "33101",
"country": "US"
}
},
"condition": "trt-cream",
"intake_answers": [
{
"question_id": "current_medications",
"question_text": "What medications are you currently taking?",
"answer": "Lisinopril 10mg daily, Vitamin D 2000 IU"
},
{
"question_id": "allergies",
"question_text": "Do you have any known allergies?",
"answer": "No known allergies"
},
{
"question_id": "medical_history",
"question_text": "Relevant medical history",
"answer": "Hypertension, managed with medication. No surgeries."
},
{
"question_id": "current_symptoms",
"question_text": "What symptoms are you experiencing?",
"answer": "Fatigue, low energy, reduced libido, difficulty concentrating"
},
{
"question_id": "prior_treatments",
"question_text": "Have you tried any prior treatments?",
"answer": "No prior testosterone therapy"
},
{
"question_id": "recent_labs",
"question_text": "Recent lab results (if available)",
"answer": "Total T: 280 ng/dL, Free T: 5.2 pg/mL, CBC within normal limits"
}
],
"pharmacy_preference": "partner",
"partner_reference": "ACME-CASE-4821",
"metadata": {
"internal_case_id": "4821",
"provider_name": "Dr. Partner Physician"
}
}

Required fields:

FieldTypeDescription
patientobjectPatient demographics (see schema below)
patient.emailstringPatient email address
patient.first_namestringPatient first name
patient.last_namestringPatient last name
patient.date_of_birthstringISO date (YYYY-MM-DD)
patient.genderstringmale, female, or other
patient.phonestringPhone number with country code
patient.addressobjectFull address including state and country
conditionstringTreatment category (see Products endpoint)
intake_answersarrayStructured intake responses (see Section 10)
partner_referencestringPartner's internal reference for this case

Optional fields:

FieldTypeDefaultDescription
pharmacy_preferencestring"partner""partner" (you handle Rx routing) or "stealth" (we route to our pharmacy network)
preferred_pharmacy_idstringnullSpecific pharmacy ID from our network (only when pharmacy_preference = "stealth")
metadataobject{}Arbitrary key-value pairs returned in webhooks and API responses

Response (201 Created):

{
"appointment_id": "appt_f7c2e1a4b8d3",
"patient_id": "prof_a1b2c3d4e5f6",
"partner_reference": "ACME-CASE-4821",
"status": "pending",
"condition": "trt-cream",
"estimated_review_hours": 24,
"review_fee": {
"amount_cents": 4500,
"currency": "USD",
"status": "charged"
},
"created_at": "2026-03-02T14:30:00Z"
}

Error responses:

StatusCodeWhen
400VALIDATION_ERRORMissing or invalid required fields
400INVALID_INTAKERequired intake questions missing or malformed
400UNSUPPORTED_JURISDICTIONPatient's state/country not supported
402PAYMENT_REQUIREDReview fee charge failed
409DUPLICATE_REFERENCEpartner_reference already exists for this partner

7. API Reference — Appointments​

7.1 GET /partner/appointments​

List all appointments submitted by this partner.

Query parameters:

ParameterTypeDescription
statusstringFilter by status: pending, in_review, doctor_reviewed, rejected, completed
conditionstringFilter by treatment category
patient_emailstringFilter by patient email
created_afterISO datetimeOnly appointments created after this time
created_beforeISO datetimeOnly appointments created before this time
limitintegerResults per page (default: 50, max: 200)
cursorstringPagination cursor from previous response

Response (200):

{
"appointments": [
{
"appointment_id": "appt_f7c2e1a4b8d3",
"patient_id": "prof_a1b2c3d4e5f6",
"partner_reference": "ACME-CASE-4821",
"condition": "trt-cream",
"status": "doctor_reviewed",
"has_prescription": true,
"pharmacy_preference": "partner",
"created_at": "2026-03-02T14:30:00Z",
"reviewed_at": "2026-03-03T09:15:00Z"
}
],
"pagination": {
"has_more": false,
"next_cursor": null
}
}

7.2 GET /partner/appointments/:appointment_id​

Get full details for a single appointment.

Response (200):

{
"appointment_id": "appt_f7c2e1a4b8d3",
"patient_id": "prof_a1b2c3d4e5f6",
"partner_reference": "ACME-CASE-4821",
"condition": "trt-cream",
"status": "doctor_reviewed",
"patient": {
"first_name": "Jane",
"last_name": "Doe",
"email": "jane.doe@example.com",
"date_of_birth": "1990-05-15",
"gender": "female"
},
"prescription": {
"status": "signed",
"medications": [
{
"name": "Testosterone Cypionate Cream 200mg/mL",
"dosage_instructions": "Apply 1mL topically once daily",
"quantity": 1,
"quantity_unit": "tube",
"refills": 2,
"days_supply": 30
}
],
"prescriber": {
"name": "Dr. Sarah Johnson",
"npi": "1234567890",
"license_state": "FL"
},
"notes": "Patient is a good candidate for TRT based on symptoms and lab values.",
"signed_at": "2026-03-03T09:15:00Z"
},
"pharmacy_routing": null,
"review_fee": {
"amount_cents": 4500,
"currency": "USD",
"status": "charged",
"charged_at": "2026-03-02T14:30:00Z"
},
"denial_details": null,
"metadata": { "internal_case_id": "4821" },
"created_at": "2026-03-02T14:30:00Z",
"updated_at": "2026-03-03T09:15:00Z"
}

When an appointment is denied, the denial_details field is populated:

{
"denial_details": {
"category": "medical_contraindication",
"reason": "Patient's current medications present a contraindication.",
"denied_at": "2026-03-03T09:20:00Z"
}
}

8. API Reference — Prescriptions​

8.1 GET /partner/appointments/:appointment_id/prescription​

Returns the full prescription payload after a physician has reviewed and approved the appointment.

Response (200):

{
"prescription_id": "rx_e4f5a6b7c8d9",
"appointment_id": "appt_f7c2e1a4b8d3",
"status": "signed",
"prescriber": {
"name": "Dr. Sarah Johnson",
"npi": "1234567890",
"license_state": "FL",
"dea_number": "FJ1234567"
},
"patient": {
"first_name": "Jane",
"last_name": "Doe",
"date_of_birth": "1990-05-15"
},
"medications": [
{
"name": "Testosterone Cypionate Cream 200mg/mL",
"ndc": "12345-6789-01",
"dosage_instructions": "Apply 1mL topically once daily",
"quantity": 1,
"quantity_unit": "tube",
"refills": 2,
"days_supply": 30
}
],
"diagnosis_codes": ["E29.1"],
"notes": "Patient is a good candidate for TRT based on symptoms and lab values.",
"signed_at": "2026-03-03T09:15:00Z",
"valid_until": "2027-03-03T09:15:00Z"
}

Error responses:

StatusCodeWhen
404PRESCRIPTION_NOT_AVAILABLEAppointment has not been reviewed yet, or was denied
404APPOINTMENT_NOT_FOUNDAppointment does not exist or does not belong to this partner

9. API Reference — Pharmacy Routing​

9.1 POST /partner/appointments/:appointment_id/route-pharmacy​

Request that we route a signed prescription to a pharmacy in our network. This is optional — only call this if pharmacy_preference was set to "stealth" at submission time, or if you decide after review that you want us to handle routing.

Request body:

{
"pharmacy_id": "pharm_abc123"
}

The pharmacy_id is optional. If omitted, we route to the default pharmacy in our network for the patient's location.

Response (200):

{
"routing_status": "submitted",
"pharmacy": {
"name": "CDRX Pharmacy",
"npi": "9876543210",
"city": "Tampa",
"state": "FL"
},
"estimated_fill_date": "2026-03-05",
"routed_at": "2026-03-03T10:00:00Z"
}

Error responses:

StatusCodeWhen
400PRESCRIPTION_NOT_SIGNEDCannot route — appointment has not been approved
400ALREADY_ROUTEDPrescription has already been sent to a pharmacy
404PHARMACY_NOT_FOUNDSpecified pharmacy_id does not exist

9.2 GET /partner/appointments/:appointment_id/routing-status​

Check the status of a pharmacy routing request.

Response (200):

{
"routing_status": "filled",
"pharmacy": {
"name": "CDRX Pharmacy",
"npi": "9876543210"
},
"routed_at": "2026-03-03T10:00:00Z",
"filled_at": "2026-03-05T14:00:00Z",
"tracking": {
"carrier": "USPS",
"tracking_number": "9400111899223456789012",
"shipped_at": "2026-03-05T16:00:00Z"
}
}

9.3 GET /partner/products​

List available treatment categories that can be submitted for review.

Response (200):

{
"products": [
{
"category_id": "trt-cream",
"name": "TRT Cream",
"description": "Testosterone Replacement Therapy — topical cream",
"supported_countries": ["US"],
"supported_states": ["FL", "TX", "CA", "NY"],
"review_fee_cents": 4500,
"intake_schema_version": "1.0",
"required_intake_questions": [
"current_medications", "allergies", "medical_history",
"current_symptoms", "prior_treatments", "recent_labs"
]
},
{
"category_id": "bpc-157",
"name": "BPC-157 Peptide",
"description": "Body Protection Compound peptide therapy",
"supported_countries": ["US"],
"supported_states": ["FL", "TX", "CA"],
"review_fee_cents": 4500,
"intake_schema_version": "1.0",
"required_intake_questions": [
"current_medications", "allergies", "medical_history",
"current_symptoms", "prior_treatments"
]
}
]
}

10. Structured Intake Schema​

When submitting an appointment, the intake_answers array must include responses to required questions defined for the treatment category. Use the GET /partner/products endpoint to retrieve the list of required_intake_questions for each category.

10.1 Common Questions (required for all conditions)​

Question IDDescriptionExpected Answer Format
current_medicationsAll medications the patient is currently takingFree text or structured list
allergiesKnown allergies (medications, foods, environmental)Free text; "None" if none
medical_historyRelevant medical history, surgeries, hospitalizationsFree text
current_symptomsPresenting symptoms the patient is seeking treatment forFree text or checklist
prior_treatmentsPrevious treatments tried for this conditionFree text; "None" if none

10.2 Condition-Specific Questions​

TRT (trt-cream):

Question IDDescriptionExpected Answer Format
recent_labsRecent lab results (Total T, Free T, CBC, metabolic panel)Free text with values and dates
testosterone_symptomsSpecific testosterone deficiency symptom checklistComma-separated symptoms

Peptides (bpc-157):

Question IDDescriptionExpected Answer Format
injury_descriptionDescription of injury or condition being treatedFree text
treatment_goalsWhat the patient hopes to achieveFree text

10.3 Additional Questions​

Partners may include additional questions beyond the required set. These should use question_id values prefixed with custom_ and will be displayed to the reviewing physician under an "Additional Information" section.

{
"question_id": "custom_exercise_routine",
"question_text": "Describe your current exercise routine",
"answer": "Weight training 4x/week, cardio 2x/week"
}

10.4 Schema Versioning​

The intake schema is versioned (e.g. intake_schema_version: "1.0"). When we add or modify required questions, we increment the version and notify partners. Previous versions remain accepted for a deprecation period.


11. Webhook Events​

11.1 Event Types​

EventWhenKey Payload Fields
appointment.submittedAppointment created and queued for reviewappointment_id, patient_id, status, estimated_review_hours
appointment.in_reviewPhysician has opened the caseappointment_id
appointment.prescription_signedPhysician approved and signed a prescriptionappointment_id, prescription (full medication details), prescriber
appointment.deniedPhysician denied the caseappointment_id, denial_category, denial_reason
prescription.routedPrescription sent to pharmacy (when we route)appointment_id, pharmacy_name, pharmacy_npi
prescription.filledPharmacy confirmed fill and shipped (when we route)appointment_id, tracking
review.billedPer-review charge processedappointment_id, amount_cents, currency, invoice_id

11.2 Webhook Payload Format​

All webhook payloads follow this structure:

{
"event_id": "evt_a1b2c3d4e5f6",
"event_type": "appointment.prescription_signed",
"appointment_id": "appt_f7c2e1a4b8d3",
"patient_id": "prof_a1b2c3d4e5f6",
"partner_reference": "ACME-CASE-4821",
"data": {
"status": "doctor_reviewed",
"condition": "trt-cream",
"prescription": {
"medications": [
{
"name": "Testosterone Cypionate Cream 200mg/mL",
"dosage_instructions": "Apply 1mL topically once daily",
"quantity": 1,
"quantity_unit": "tube",
"refills": 2,
"days_supply": 30
}
],
"prescriber": {
"name": "Dr. Sarah Johnson",
"npi": "1234567890",
"license_state": "FL"
},
"signed_at": "2026-03-03T09:15:00Z"
},
"occurred_at": "2026-03-03T09:15:00Z"
},
"metadata": { "internal_case_id": "4821" },
"created_at": "2026-03-03T09:15:00Z"
}

11.3 Denial Webhook Payload​

{
"event_id": "evt_d4e5f6a7b8c9",
"event_type": "appointment.denied",
"appointment_id": "appt_f7c2e1a4b8d3",
"patient_id": "prof_a1b2c3d4e5f6",
"partner_reference": "ACME-CASE-4821",
"data": {
"status": "rejected",
"denial_category": "medical_contraindication",
"denial_reason": "Current medications present a contraindication for the requested therapy.",
"occurred_at": "2026-03-03T09:20:00Z"
},
"metadata": { "internal_case_id": "4821" },
"created_at": "2026-03-03T09:20:00Z"
}

11.4 Denial Categories​

CategoryDescription
medical_contraindicationPatient's medical history or current medications prevent prescribing
incomplete_informationInsufficient information to make a clinical decision
not_a_candidatePatient does not meet clinical criteria for the requested therapy
age_restrictionPatient does not meet age requirements
jurisdiction_restrictionCannot prescribe in the patient's state/country

11.5 Retry Policy​

Failed webhook deliveries are retried with exponential backoff:

AttemptDelay
1Immediate
230 seconds
35 minutes
430 minutes
52 hours
612 hours

After 6 failed attempts, the event is moved to dead letter. Partners can retrieve missed events via the GET /partner/events endpoint.


12. Per-Review Billing​

Each appointment submission incurs a per-review fee. The fee is charged at the time of submission, regardless of whether the physician approves or denies the case.

12.1 How Billing Works​

  1. During onboarding, we set up a Stripe billing relationship with a review_fee_cents amount in your partner configuration.
  2. When you submit an appointment via POST /partner/appointments, the review fee is charged to your account.
  3. A review.billed webhook fires when the charge is processed.
  4. Monthly invoices are available via Stripe.

12.2 Fee Structure​

ItemDetails
Review feePer-appointment, charged at submission
Pharmacy routingNo additional fee (when applicable)
SandboxNo charges — all billing is simulated

12.3 Billing in Sandbox​

In the sandbox environment, review fees are simulated. The review_fee object in responses will show "status": "simulated" instead of "charged".


13. Data Models​

13.1 Appointment​

{
"appointment_id": "string",
"patient_id": "string",
"partner_reference": "string",
"condition": "string",
"status": "pending | in_review | doctor_reviewed | rejected | completed",
"has_prescription": "boolean",
"pharmacy_preference": "partner | stealth",
"prescription": "Prescription | null",
"pharmacy_routing": "PharmacyRouting | null",
"denial_details": "DenialDetails | null",
"review_fee": "ReviewFee",
"metadata": "object",
"created_at": "ISO datetime",
"updated_at": "ISO datetime",
"reviewed_at": "ISO datetime | null"
}

13.2 Prescription​

{
"prescription_id": "string",
"status": "signed",
"prescriber": {
"name": "string",
"npi": "string",
"license_state": "string",
"dea_number": "string"
},
"medications": [
{
"name": "string",
"ndc": "string | null",
"dosage_instructions": "string",
"quantity": "number",
"quantity_unit": "string",
"refills": "number",
"days_supply": "number"
}
],
"diagnosis_codes": ["string"],
"notes": "string | null",
"signed_at": "ISO datetime",
"valid_until": "ISO datetime"
}

13.3 Denial Details​

{
"category": "string (see denial categories)",
"reason": "string",
"denied_at": "ISO datetime"
}

13.4 Review Fee​

{
"amount_cents": "number",
"currency": "string (USD)",
"status": "charged | simulated | failed",
"charged_at": "ISO datetime | null",
"invoice_id": "string | null"
}

14. Error Handling​

14.1 Error Response Format​

All errors follow a consistent structure:

{
"error": {
"code": "VALIDATION_ERROR",
"message": "Human-readable description of what went wrong.",
"details": { "field": "patient.email", "reason": "required" }
}
}

14.2 Error Codes​

CodeHTTP StatusDescription
MISSING_CREDENTIALS401X-Partner-ID or X-Api-Key header missing
INVALID_CREDENTIALS401API key does not match
PRESCRIBER_ACCESS_REQUIRED403Endpoint requires prescriber tier
VALIDATION_ERROR400Request body fails validation
INVALID_INTAKE400Required intake questions missing
UNSUPPORTED_JURISDICTION400Patient's state/country not supported for this condition
DUPLICATE_REFERENCE409partner_reference already used
PAYMENT_REQUIRED402Review fee charge failed
APPOINTMENT_NOT_FOUND404Appointment does not exist or belongs to another partner
PRESCRIPTION_NOT_AVAILABLE404Appointment not yet reviewed or was denied
PRESCRIPTION_NOT_SIGNED400Cannot route — no signed prescription
ALREADY_ROUTED400Prescription already sent to pharmacy
RATE_LIMIT_EXCEEDED429Too many requests
INTERNAL_ERROR500Unexpected server error

15. Rate Limits & Environments​

15.1 Rate Limits​

EnvironmentLimit
Production300 requests/minute
Sandbox60 requests/minute

Rate limit headers are included in every response:

X-RateLimit-Limit: 300
X-RateLimit-Remaining: 298
X-RateLimit-Reset: 1709401200

15.2 Environments​

EnvironmentBase URLBillingDoctor Review
Sandboxhttps://sandbox.stealth.healthSimulated (no real charges)Simulated (auto-review after ~2 minutes)
Productionhttps://api.stealth.healthReal charges via StripeReal physician review (typically < 24 hours)

In sandbox, appointments are automatically advanced through the review lifecycle to allow end-to-end testing without waiting for a real physician.


Appendix: Appointment Status Lifecycle​

Prescriber Partner Appointment Status Lifecycle