{
  "openapi": "3.0.3",
  "info": {
    "title": "Qualiphy API",
    "version": "1.0.0",
    "description": "Qualiphy telemedicine API for Good Faith Exams, Rx prescriptions (partner pharmacy and custom pharmacy), and exam lifecycle management.\n\nAuthentication: every request body MUST include an `api_key` field. There is no header-based auth, no query-param auth, and no OAuth. The `api_key` is a JSON property in the POST body, alongside the other fields. This holds for every endpoint in this spec.\n\nAll endpoints are POST with `Content-Type: application/json`. Base URL: `https://api.qualiphy.me`."
  },
  "servers": [
    {
      "url": "https://api.qualiphy.me"
    }
  ],
  "tags": [
    { "name": "Exam", "description": "Patient exam invites and lifecycle." },
    { "name": "Pharmacy", "description": "Partner and custom pharmacy discovery." },
    { "name": "Webhook", "description": "Inbound webhooks Qualiphy posts to your endpoint." }
  ],
  "paths": {
    "/api/exam_list": {
      "post": {
        "tags": ["Exam"],
        "summary": "List all exams configured on your clinic",
        "description": "Returns every exam configured on your clinic. Use this to discover the `id` values you pass to `/exam_invite`. Each exam record tells you whether it is a GFE or an Rx (`rx_type`), what attachments the patient must upload, and any preset price.",
        "operationId": "examList",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ApiKeyOnlyRequest" },
              "example": { "api_key": "YOUR_API_KEY" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "List of exams.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ExamListResponse" },
                "example": {
                  "clinic_id": 1234,
                  "exams": [
                    {
                      "id": 2228,
                      "title": "Embrace Aging GFE (Dermal Fillers, Botox, PDO, Microneedling PRP)",
                      "rx_type": 1,
                      "price": null,
                      "attachments_required": 1,
                      "attachments": []
                    },
                    {
                      "id": 1822,
                      "title": "Clinical Evaluation for Injectables & Lifting",
                      "rx_type": 1,
                      "price": "27.99",
                      "attachments_required": 1,
                      "attachments": []
                    },
                    {
                      "id": 2769,
                      "title": "3 Month Supply: Semaglutide with Medication Shipping (No Labwork Required)",
                      "rx_type": 2,
                      "price": null,
                      "attachments_required": 2,
                      "attachments": ["ID"]
                    },
                    {
                      "id": 3702,
                      "title": "3 Month Supply: Semaglutide with Medication Shipping (With Lab-work Upload)",
                      "rx_type": 2,
                      "price": null,
                      "attachments_required": 2,
                      "attachments": ["ID", "Lab"]
                    },
                    {
                      "id": 1486,
                      "title": "Urgent Care - Acne (Doxycycline, Clindamycin, Benzoyl Peroxide) Treatment",
                      "rx_type": 2,
                      "price": null,
                      "attachments_required": 2,
                      "attachments": ["ID"]
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/exam_invite": {
      "post": {
        "tags": ["Exam"],
        "summary": "Create a patient exam invite",
        "description": "The core endpoint. Creates a patient exam invite, returns a meeting URL, and (by default) emails and texts the patient. Supports four distinct scenarios via parameter combinations, all hitting this same endpoint:\n\n1. **Good Faith Exam (GFE)** - non-Rx clearance for in-clinic procedures. Pass `exams` + identity fields only. No pharmacy fields. Use a `rx_type: 1` exam from `/exam_list`.\n2. **QualiphyRx Packages** (Partner Pharmacy) - prescription fulfilled through Qualiphy's compounding-pharmacy network with shipping included. Pass `pharmacy_id` (from `/partner_pharmacy_list`) and `order_set_id` (from `/partner_pharmacy_treatment_packages`). Use a `rx_type: 2` exam. Pickup not supported. Webhook events 1, 2, and 3 all fire.\n3. **Urgent Care Visit** - prescription sent to a retail pharmacy for same-day pickup/delivery. Pass `custom_pharmacy: 1`, `ncpdpid`, and the `pharmacy_*` address fields. Webhook events 1 and 2 fire (no event 3 — retail pharmacy handles fulfillment).\n4. **Choose Your Own Pharmacy** - same field set as Urgent Care, used for non-urgent meds. The `ncpdpid` and `pharmacy_*` fields come from `/custom_pharmacy_search`.\n\n**Watch the `custom_pharmacy` mapping**: `1` means custom (retail), `2` means partner. Counter-intuitive. For test mode, set both `state` and `tele_state` to `TE` and use names like `Test Approve` / `Test Prescription Approve` to steer the test provider's verdict.",
        "operationId": "examInvite",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ExamInviteRequest" },
              "examples": {
                "scenarioGFE": {
                  "summary": "Scenario 1 - Good Faith Exam (non-Rx)",
                  "value": {
                    "api_key": "YOUR_API_KEY",
                    "exams": [4226],
                    "first_name": "Test",
                    "last_name": "Approve",
                    "email": "test@example.com",
                    "dob": "1985-03-12",
                    "phone_number": "+15555550100",
                    "state": "TE",
                    "tele_state": "TE",
                    "webhook_url": "https://yourstore.com/webhooks/qualiphy",
                    "additional_data": "{\"order_id\":\"WC-12847\"}",
                    "redirect_approve": "https://yourstore.com/cleared",
                    "redirect_reject": "https://yourstore.com/needs-followup"
                  }
                },
                "scenarioPartnerPharmacy": {
                  "summary": "Scenario 2 - QualiphyRx Packages (partner pharmacy)",
                  "value": {
                    "api_key": "YOUR_API_KEY",
                    "exams": [1292],
                    "first_name": "Test",
                    "last_name": "Prescription Approve",
                    "email": "test@example.com",
                    "dob": "1982-09-04",
                    "phone_number": "+15555550100",
                    "state": "TE",
                    "tele_state": "TE",
                    "pharmacy_id": 47,
                    "order_set_id": "OS-2104",
                    "webhook_url": "https://yourapp.com/webhooks/qualiphy",
                    "additional_data": "{\"subscription_id\":\"SUB-9912\"}"
                  }
                },
                "scenarioUrgentOrCustom": {
                  "summary": "Scenario 3/4 - Urgent Care or Choose Your Own Pharmacy",
                  "value": {
                    "api_key": "YOUR_API_KEY",
                    "exams": [3104],
                    "first_name": "Test",
                    "last_name": "Prescription Approve",
                    "email": "test@example.com",
                    "dob": "1991-02-18",
                    "phone_number": "+15555550100",
                    "state": "TE",
                    "tele_state": "TE",
                    "custom_pharmacy": 1,
                    "ncpdpid": "1234567",
                    "pharmacy_name": "CVS Pharmacy #5821",
                    "pharmacy_address_line_1": "1500 Main St",
                    "pharmacy_city": "Austin",
                    "pharmacy_state": "TX",
                    "pharmacy_zip_code": "78701",
                    "pharmacy_phone": "+15125550000",
                    "webhook_url": "https://yourapp.com/webhooks/qualiphy"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Invite created. `patient_exam_id` is the per-patient instance ID — use it for cancel, resend, submit-answers, attachment upload, and to correlate webhook payloads. `exam_id` is the catalog ID you passed in.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ExamInviteResponse" },
                "example": {
                  "http_code": 200,
                  "meeting_url": "https://app.qualiphy.me/meeting/...",
                  "meeting_uuid": "abc-123-def-456",
                  "patient_exams": [
                    {
                      "patient_exam_id": 4844,
                      "exam_title": "GFE Aesthetic",
                      "exam_id": 4226
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Bad request. Typically a missing required field, malformed `dob` (must be YYYY-MM-DD) or `phone_number`, or an invalid `tele_state` (use a two-letter uppercase abbreviation, `DC` for D.C., or `TE` for test mode). The response body specifies which field failed."
          },
          "401": {
            "description": "Unauthorized. Possible response messages and what each means:\n- `Unauthorized access.` - api_key missing or invalid.\n- `Need to add Patient Care Provider via Qualiphy Portal!` - no provider configured for the patient's `tele_state`. Add one in Portal -> Operators.\n- `You need to signed docusign and signed rx` - onboarding agreements incomplete.\n- `Need to sign Prescriptive Services Contract via Qualiphy Portal!` - required before any Rx exam (GFE works without it)."
          }
        }
      }
    },
    "/api/exam_upload_attachment": {
      "post": {
        "tags": ["Exam"],
        "summary": "Upload an attachment to a patient exam",
        "description": "Attach a document to a patient exam (ID photos, lab results, prior records). The provider sees attachments during the screening.\n\n**Spec note:** the public docs section for this endpoint contains only a one-line description and does not enumerate request fields, document a curl example, or show a response shape. The schema below is a best-effort reconstruction (api_key + patient_exam_id + file payload). Before going live, confirm the exact field names (especially how the file is encoded — multipart vs base64 JSON) directly with Qualiphy support; the rest of the API uses `application/json`, so base64 inside JSON is the most likely shape.",
        "operationId": "examUploadAttachment",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ExamUploadAttachmentRequest" },
              "example": {
                "api_key": "YOUR_API_KEY",
                "patient_exam_id": 4844,
                "file_name": "id-front.jpg",
                "file_type": "ID",
                "file_base64": "<base64-encoded-bytes>"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Attachment received. Response shape is not documented; the example below is a best-effort acknowledgment shape consistent with the other side-effect endpoints (`/exam_invite_resend`, `/exam_submit_answers`).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/GenericOk" },
                "example": { "http_code": 200, "status": "ok" }
              }
            }
          }
        }
      }
    },
    "/api/exam_questions": {
      "post": {
        "tags": ["Exam"],
        "summary": "Get the screening question schema for an exam",
        "description": "Returns the screening question schema for a given exam ID. Use to build pre-fill UI on your own intake form, or to map your existing intake fields onto the Qualiphy question set.",
        "operationId": "examQuestions",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ExamQuestionsRequest" },
              "example": {
                "api_key": "YOUR_API_KEY",
                "exam_id": 4226
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Question schema for the requested exam. Note: the public docs do not include a sample response body for this endpoint; the example below mirrors the `{ no, question }` shape used by `/exam_submit_answers` and the webhook `questions_answers` array. Confirm exact fields against a live call.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ExamQuestionsResponse" },
                "example": {
                  "exam_id": 4226,
                  "questions": [
                    { "no": 1, "question": "Are you currently taking any medications?" },
                    { "no": 2, "question": "Have you had any reactions to anesthesia?" },
                    { "no": 3, "question": "Are you pregnant or breastfeeding?" }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/exam_submit_answers": {
      "post": {
        "tags": ["Exam"],
        "summary": "Submit screening answers on behalf of the patient",
        "description": "Submits answers on behalf of the patient. Pair with `/exam_questions` to pre-fill the screening from your own intake data so patients are not answering the same questions twice. Submit answers for every question to skip the questionnaire entirely.",
        "operationId": "examSubmitAnswers",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ExamSubmitAnswersRequest" },
              "example": {
                "api_key": "YOUR_API_KEY",
                "patient_exam_id": 149784,
                "answers": [
                  { "no": 1, "question": "Are you currently taking any medications?", "answer": "No." },
                  { "no": 2, "question": "Have you had any reactions to anesthesia?", "answer": "No." },
                  { "no": 3, "question": "Are you pregnant or breastfeeding?", "answer": "No." }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Answers accepted. Response shape is not documented; the example below is a best-effort acknowledgment.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/GenericOk" },
                "example": { "http_code": 200, "status": "ok" }
              }
            }
          }
        }
      }
    },
    "/api/exam_invite_resend": {
      "post": {
        "tags": ["Exam"],
        "summary": "Resend invite email and SMS",
        "description": "Re-sends the invite email and SMS to the patient. Use when a patient says they did not receive their original invite.",
        "operationId": "examInviteResend",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/PatientExamIdRequest" },
              "example": {
                "api_key": "YOUR_API_KEY",
                "patient_exam_id": 4844
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Invite resent. Response shape is not documented; the example below is a best-effort acknowledgment.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/GenericOk" },
                "example": { "http_code": 200, "status": "ok" }
              }
            }
          }
        }
      }
    },
    "/api/exam_invite_cancel": {
      "post": {
        "tags": ["Exam"],
        "summary": "Cancel a patient exam invite",
        "description": "Cancel a previously created patient exam invite. Useful when a patient cancels their order or you need to clean up a duplicate invite. Cancellation is only valid before the provider has acted on the exam. Pass `patient_exam_id` (not `exam_id`).",
        "operationId": "examInviteCancel",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/PatientExamIdRequest" },
              "example": {
                "api_key": "YOUR_API_KEY",
                "patient_exam_id": 4844
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cancelled.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ExamCancelResponse" },
                "example": {
                  "http_code": 200,
                  "patient_exam_id": 55322,
                  "status": "cancelled",
                  "cancelled_at": "2026-04-22T19:08:29.680Z"
                }
              }
            }
          },
          "400": {
            "description": "Cannot cancel. Possible messages: `Exam status is already approved.`, `Exam status is already rejected.`, `Exam status is already Deferred to Medical Director.`. Idempotency: calling cancel twice on a cancelled exam returns 400 with the relevant status — treat as success."
          }
        }
      }
    },
    "/api/partner_pharmacy_list": {
      "post": {
        "tags": ["Pharmacy"],
        "summary": "List partner compounding pharmacies",
        "description": "Lists Qualiphy's partner compounding pharmacies available for your account. Returns each pharmacy's `pharmacy_id`, which you pass into `/exam_invite` for Rx programs.",
        "operationId": "partnerPharmacyList",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ApiKeyOnlyRequest" },
              "example": { "api_key": "YOUR_API_KEY" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Available partner pharmacies.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PartnerPharmacyListResponse" },
                "example": {
                  "pharmacies": [
                    { "pharmacy_id": 17, "name": "Empower Pharmacy", "type": "compounding" },
                    { "pharmacy_id": 22, "name": "Strive Pharmacy", "type": "compounding" }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/partner_pharmacy_treatment_packages": {
      "post": {
        "tags": ["Pharmacy"],
        "summary": "List treatment packages for a partner pharmacy",
        "description": "Lists treatment packages (drug + dosage combinations + pricing) available at a specific partner pharmacy. Returns `order_set_id` values for use with `/exam_invite`. Legacy `exam_pos_id` is still returned for older integrations.",
        "operationId": "partnerPharmacyTreatmentPackages",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/PartnerPharmacyPackagesRequest" },
              "example": {
                "api_key": "YOUR_API_KEY",
                "pharmacy_id": 17,
                "exam_id": 1292
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Treatment packages.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PartnerPharmacyPackagesResponse" },
                "example": {
                  "treatment_packages": [
                    {
                      "order_set_id": "OS-2104",
                      "exam_pos_id": 4861,
                      "drug_name": "Semaglutide",
                      "strength": "2mg",
                      "size": "2mL",
                      "duration_days": 30,
                      "price": 29900
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/custom_pharmacy_search": {
      "post": {
        "tags": ["Pharmacy"],
        "summary": "Search any U.S. pharmacy",
        "description": "Look up any U.S. pharmacy by name, location, or NCPDPID. Returns full address and identifier fields needed for `/exam_invite` when using custom pharmacy routing.",
        "operationId": "customPharmacySearch",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CustomPharmacySearchRequest" },
              "example": {
                "api_key": "YOUR_API_KEY",
                "search": "CVS",
                "zip_code": "10001"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Matching pharmacies.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CustomPharmacySearchResponse" },
                "example": {
                  "pharmacies": [
                    {
                      "ncpdpid": "3318547",
                      "name": "CVS Pharmacy #4382",
                      "address_line_1": "342 W 23rd St",
                      "city": "New York",
                      "state": "NY",
                      "zip_code": "10011",
                      "phone": "2127757582"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "examLifecycleWebhook": {
      "post": {
        "tags": ["Webhook"],
        "summary": "Inbound webhook from Qualiphy",
        "description": "Qualiphy POSTs lifecycle events to the `webhook_url` you supplied to `/exam_invite`. Always dispatch on the `event` integer — treating the webhook as a single payload type is the most common integration mistake:\n- `1` = Consultation Complete. Fires for every exam (GFE, Rx, Urgent Care) when the provider finishes the consultation. Carries the verdict in `exam_status` (Approved, Rejected, Deferred to Medical Director, NA, Missed) and the screening Q&A.\n- `2` = Prescription Confirmed. Fires only for Rx exams, after event 1, when the treatment order is written and confirmed at the pharmacy. Carries the full `prescription[]` array.\n- `3` = Prescription Tracking. Fires only for **partner-pharmacy** Rx shipments when the package ships. Custom-pharmacy / urgent-care exams do NOT receive event 3 — the retail pharmacy is the source of truth for fulfillment.\n\nRespond 200 immediately (under 100ms) and process asynchronously. Treat `patient_exam_id` as the idempotency key. Webhook source verification is not yet available — IP-allowlist if possible and ensure HTTPS.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/WebhookEvent" },
              "examples": {
                "event1Approved": {
                  "summary": "Event 1 - Consultation Complete (Approved)",
                  "value": {
                    "event": 1,
                    "reason": "Initial consultation complete",
                    "exam_id": "exam-001",
                    "exam_url": "https://d29tu1ox8kfrdk.cloudfront.net/pdfs/...",
                    "exam_name": "General Health Check",
                    "exam_status": "Approved",
                    "rx_status": "Pharmacy Confirmed",
                    "clinic_name": "Downtown Medical Center",
                    "patient_email": "patient@example.com",
                    "patient_phone_number": "5551234567",
                    "patient_exam_id": "patient-exam-789",
                    "provider_name": "NP Liza",
                    "additional_data": "{\"order_id\":\"WC-12847\"}",
                    "questions_answers": [
                      { "no": 1, "question": "How are you feeling?", "answer": "Good." }
                    ]
                  }
                },
                "event2Prescription": {
                  "summary": "Event 2 - Prescription Confirmed",
                  "value": {
                    "event": 2,
                    "exam_id": 1292,
                    "exam_name": "GLP-1 Weight Loss",
                    "exam_status": "Approved",
                    "exam_url": "https://d29tu1ox8kfrdk.cloudfront.net/pdfs/...",
                    "rx_status": "Pharmacy Confirmed",
                    "clinic_name": "Downtown Medical Center",
                    "patient_email": "patient@example.com",
                    "patient_phone_number": "5551234567",
                    "patient_exam_id": 149784,
                    "provider_name": "NP Liza",
                    "prescribed_os_id": 486,
                    "prescription": [
                      {
                        "prescription_id": 3320,
                        "drug_name": "Semaglutide",
                        "dosage_form_type": "Inj",
                        "strength": "2mg",
                        "size": "2mL",
                        "quantity": "2",
                        "quantity_units": "",
                        "directions": "Inject 0.25mg subcutaneously weekly.",
                        "duration_days": 30,
                        "refill_amount": 2,
                        "ndc_number": "",
                        "gpi_number": "",
                        "schedule_code": "",
                        "active_ingredient": "",
                        "inactive_ingredient": "",
                        "pharmacy_name": "Choose Your Pharmacy",
                        "pharmacy_contacts": [
                          { "full_name": null, "email": null, "phone_number": null, "role": null }
                        ],
                        "prescription_tracking": [
                          { "rx_number": null, "tracking_number": null, "ship_carrier": null, "delivery_service": null, "pharmacy_location": null }
                        ],
                        "created_at": "2025-03-19T18:42:27.000Z"
                      }
                    ],
                    "questions_answers": [
                      { "no": 1, "question": "How are you feeling?", "answer": "Good." }
                    ]
                  }
                },
                "event3Tracking": {
                  "summary": "Event 3 - Prescription Tracking",
                  "value": {
                    "event": 3,
                    "patient_exam_id": "patient-exam-789",
                    "prescription_id": "12345",
                    "tracking_number": "123ABC456DEF",
                    "delivery_service": "Overnight",
                    "pharmacy_location": "Main Street Location",
                    "order_id": "1234567890"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Acknowledge receipt. Respond 200 fast; process the payload asynchronously."
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ApiKeyOnlyRequest": {
        "type": "object",
        "required": ["api_key"],
        "properties": {
          "api_key": { "type": "string", "description": "Your account API key." }
        }
      },
      "PatientExamIdRequest": {
        "type": "object",
        "required": ["api_key", "patient_exam_id"],
        "properties": {
          "api_key": { "type": "string" },
          "patient_exam_id": { "type": "integer", "description": "From the `patient_exams[]` array in the original `/exam_invite` response. Not the `exam_id`." }
        }
      },
      "Exam": {
        "type": "object",
        "properties": {
          "id": { "type": "integer", "description": "Exam ID. Pass this to `/exam_invite` in the `exams` array." },
          "title": { "type": "string", "description": "Human-readable exam name as configured in the Qualiphy portal." },
          "rx_type": { "type": "integer", "description": "1 = non-Rx (GFE). 2 = Rx (covers QualiphyRx Packages, Urgent Care, and Choose-Your-Own-Pharmacy — there is no separate \"urgent\" rx_type; distinguish those by `title`)." },
          "price": { "type": "string", "nullable": true, "description": "Preset price as a decimal string (e.g. \"27.99\") when one is configured, otherwise null." },
          "attachments_required": { "type": "integer", "description": "Number of files the patient must upload before the consultation. Always at least 1. (Note: the docs glossary treats this as an inverted flag — 1=not required, 2=required — while the endpoint param table describes it as a count. Treat the value as a count of required files; if you see 1 with an empty `attachments` array, only the standard ID upload is required.)" },
          "attachments": { "type": "array", "items": { "type": "string" }, "description": "Names of required attachments beyond the default ID upload, e.g. `[\"ID\"]`, `[\"ID\", \"Lab\"]`, or `[\"Picture of hair loss area\"]`. Empty array means only the standard ID is required." }
        }
      },
      "ExamListResponse": {
        "type": "object",
        "properties": {
          "clinic_id": { "type": "integer" },
          "exams": { "type": "array", "items": { "$ref": "#/components/schemas/Exam" } }
        }
      },
      "ExamInviteRequest": {
        "type": "object",
        "required": [
          "api_key",
          "exams",
          "first_name",
          "last_name",
          "email",
          "dob",
          "phone_number",
          "tele_state"
        ],
        "description": "The `/exam_invite` request body. Required fields are the same across all four scenarios; the optional fields vary:\n- **GFE** (rx_type 1 exam): identity fields only, no pharmacy fields.\n- **Partner Pharmacy**: add `pharmacy_id` and (optionally) `order_set_id`.\n- **Urgent Care / Custom Pharmacy**: add `custom_pharmacy: 1`, `ncpdpid`, and the `pharmacy_*` address fields.\nFor test mode, set both `state` and `tele_state` to `TE`.",
        "properties": {
          "api_key": { "type": "string", "description": "Your account API key." },
          "exams": { "type": "array", "items": { "type": "integer" }, "description": "Array of exam IDs from `/exam_list`. Stack non-Rx exams freely; only one Rx exam may run per meeting." },
          "first_name": { "type": "string", "description": "Patient first name. In test mode, use `Test` plus an outcome keyword (`Approve`, `Reject`, `N/A`, `Prescription Approve`, `Prescription Reject`) — the test provider reads the name to decide how to process the exam." },
          "last_name": { "type": "string", "description": "Patient last name." },
          "email": { "type": "string", "description": "Patient email. Used for invite delivery and as the patient identifier." },
          "dob": { "type": "string", "description": "Date of birth as YYYY-MM-DD.", "example": "1990-06-30" },
          "phone_number": { "type": "string", "description": "Patient phone. Two formats accepted: `+15552223232` or `1234567890`." },
          "tele_state": { "type": "string", "description": "Two-letter UPPERCASE state abbreviation of the **patient** (not your clinic). Determines provider matching and is the most common source of integration bugs. Use `DC` for Washington D.C., or `TE` for test mode." },
          "webhook_url": { "type": "string", "description": "Where to POST the lifecycle webhook events. Highly recommended — without it, you have no way to know when the exam completes." },
          "additional_data": { "type": "string", "description": "Stringified JSON. Returned unchanged in the webhook payload. Use it to round-trip your internal IDs (order ID, subscription ID, etc.)." },
          "redirect_approve": { "type": "string", "description": "URL the patient is sent to after an Approved verdict." },
          "redirect_reject": { "type": "string", "description": "URL the patient is sent to after a Rejected verdict." },
          "redirect_na": { "type": "string", "description": "URL the patient is sent to after an NA verdict." },
          "redirect_missed": { "type": "string", "description": "URL the patient is sent to after a Missed verdict. Fires after a 3-minute provider-availability timeout." },
          "gender": { "type": "integer", "description": "Integer, not string. 1=Male, 2=Female, 3=Prefer not to say." },
          "dmv_license_number": { "type": "string", "description": "Patient's DMV license number for ID verification." },
          "address_line_1": { "type": "string", "description": "Patient's home address line 1." },
          "address_line_2": { "type": "string", "description": "Patient's home address line 2." },
          "city": { "type": "string", "description": "Patient's home address city." },
          "state": { "type": "string", "description": "Patient's home **address** state (two-letter abbreviation). Conceptually different from `tele_state`, which drives provider matching — they are often the same value. Set to `TE` for test mode (along with `tele_state`)." },
          "zip_code": { "type": "string", "description": "Patient's home ZIP." },
          "shipping_address_line_1": { "type": "string", "description": "Where to ship Rx treatments. Different from home address when shipping to clinic, P.O. box, or alternate location." },
          "shipping_address_line_2": { "type": "string" },
          "shipping_city": { "type": "string" },
          "shipping_state": { "type": "string" },
          "shipping_zip_code": { "type": "string" },
          "pharmacy_id": { "type": "integer", "description": "Partner pharmacy ID from `/partner_pharmacy_list`. Used for the QualiphyRx Packages (Partner Pharmacy) scenario." },
          "order_set_id": { "type": "string", "description": "Preselect a specific drug + dosage package from `/partner_pharmacy_treatment_packages`. Optional even on partner-pharmacy invites; omit to let the provider choose." },
          "exam_pos_id": { "type": "integer", "description": "Legacy alias for `order_set_id`. Still accepted as a fallback for older integrations." },
          "custom_pharmacy": { "type": "integer", "description": "Counter-intuitive mapping — easy to invert. 1=Custom Pharmacy (retail like CVS, Walgreens). 2=Partner Pharmacy (Qualiphy's compounding network). Used to flag the Urgent Care / Choose Your Own Pharmacy scenarios." },
          "ncpdpid": { "type": "string", "description": "Custom pharmacy's NCPDPID from `/custom_pharmacy_search`. Required when `custom_pharmacy: 1`." },
          "pharmacy_name": { "type": "string", "description": "Custom pharmacy name. Used with `custom_pharmacy: 1`." },
          "pharmacy_address_line_1": { "type": "string", "description": "Custom pharmacy street address line 1." },
          "pharmacy_address_line_2": { "type": "string", "description": "Custom pharmacy street address line 2." },
          "pharmacy_city": { "type": "string", "description": "Custom pharmacy city." },
          "pharmacy_state": { "type": "string", "description": "Custom pharmacy state (two-letter)." },
          "pharmacy_zip_code": { "type": "string", "description": "Custom pharmacy ZIP." },
          "pharmacy_phone": { "type": "string", "description": "Custom pharmacy phone." },
          "pharmacy_type": { "type": "string", "description": "Custom pharmacy type label, when applicable." },
          "provider_pos_selection": { "type": "integer", "description": "\"POS\" = Pharmacy / Order Set. 1=Provider does not choose (you've fully specified both). 2=Provider picks order set only (you specified pharmacy). 3=Provider picks both pharmacy and order set (most flexible)." },
          "custom_pharmacy_patient_choice": { "type": "integer", "description": "1=Custom pharmacy is preselected by you in this request. 2=Patient picks their own custom pharmacy during the meeting." },
          "skip_pharmacy_confirmation": { "type": "integer", "description": "1=Show pharmacy confirmation screen. 2=Skip it. Only actually skips when all pharmacy fields are present in the payload." },
          "custom_pharmacy_patient_billing": { "type": "integer", "description": "1=Clinic is billed by the pharmacy. 2=Patient is billed." },
          "custom_pharmacy_clinic_billing": { "type": "string", "description": "Clinic identifier for pharmacy billing. Required when `custom_pharmacy_patient_billing: 1`." },
          "custom_pharmacy_delivery_method": { "type": "integer", "description": "1=Patient picks up at pharmacy. 2=Ship to patient or clinic." }
        }
      },
      "ExamInviteResponse": {
        "type": "object",
        "properties": {
          "http_code": { "type": "integer" },
          "meeting_url": { "type": "string" },
          "meeting_uuid": { "type": "string" },
          "patient_exams": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "patient_exam_id": { "type": "integer" },
                "exam_title": { "type": "string" },
                "exam_id": { "type": "integer" }
              }
            }
          }
        }
      },
      "ExamUploadAttachmentRequest": {
        "type": "object",
        "required": ["api_key", "patient_exam_id"],
        "description": "The public docs section for `/api/exam_upload_attachment` does not enumerate request fields. The shape below is a best-effort reconstruction (api_key + patient_exam_id + file payload). Confirm exact field names with Qualiphy before going live.",
        "properties": {
          "api_key": { "type": "string" },
          "patient_exam_id": { "type": "integer", "description": "From the `patient_exams[]` array in the original `/exam_invite` response." },
          "file_name": { "type": "string", "description": "Original filename including extension, e.g. `id-front.jpg`." },
          "file_type": { "type": "string", "description": "Attachment kind. Match the strings shown in `/exam_list` `attachments[]` for the exam, e.g. `ID`, `Lab`." },
          "file_base64": { "type": "string", "description": "Base64-encoded file contents. (Most likely encoding; the rest of the API is JSON-only.)" }
        }
      },
      "ExamQuestionsRequest": {
        "type": "object",
        "required": ["api_key", "exam_id"],
        "properties": {
          "api_key": { "type": "string" },
          "exam_id": { "type": "integer" }
        }
      },
      "ExamQuestionsResponse": {
        "type": "object",
        "description": "Schema not fully documented; questions are returned as numbered objects matching the `{ no, question, answer }` shape used by `/exam_submit_answers` and the webhook `questions_answers` array.",
        "properties": {
          "exam_id": { "type": "integer" },
          "questions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "no": { "type": "integer" },
                "question": { "type": "string" }
              }
            }
          }
        }
      },
      "Answer": {
        "type": "object",
        "properties": {
          "no": { "type": "integer" },
          "question": { "type": "string" },
          "answer": { "type": "string" }
        }
      },
      "ExamSubmitAnswersRequest": {
        "type": "object",
        "required": ["api_key", "patient_exam_id", "answers"],
        "properties": {
          "api_key": { "type": "string" },
          "patient_exam_id": { "type": "integer" },
          "answers": { "type": "array", "items": { "$ref": "#/components/schemas/Answer" } }
        }
      },
      "ExamCancelResponse": {
        "type": "object",
        "properties": {
          "http_code": { "type": "integer" },
          "patient_exam_id": { "type": "integer" },
          "status": { "type": "string", "example": "cancelled" },
          "cancelled_at": { "type": "string", "format": "date-time" }
        }
      },
      "Pharmacy": {
        "type": "object",
        "properties": {
          "pharmacy_id": { "type": "integer" },
          "name": { "type": "string" },
          "type": { "type": "string", "example": "compounding" }
        }
      },
      "PartnerPharmacyListResponse": {
        "type": "object",
        "properties": {
          "pharmacies": { "type": "array", "items": { "$ref": "#/components/schemas/Pharmacy" } }
        }
      },
      "PartnerPharmacyPackagesRequest": {
        "type": "object",
        "required": ["api_key", "pharmacy_id", "exam_id"],
        "properties": {
          "api_key": { "type": "string" },
          "pharmacy_id": { "type": "integer" },
          "exam_id": { "type": "integer" }
        }
      },
      "TreatmentPackage": {
        "type": "object",
        "properties": {
          "order_set_id": { "type": "string" },
          "exam_pos_id": { "type": "integer", "description": "Legacy ID for older integrations." },
          "drug_name": { "type": "string" },
          "strength": { "type": "string" },
          "size": { "type": "string" },
          "duration_days": { "type": "integer" },
          "price": { "type": "integer", "description": "Price in cents." }
        }
      },
      "PartnerPharmacyPackagesResponse": {
        "type": "object",
        "properties": {
          "treatment_packages": { "type": "array", "items": { "$ref": "#/components/schemas/TreatmentPackage" } }
        }
      },
      "CustomPharmacySearchRequest": {
        "type": "object",
        "required": ["api_key"],
        "description": "At least one of search, zip_code, or ncpdpid should be supplied.",
        "properties": {
          "api_key": { "type": "string" },
          "search": { "type": "string", "description": "Pharmacy name fragment, e.g. \"CVS\"." },
          "zip_code": { "type": "string", "description": "Five-digit ZIP for proximity filtering." },
          "ncpdpid": { "type": "string", "description": "Direct NCPDPID lookup." }
        }
      },
      "CustomPharmacy": {
        "type": "object",
        "properties": {
          "ncpdpid": { "type": "string" },
          "name": { "type": "string" },
          "address_line_1": { "type": "string" },
          "city": { "type": "string" },
          "state": { "type": "string" },
          "zip_code": { "type": "string" },
          "phone": { "type": "string" }
        }
      },
      "CustomPharmacySearchResponse": {
        "type": "object",
        "properties": {
          "pharmacies": { "type": "array", "items": { "$ref": "#/components/schemas/CustomPharmacy" } }
        }
      },
      "GenericOk": {
        "type": "object",
        "properties": {
          "http_code": { "type": "integer" },
          "status": { "type": "string" }
        }
      },
      "PharmacyContact": {
        "type": "object",
        "properties": {
          "full_name": { "type": "string", "nullable": true },
          "email": { "type": "string", "nullable": true },
          "phone_number": { "type": "string", "nullable": true },
          "role": { "type": "string", "nullable": true }
        }
      },
      "PrescriptionTracking": {
        "type": "object",
        "properties": {
          "rx_number": { "type": "string", "nullable": true },
          "tracking_number": { "type": "string", "nullable": true },
          "ship_carrier": { "type": "string", "nullable": true },
          "delivery_service": { "type": "string", "nullable": true },
          "pharmacy_location": { "type": "string", "nullable": true }
        }
      },
      "PrescriptionItem": {
        "type": "object",
        "properties": {
          "prescription_id": { "type": "integer" },
          "drug_name": { "type": "string" },
          "dosage_form_type": { "type": "string" },
          "strength": { "type": "string" },
          "size": { "type": "string" },
          "quantity": { "type": "string" },
          "quantity_units": { "type": "string" },
          "directions": { "type": "string" },
          "duration_days": { "type": "integer" },
          "refill_amount": { "type": "integer" },
          "ndc_number": { "type": "string" },
          "gpi_number": { "type": "string" },
          "schedule_code": { "type": "string", "description": "DEA controlled-substance schedule when applicable." },
          "active_ingredient": { "type": "string" },
          "inactive_ingredient": { "type": "string" },
          "pharmacy_name": { "type": "string" },
          "pharmacy_contacts": { "type": "array", "items": { "$ref": "#/components/schemas/PharmacyContact" } },
          "prescription_tracking": { "type": "array", "items": { "$ref": "#/components/schemas/PrescriptionTracking" } },
          "created_at": { "type": "string", "format": "date-time" }
        }
      },
      "WebhookEvent1ConsultationComplete": {
        "type": "object",
        "description": "Fires for every exam (GFE, Rx, Urgent Care) when the provider finishes the consultation. Carries the verdict and the patient's Q&A.",
        "properties": {
          "event": { "type": "integer", "enum": [1] },
          "reason": { "type": "string", "description": "Free-text reason from the provider. For Approved: typically a generic completion note. For Rejected/NA/Missed: usually describes why." },
          "exam_id": { "type": "string", "description": "Catalog exam identifier as a string in this payload." },
          "exam_url": { "type": "string", "nullable": true, "description": "CloudFront-hosted PDF of the consultation record. URLs include a signed-expiration query param — mirror the file to your own storage if you need long-term access. May be `null` for NA / Missed outcomes." },
          "exam_name": { "type": "string" },
          "exam_status": { "type": "string", "enum": ["Approved", "Rejected", "Deferred to Medical Director", "NA", "Missed"] },
          "rx_status": { "type": "string", "nullable": true, "description": "Prescription status when applicable, e.g. `Pharmacy Confirmed`. `null` for non-Rx exams or non-Approved outcomes." },
          "clinic_name": { "type": "string" },
          "patient_email": { "type": "string" },
          "patient_phone_number": { "type": "string" },
          "patient_exam_id": { "type": "string", "description": "Per-patient instance ID. Use as the idempotency key." },
          "provider_name": { "type": "string", "nullable": true, "description": "Provider who handled the exam. `null` for NA / Missed outcomes where no provider acted." },
          "additional_data": { "type": "string", "description": "Echoed unchanged from `/exam_invite` (stringified JSON)." },
          "questions_answers": { "type": "array", "items": { "$ref": "#/components/schemas/Answer" }, "description": "Screening Q&A. Empty array for Missed outcomes where the patient never started." }
        }
      },
      "WebhookEvent2PrescriptionConfirmed": {
        "type": "object",
        "description": "Fires only for Rx exams, after event 1, when the treatment order has been written and confirmed at the pharmacy. `prescription` is an array — an exam can include multiple treatment items (e.g. HRT often returns 2+).",
        "properties": {
          "event": { "type": "integer", "enum": [2] },
          "exam_id": { "type": "integer", "description": "Catalog exam identifier. Returned as an integer on event 2 (unlike event 1's string)." },
          "exam_name": { "type": "string" },
          "exam_status": { "type": "string" },
          "exam_url": { "type": "string", "description": "CloudFront-hosted PDF of the consultation record." },
          "rx_status": { "type": "string" },
          "clinic_name": { "type": "string" },
          "patient_email": { "type": "string" },
          "patient_phone_number": { "type": "string" },
          "patient_exam_id": { "type": "integer", "description": "Returned as an integer on event 2." },
          "provider_name": { "type": "string" },
          "prescribed_os_id": { "type": "integer", "description": "Order set ID the provider prescribed." },
          "prescription": { "type": "array", "items": { "$ref": "#/components/schemas/PrescriptionItem" }, "description": "One entry per drug. May include controlled substances (`schedule_code` set, e.g. `C3`) — storage of these fields is regulated." },
          "questions_answers": { "type": "array", "items": { "$ref": "#/components/schemas/Answer" }, "description": "Same Q&A shape as event 1." }
        }
      },
      "WebhookEvent3PrescriptionTracking": {
        "type": "object",
        "description": "Fires when the partner pharmacy ships the treatment. Does NOT fire for custom-pharmacy / urgent-care exams — those pharmacies handle their own tracking outside Qualiphy.",
        "properties": {
          "event": { "type": "integer", "enum": [3] },
          "patient_exam_id": { "type": "string" },
          "prescription_id": { "type": "string", "description": "Matches a `prescription[].prescription_id` from event 2." },
          "tracking_number": { "type": "string" },
          "delivery_service": { "type": "string", "description": "e.g. `Overnight`, `Standard`." },
          "pharmacy_location": { "type": "string" },
          "order_id": { "type": "string" }
        }
      },
      "WebhookEvent": {
        "oneOf": [
          { "$ref": "#/components/schemas/WebhookEvent1ConsultationComplete" },
          { "$ref": "#/components/schemas/WebhookEvent2PrescriptionConfirmed" },
          { "$ref": "#/components/schemas/WebhookEvent3PrescriptionTracking" }
        ],
        "discriminator": {
          "propertyName": "event"
        }
      }
    }
  }
}
