{"openapi":"3.1.0","info":{"title":"Wyndly API","description":"\nThe Wyndly API enables healthcare providers and diagnostic partners\nto integrate allergy testing and treatment workflows into their\nexisting systems.\n\nWith this API, you can submit test results to Wyndly, receive\nrecommended allergy treatment mixes, and even ship treatment once\nauthorized by your care providers.\n\n**What you can do:**\n- Submit allergen diagnostic test results for patients\n- Receive real-time webhook notifications when treatment recommendations are ready\n- Track shipments as immunotherapy treatments are delivered to patients\n\n**Why integrate with Wyndly:**\n- Streamline the path from allergy diagnosis to treatment\n- Automate handoffs between your system and Wyndly's clinical team\n- Keep your systems in sync with real-time event notifications\n- Eliminate manual data entry and reduce errors\n\n---\n\n## Integration Flow\n\n![Integration Flow Diagram](https://wyndly-public-artifacts.s3.us-east-2.amazonaws.com/api-docs/integration-flow.png)\n\n---\n\n## Quick Start\n\n### Authentication\nAll API requests require an API key passed in the `X-Api-Key` header.\n\n### Base URL\nAll endpoints are prefixed with `/api/v1`.\n\n---\n\n## Examples\n\n### 1. Configure Your Webhook Endpoint\nFirst, register one or more webhook URLs to receive event notifications:\n\n```bash\ncurl -X POST \"https://api.wyndly.com/api/v1/webhooks\" \\\n  -H \"X-Api-Key: your-api-key\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"webhook_url\": \"https://your-server.com/webhooks\",\n    \"webhook_secret\": \"your-secret-for-signature-verification\"\n  }'\n```\n\n### 2. Submit Diagnostic Results\nSubmit allergen test results for a new patient:\n\n```bash\ncurl -X POST \"https://api.wyndly.com/api/v1/diagnostics\" \\\n  -H \"X-Api-Key: your-api-key\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"provider\": {\n      \"name\": \"Dr. Smith\",\n      \"npi\": 1234567890\n    },\n    \"patient_info\": {\n      \"name\": \"John Doe\",\n      \"dob\": \"1990-01-15\",\n      \"email\": \"john@example.com\",\n      \"phone_number\": \"5551234567\",\n      \"address\": {\n        \"primary_line\": \"123 Main St\",\n        \"city\": \"Austin\",\n        \"state\": \"TX\",\n        \"zip_code\": \"78701\"\n      }\n    },\n    \"results\": [\n      {\"allergen_name\": \"cat_dander\", \"value\": 3.5},\n      {\"allergen_name\": \"dog_dander\", \"value\": 1.2},\n      {\"allergen_name\": \"timothy_grass\", \"value\": 4.8}\n    ],\n    \"measurement_unit\": \"kU/L\",\n    \"intake\": {\n      \"triggers\": [\"cat\", \"pollen\"],\n      \"seasons\": [\"spring\", \"fall\"],\n      \"tried_allergy_meds\": true,\n      \"allergy_medications\": [\"otc_antihistamines\", \"nasal_treatments\"],\n      \"contraindications_to_slit\": false\n    }\n  }'\n```\n\n### 3. Search for Patients\nFind patients by name, DOB, or email:\n\n```bash\ncurl -X POST \"https://api.wyndly.com/api/v1/patients/search\" \\\n  -H \"X-Api-Key: your-api-key\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\": \"John\", \"limit\": 10}'\n```\n\n### 4. Simulate Webhooks (Sandbox Only)\nTest your webhook integration by triggering simulated events. With no body, the\nfull canonical lifecycle is fired — `recommendation_ready` → `ready_for_shipment`\n→ `shipment_shipped` (kit dispatched) → `shipment_delivered` (member received it):\n\n```bash\ncurl -X POST \"https://api.wyndly.com/api/v1/sandbox/webhooks/simulate\" \\\n  -H \"X-Api-Key: your-api-key\"\n```\n\nOr target a single event with a specific scenario:\n\n```bash\ncurl -X POST \"https://api.wyndly.com/api/v1/sandbox/webhooks/simulate\" \\\n  -H \"X-Api-Key: your-api-key\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"event_type\": \"shipment_delivered\", \"scenario\": \"needs_renewal\"}'\n```\n\n`treatment_declined` is also simulatable this way — in production it fires\ninstead of `recommendation_ready` when a diagnostic yields no recommended\ntreatment (tested negative, no reported triggers/seasons).\n\nTo chain `/simulate` into `/shipments/request`, pass `persist=true` so the\nreturned `patient_id`/`prescription_id` exist as real sandbox rows:\n\n```bash\ncurl -X POST \"https://api.wyndly.com/api/v1/sandbox/webhooks/simulate\" \\\n  -H \"X-Api-Key: your-api-key\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"event_type\": \"ready_for_shipment\", \"scenario\": \"rx_valid\", \"persist\": true}'\n```\n\nThe response `context` object surfaces the IDs to reuse downstream.\n\n**Full walkthrough:** expand the `POST /sandbox/webhooks/simulate` operation\nbelow for the complete partner integration guide — every scenario, the\n`persist=true` end-to-end chain into `/shipments/request`, full error table,\nand a Python example.\n\n---\n\n## Error Handling\n\nAll errors return a consistent JSON structure with an `error_code`\nyou can use for programmatic handling, plus a human-readable `message`:\n\n```json\n{\n  \"error_code\": \"VALIDATION_ERROR\",\n  \"message\": \"Request validation failed\",\n  \"details\": [...]\n}\n```\n\n### HTTP Status Codes\n\n| Status | Meaning |\n|--------|---------|\n| **400** | Bad request — missing required fields or invalid values |\n| **401** | Unauthorized — invalid or missing API key |\n| **403** | Forbidden — action not allowed (e.g., sandbox-only endpoint) |\n| **404** | Not found — resource doesn't exist or you don't have access |\n| **422** | Validation error — request body failed schema validation |\n| **500** | Server error — something went wrong on our end |\n\n### Common Error Codes\n\n| Error Code | Description |\n|------------|-------------|\n| `INVALID_API_KEY` | API key is missing, invalid, or inactive |\n| `VALIDATION_ERROR` | Request body doesn't match expected schema |\n| `PATIENT_NOT_FOUND` | The specified patient_id doesn't exist |\n| `DIAGNOSTIC_NOT_FOUND` | The specified diagnostic ID doesn't exist |\n| `NO_WEBHOOK_CONFIGURED` | No webhook URL has been registered |\n| `SANDBOX_ONLY` | This endpoint is only available in sandbox mode |\n\n### Design Philosophy\n\nWe use **specific error codes** so you can handle errors\nprogrammatically without parsing message text. The `error_code` is\nstable and won't change, while `message` may be updated for clarity.\n\nFor validation errors (422), the `details` array tells you exactly\nwhich fields failed and why—making it easy to surface actionable\nfeedback to your users.\n","version":"1.0.0"},"paths":{"/api/v1/diagnostics":{"post":{"tags":["Diagnostics"],"summary":"Submit diagnostic results","description":"Submit allergen test results from external providers for review","operationId":"create_diagnostic_api_v1_diagnostics_post","parameters":[{"name":"x-api-key","in":"header","required":true,"schema":{"type":"string","description":"API Key","title":"X-Api-Key"},"description":"API Key"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiagnosticRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiagnosticResponse"}}}},"401":{"description":"Authentication failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/diagnostics/{diagnostic_id}":{"get":{"tags":["Diagnostics"],"summary":"Get diagnostic request details","description":"Retrieve details of a specific diagnostic request by ID","operationId":"get_diagnostic_api_v1_diagnostics__diagnostic_id__get","parameters":[{"name":"diagnostic_id","in":"path","required":true,"schema":{"type":"string","title":"Diagnostic Id"}},{"name":"x-api-key","in":"header","required":true,"schema":{"type":"string","description":"API Key","title":"X-Api-Key"},"description":"API Key"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiagnosticDetailResponse"}}}},"401":{"description":"Authentication failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Diagnostic not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/patients/search":{"post":{"tags":["Patients"],"summary":"Search patients","description":"Search for patients belonging to the authenticated partner with optional filters","operationId":"search_patients_api_v1_patients_search_post","parameters":[{"name":"x-api-key","in":"header","required":true,"schema":{"type":"string","description":"API Key","title":"X-Api-Key"},"description":"API Key"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatientSearchRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatientsListResponse"}}}},"401":{"description":"Authentication failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/shipments/upcoming":{"post":{"tags":["Shipments"],"summary":"List upcoming shipments","description":"Get a list of upcoming shipments for the authenticated partner","operationId":"get_upcoming_shipments_api_v1_shipments_upcoming_post","parameters":[{"name":"x-api-key","in":"header","required":true,"schema":{"type":"string","description":"API Key","title":"X-Api-Key"},"description":"API Key"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShipmentSearchRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShipmentsListResponse"}}}},"401":{"description":"Authentication failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/shipments/request":{"post":{"tags":["Shipments"],"summary":"Request a shipment (v2)","description":"Request the next refill shipment for a patient. Returns 409 if the Rx is expired. Optional `shipment_type` cadence and `address` override are persisted onto the order and echoed in the response. One request per patient per UTC day; duplicate same-day requests return the existing order with `idempotent: true`.","operationId":"request_shipment_api_v1_shipments_request_post","parameters":[{"name":"x-api-key","in":"header","required":true,"schema":{"type":"string","description":"API Key","title":"X-Api-Key"},"description":"API Key"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShipmentRequestBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShipmentRequestResponse"}}}},"401":{"description":"Authentication failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Patient not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Prescription expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RxExpiredError"}}}}}}},"/api/v1/prescriptions/reauth-link":{"post":{"tags":["Prescriptions"],"summary":"Generate a fresh JotForm reauth link","description":"Return a pre-filled JotForm URL the partner can hand back to the patient to re-authorize their prescription. Use this when refills are exhausted or the prescription is expiring. The same URL is delivered automatically in the rx_renewal_needed webhook; this endpoint lets partners pull it on demand.","operationId":"get_reauth_link_api_v1_prescriptions_reauth_link_post","parameters":[{"name":"x-api-key","in":"header","required":true,"schema":{"type":"string","description":"API Key","title":"X-Api-Key"},"description":"API Key"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReauthLinkRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReauthLinkResponse"}}}},"401":{"description":"Authentication failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Patient not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/sandbox/webhooks/simulate":{"post":{"tags":["Sandbox"],"summary":"Simulate webhook events","description":"# Sandbox `/simulate` — partner integration guide\n\nThis guide walks through testing your webhook integration with Wyndly using the sandbox `/simulate` endpoint. It assumes you've already configured a webhook URL on the partner dashboard and you have a sandbox API key.\n\n**Audience:** developers integrating with the Wyndly partner API for the first time.\n**Sandbox base URL:** `https://sandbox-api.wyndly.com`\n\n---\n\n## Table of contents\n\n1. [What does `/simulate` do?](#what-does-simulate-do)\n2. [Before you start](#before-you-start)\n3. [Quick start: fire the full lifecycle](#quick-start-fire-the-full-lifecycle)\n4. [Targeting a single event](#targeting-a-single-event)\n5. [Scenarios](#scenarios)\n6. [End-to-end testing with `persist=true`](#end-to-end-testing-with-persisttrue)\n7. [Request body reference](#request-body-reference)\n8. [Response shape](#response-shape)\n9. [Error codes](#error-codes)\n10. [Common recipes](#common-recipes)\n11. [Python example](#python-example)\n12. [Troubleshooting](#troubleshooting)\n\n---\n\n## What does `/simulate` do?\n\n`POST /api/v1/sandbox/webhooks/simulate` fires one or more partner webhook events at your configured webhook URL(s), with realistic payloads. It lets you:\n\n- Verify your webhook receiver handles each event type without waiting for real patient activity.\n- Test HMAC signature verification on the receiving side.\n- Drive scenario-based payload values (e.g., \"prescription expired\", \"1 refill left\") to exercise different code paths in your integration.\n- Optionally seed real patient + prescription rows in the sandbox database so the IDs returned by `/simulate` can be passed straight into other endpoints like `/shipments/request`.\n\nThe endpoint is **sandbox-only**. Calling it against `https://api.wyndly.com` (production) returns `403 SANDBOX_ONLY`.\n\n---\n\n## Before you start\n\nYou need:\n\n1. **A sandbox API key.** Use this in the `X-Api-Key` header.\n2. **At least one active webhook URL** configured on your partner record. `/simulate` returns `404 NO_WEBHOOK_CONFIGURED` if there is none.\n3. **A webhook receiver that responds with 2xx.** A test service like [webhook.site](https://webhook.site) works well for ad-hoc testing.\n\nTo configure a webhook URL in sandbox:\n\n```bash\ncurl -X POST \"https://sandbox-api.wyndly.com/api/v1/webhooks\" \\\n  -H \"X-Api-Key: $WYNDLY_SANDBOX_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"webhook_url\": \"https://webhook.site/your-unique-token\",\n    \"webhook_secret\": \"any-string-you-pick\"\n  }'\n```\n\nThe `webhook_secret` is used to HMAC-sign every webhook delivery so your receiver can verify authenticity. You'll see the signature in the `X-Webhook-Signature` header.\n\n---\n\n## Quick start: fire the full lifecycle\n\nThe simplest possible call — no body, no flags — fires the full happy-path lifecycle of four events in order:\n\n```bash\ncurl -X POST \"https://sandbox-api.wyndly.com/api/v1/sandbox/webhooks/simulate\" \\\n  -H \"X-Api-Key: $WYNDLY_SANDBOX_API_KEY\"\n```\n\nThe events fire in this order:\n\n1. `recommendation_ready` — clinician approved a treatment plan; partner can present it to the patient.\n2. `ready_for_shipment` — prescription approved; partner can request a shipment.\n3. `shipment_shipped` — kit dispatched; carries the tracking number + carrier so you can notify the member and stamp tracking.\n4. `shipment_delivered` — carrier confirmed the member received their medication.\n\nA successful response looks like:\n\n```json\n{\n  \"success\": true,\n  \"webhook_urls\": [\"https://webhook.site/your-unique-token\"],\n  \"results\": [\n    { \"event_type\": \"recommendation_ready\", \"success\": true, \"status_code\": 200, \"payload\": {...}, \"...\" },\n    { \"event_type\": \"ready_for_shipment\",   \"success\": true, \"status_code\": 200, \"payload\": {...}, \"...\" },\n    { \"event_type\": \"shipment_shipped\",     \"success\": true, \"status_code\": 200, \"payload\": {...}, \"...\" },\n    { \"event_type\": \"shipment_delivered\",   \"success\": true, \"status_code\": 200, \"payload\": {...}, \"...\" }\n  ],\n  \"context\": {\n    \"event_types_fired\": [\"recommendation_ready\", \"ready_for_shipment\", \"shipment_shipped\", \"shipment_delivered\"],\n    \"scenario\": \"rx_valid\",\n    \"patient_id\": 4242,\n    \"prescription_id\": 9876,\n    \"shipment_id\": \"sandbox-ord-a1b2c3d4e5f6\",\n    \"diagnostic_request_id\": \"test-9c8f6a18-...\",\n    \"persisted\": false\n  },\n  \"message\": \"Delivered 4 webhook event(s) to 1 endpoint(s)\"\n}\n```\n\nThe `results` array tells you each delivery's outcome (status code, success flag). The `context` object surfaces the IDs threaded through every fired event — useful for chaining downstream calls (more on that below).\n\n---\n\n## Targeting a single event\n\nWhen you're iterating on one specific event handler, fire just that event by setting `event_type`:\n\n```bash\ncurl -X POST \"https://sandbox-api.wyndly.com/api/v1/sandbox/webhooks/simulate\" \\\n  -H \"X-Api-Key: $WYNDLY_SANDBOX_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"event_type\": \"shipment_cancelled\"}'\n```\n\nValid `event_type` values:\n\n| Event                    | What it represents                                                                  |\n|--------------------------|-------------------------------------------------------------------------------------|\n| `recommendation_ready`   | Treatment plan approved by clinical team.                                           |\n| `treatment_declined`     | Diagnostic completed but no treatment is recommended. Fires **instead of** `recommendation_ready` in production. `reason` is `no_positive_results_and_no_reported_symptoms` (nothing recommended and no reported triggers/seasons) or `no_positive_lab_results` (a partner requiring a validated lab result had a negative lab panel, even if symptoms were reported). |\n| `ready_for_shipment`     | Prescription approved — partner can call `/shipments/request`.                      |\n| `shipment_shipped`       | Kit dispatched / in transit (status: `in_transit`). Carries tracking number + carrier. |\n| `shipment_delivered`     | Carrier confirmed delivery (terminal status: `delivered`). Formerly named `shipment_shipped`. |\n| `shipment_cancelled`     | Shipment did not complete (terminal status: `cancelled` — returned/failed/expired). |\n| `rx_renewal_needed`      | Prescription is exhausted or expiring soon. Partner should re-engage the clinician. |\n\n### Companion `rx_renewal_needed` events\n\nWhen you target a terminal shipment event (`shipment_delivered` or `shipment_cancelled`) and the scenario indicates renewal is needed, `/simulate` automatically fires a companion `rx_renewal_needed` event right after the terminal. This mirrors what production does — your receiver should be prepared to handle the pair. The dispatch event (`shipment_shipped`) never carries a renewal companion — it's tracking-only.\n\nExample:\n\n```bash\ncurl -X POST \"https://sandbox-api.wyndly.com/api/v1/sandbox/webhooks/simulate\" \\\n  -H \"X-Api-Key: $WYNDLY_SANDBOX_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"event_type\": \"shipment_delivered\", \"scenario\": \"needs_renewal\"}'\n```\n\nYou'll see two results in the response: `shipment_delivered` followed by `rx_renewal_needed`, both carrying the same `patient_id`.\n\n---\n\n## Scenarios\n\nScenarios control the prescription-state values inside the fired payloads (and the database row state if you use `persist=true` — see below).\n\n| Scenario        | refills_remaining | days_until_expiry | What it tests                                                                                              |\n|-----------------|------------------:|------------------:|------------------------------------------------------------------------------------------------------------|\n| `rx_valid`      | 4                 | ~365              | Happy path. Fresh prescription, plenty of refills. Default if you omit `scenario`.                         |\n| `refills_low`   | 1                 | ~335              | Patient has one refill left, no renewal needed yet. Tests \"low refill\" UX without renewal flow.            |\n| `needs_renewal` | 0                 | ~265              | Refills exhausted. Triggers `needs_renewal=true` in the payload and a companion `rx_renewal_needed` event. |\n| `rx_expired`    | 0                 | < 0               | Prescription past its 366-day window. `rx_valid=false` and `/shipments/request` returns `rx_expired`.      |\n\nUse the scenario field whenever you want to test how your code reacts to different prescription states. For example, your \"needs renewal\" UI should appear when the payload's `rx_renewal_needed` field is `true`:\n\n```bash\n# Trigger the renewal banner\ncurl -X POST \"https://sandbox-api.wyndly.com/api/v1/sandbox/webhooks/simulate\" \\\n  -H \"X-Api-Key: $WYNDLY_SANDBOX_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"event_type\": \"shipment_delivered\", \"scenario\": \"needs_renewal\"}'\n```\n\n---\n\n## End-to-end testing with `persist=true`\n\nBy default `/simulate` is a \"pure\" webhook firehose — the IDs in the payloads are synthetic and only useful for testing your receiver. If you want to chain calls (e.g., call `/shipments/request` with a `patient_id` from a `ready_for_shipment` payload), set `persist: true` and `/simulate` will create real rows in the sandbox database so the IDs work end-to-end.\n\n### The flow\n\n```\n┌──────────────────────────────┐\n│  POST /simulate              │\n│  { event_type: ready_for…,   │ ──── creates sandbox rows\n│    scenario: rx_valid,       │       (patients, prescriptions,\n│    persist: true }           │        backfill drops_orders)\n└──────────────┬───────────────┘\n               │\n               │ returns context.patient_id, context.prescription_id\n               ▼\n┌──────────────────────────────┐\n│  POST /shipments/request     │\n│  { patient_id, prescription_id }\n└──────────────┬───────────────┘\n               │\n               │ returns order_id (the real shipment_id)\n               ▼\n┌──────────────────────────────┐\n│  POST /simulate              │\n│  { event_type: shipment_…,   │\n│    patient_id,               │\n│    prescription_id,          │\n│    shipment_id (the real one)│\n└──────────────────────────────┘\n```\n\n### Step 1 — seed the patient and prescription\n\n```bash\ncurl -X POST \"https://sandbox-api.wyndly.com/api/v1/sandbox/webhooks/simulate\" \\\n  -H \"X-Api-Key: $WYNDLY_SANDBOX_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"event_type\": \"ready_for_shipment\",\n    \"scenario\": \"rx_valid\",\n    \"persist\": true\n  }'\n```\n\nGrab `context.patient_id` and `context.prescription_id` from the response. Those rows now exist in `sandbox.patients` and `sandbox.prescriptions`.\n\n### Step 2 — request a real shipment\n\n```bash\ncurl -X POST \"https://sandbox-api.wyndly.com/api/v1/shipments/request\" \\\n  -H \"X-Api-Key: $WYNDLY_SANDBOX_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"patient_id\": <from step 1>,\n    \"prescription_id\": <from step 1>\n  }'\n```\n\nThis goes through the real refill logic. Returns `order_id` — that's the real `shipment_id` for the next step.\n\n### Step 3 — fire the terminal event with the real shipment id\n\n```bash\ncurl -X POST \"https://sandbox-api.wyndly.com/api/v1/sandbox/webhooks/simulate\" \\\n  -H \"X-Api-Key: $WYNDLY_SANDBOX_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"event_type\": \"shipment_delivered\",\n    \"patient_id\": <from step 1>,\n    \"prescription_id\": <from step 1>,\n    \"shipment_id\": <order_id from step 2>\n  }'\n```\n\nNow your `shipment_delivered` handler sees a `shipment_id` that exists in the sandbox database — useful if your integration looks up shipments by ID after receiving the webhook. (The dispatch `shipment_shipped` event works the same way; swap the `event_type`.)\n\n### What persist mode actually writes\n\nWhen `persist=true`:\n\n- **`sandbox.patients`** row created (auto-assigned id; bound to your `partner_id` from the API key).\n- **`sandbox.prescriptions`** row created with `date_approved` matching the scenario.\n- **`sandbox.drops_orders`** backfill rows added when the scenario implies prior refills used (3 rows for `refills_low`, 4 for `needs_renewal`/`rx_expired`, 0 for `rx_valid`). These are needed because refill counts are derived from `COUNT(drops_orders)`, not a column on `prescriptions`.\n\nThe endpoint never writes the **terminal shipment row** itself — that's what `/shipments/request` is for in step 2.\n\n### What persist mode does NOT support\n\n- Choosing the exact `id` for a brand-new patient/prescription. The columns are `GENERATED ALWAYS AS IDENTITY`. If you pass `patient_id: 9999` and that row doesn't exist, `/simulate` creates a row with a database-assigned id (not 9999) and returns the new id in `context`. If `9999` exists and belongs to your partner, it's reused as-is.\n- Mutating existing rows to match a scenario. If you pass a `prescription_id` that exists but whose state doesn't match the requested scenario, you get `409 SCENARIO_MISMATCH`.\n- Using `persist=true` with `event_type: \"treatment_declined\"`. A declined treatment has no prescription or shipment flow to seed, so this returns `400 PERSIST_UNSUPPORTED_FOR_EVENT`.\n- Running outside the sandbox stage. `persist=true` requires `stage=sandbox` (the only stage with the right `search_path`). Calling it elsewhere returns `400 PERSIST_REQUIRES_SANDBOX_STAGE`.\n\n---\n\n## Request body reference\n\nAll fields are optional. The endpoint also accepts an empty body or no body at all.\n\n| Field                   | Type    | Default                     | Description |\n|-------------------------|---------|-----------------------------|-------------|\n| `event_type`            | string  | — (fires full lifecycle)    | One of `recommendation_ready`, `treatment_declined`, `ready_for_shipment`, `shipment_shipped`, `shipment_delivered`, `shipment_cancelled`, `rx_renewal_needed`. Omit to fire the full lifecycle `recommendation_ready` → `ready_for_shipment` → `shipment_shipped` → `shipment_delivered`. |\n| `scenario`              | string  | `rx_valid`                  | One of `rx_valid`, `refills_low`, `needs_renewal`, `rx_expired`. Drives payload values and (when `persist=true`) DB row state. |\n| `patient_id`            | integer | auto-generated synthetic    | Use a specific `patient_id`. With `persist=true`, this must belong to your partner if it exists; otherwise a new row is created with an auto-assigned id. |\n| `prescription_id`       | integer | auto-generated synthetic    | Use a specific `prescription_id`. Same ownership/identity rules as `patient_id`. |\n| `shipment_id`           | string  | auto-generated synthetic    | Use a specific `shipment_id` (e.g., one you got back from `/shipments/request`). |\n| `diagnostic_request_id` | string  | auto-generated synthetic    | Use a specific `diagnostic_request_id`. |\n| `persist`               | boolean | `false`                     | When `true`, write rows into the sandbox schema so the returned IDs work against other endpoints. Sandbox stage only. |\n\n---\n\n## Response shape\n\n```json\n{\n  \"success\": true,\n  \"webhook_urls\": [\"https://...\"],\n  \"results\": [\n    {\n      \"webhook_id\": 1,\n      \"webhook_url\": \"https://...\",\n      \"event_type\": \"ready_for_shipment\",\n      \"success\": true,\n      \"status_code\": 200,\n      \"message\": \"Delivered successfully to https://... (status 200)\",\n      \"payload\": { /* the exact payload that was POSTed to your URL */ }\n    }\n  ],\n  \"context\": {\n    \"event_types_fired\": [\"ready_for_shipment\"],\n    \"scenario\": \"rx_valid\",\n    \"patient_id\": 4242,\n    \"prescription_id\": 9876,\n    \"shipment_id\": \"sandbox-ord-...\",\n    \"diagnostic_request_id\": \"test-...\",\n    \"persisted\": false\n  },\n  \"message\": \"Delivered 1 webhook event(s) to 1 endpoint(s)\"\n}\n```\n\n| Field         | Description |\n|---------------|-------------|\n| `success`     | `true` only if **all** deliveries succeeded (every result has `success=true`). |\n| `webhook_urls`| List of the active webhook URLs this call sent to. |\n| `results`     | One entry per `(webhook URL, event)` pair. Each entry shows the exact payload, delivery status, and the webhook receiver's HTTP response code. |\n| `context`     | The IDs and scenario used. Copy `patient_id`/`prescription_id`/`shipment_id` from here into follow-up calls. |\n| `message`     | Human-readable summary. |\n\n**Important:** if `results[].success` is `false` for any entry, `success` at the top level is `false` too — but the HTTP status is still 200. You'll see the failure reason in `message`. The endpoint only returns 4xx/5xx for errors in the request itself, not delivery failures at the partner end.\n\n---\n\n## Error codes\n\n| HTTP | `error_code`                       | Why                                                                                   |\n|-----:|------------------------------------|---------------------------------------------------------------------------------------|\n| 400  | `PERSIST_REQUIRES_SANDBOX_STAGE`   | `persist=true` was used outside the `sandbox` stage.                                  |\n| 400  | `PERSIST_UNSUPPORTED_FOR_EVENT`    | `persist=true` was used with `event_type: \"treatment_declined\"`, which has no prescription or shipment flow to seed. |\n| 401  | `INVALID_API_KEY`                  | API key is missing, unknown, or inactive.                                             |\n| 403  | `SANDBOX_ONLY`                     | Called outside dev/sandbox. The endpoint is not available in staging or production.   |\n| 403  | `PATIENT_NOT_OWNED`                | `persist=true` with a `patient_id` that belongs to a different partner.               |\n| 403  | `RX_NOT_OWNED`                     | `persist=true` with a `prescription_id` that's not tied to the resolved patient.      |\n| 404  | `NO_WEBHOOK_CONFIGURED`            | Your partner record has no active webhook URLs.                                       |\n| 409  | `SCENARIO_MISMATCH`                | Provided `prescription_id` exists but its live state doesn't match the requested `scenario`. Pass a different id or omit it. |\n| 422  | (Pydantic)                         | Body validation error (e.g., unknown `event_type` or `scenario`).                     |\n| 500  | `SANDBOX_NOT_PROVISIONED`          | The sandbox schema is missing required tables. Should not happen in normal operation. |\n| 500  | `INTERNAL_ERROR`                   | Unexpected server error. Check CloudWatch logs or contact Wyndly.                     |\n\n---\n\n## Common recipes\n\n### Test your `recommendation_ready` handler in isolation\n\n```bash\ncurl -X POST \"https://sandbox-api.wyndly.com/api/v1/sandbox/webhooks/simulate\" \\\n  -H \"X-Api-Key: $WYNDLY_SANDBOX_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"event_type\": \"recommendation_ready\"}'\n```\n\n### Test your `treatment_declined` handler\n\nIn production this event fires **instead of** `recommendation_ready` when no treatment is recommended — either the patient tested negative and reported no triggers/seasons (`reason: no_positive_results_and_no_reported_symptoms`), or, for a partner configured to require a validated lab result, the lab panel showed no positive allergen even if symptoms were reported (`reason: no_positive_lab_results`). There is no treatment plan and no JotForm link.\n\n```bash\ncurl -X POST \"https://sandbox-api.wyndly.com/api/v1/sandbox/webhooks/simulate\" \\\n  -H \"X-Api-Key: $WYNDLY_SANDBOX_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"event_type\": \"treatment_declined\"}'\n```\n\n### Verify your receiver shows \"1 refill left\" warnings\n\n```bash\ncurl -X POST \"https://sandbox-api.wyndly.com/api/v1/sandbox/webhooks/simulate\" \\\n  -H \"X-Api-Key: $WYNDLY_SANDBOX_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"event_type\": \"shipment_delivered\", \"scenario\": \"refills_low\"}'\n```\n\n### Verify your receiver handles the renewal-needed companion event\n\n```bash\ncurl -X POST \"https://sandbox-api.wyndly.com/api/v1/sandbox/webhooks/simulate\" \\\n  -H \"X-Api-Key: $WYNDLY_SANDBOX_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"event_type\": \"shipment_delivered\", \"scenario\": \"needs_renewal\"}'\n```\n\nYour receiver should see two POSTs: a `shipment_delivered` followed by an `rx_renewal_needed`.\n\n### Verify your `/shipments/request` retry logic when the prescription is expired\n\n```bash\n# 1) Seed an expired prescription\ncurl -X POST \"https://sandbox-api.wyndly.com/api/v1/sandbox/webhooks/simulate\" \\\n  -H \"X-Api-Key: $WYNDLY_SANDBOX_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"event_type\": \"ready_for_shipment\", \"scenario\": \"rx_expired\", \"persist\": true}'\n\n# Note the patient_id/prescription_id from the response.\n\n# 2) Try to request a shipment — should fail with 409 rx_expired\ncurl -X POST \"https://sandbox-api.wyndly.com/api/v1/shipments/request\" \\\n  -H \"X-Api-Key: $WYNDLY_SANDBOX_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"patient_id\": <from step 1>, \"prescription_id\": <from step 1>}'\n```\n\n---\n\n## Python example\n\n```python\nimport os\nimport requests\n\nBASE_URL = \"https://sandbox-api.wyndly.com\"\nAPI_KEY = os.environ[\"WYNDLY_SANDBOX_API_KEY\"]\n\ndef simulate(**body):\n    response = requests.post(\n        f\"{BASE_URL}/api/v1/sandbox/webhooks/simulate\",\n        headers={\"X-Api-Key\": API_KEY, \"Content-Type\": \"application/json\"},\n        json=body,\n    )\n    response.raise_for_status()\n    return response.json()\n\n\n# Quick smoke test: fire the full lifecycle.\nresult = simulate()\nprint(f\"Fired: {result['context']['event_types_fired']}\")\nprint(f\"Patient ID used: {result['context']['patient_id']}\")\n\n\n# Test a single event with a scenario.\nresult = simulate(event_type=\"shipment_cancelled\", scenario=\"needs_renewal\")\nevents = [r[\"event_type\"] for r in result[\"results\"]]\nprint(f\"Fired: {events}\")  # ['shipment_cancelled', 'rx_renewal_needed']\n\n\n# End-to-end: persist → /shipments/request → terminal event.\nseed = simulate(event_type=\"ready_for_shipment\", scenario=\"rx_valid\", persist=True)\npatient_id = seed[\"context\"][\"patient_id\"]\nprescription_id = seed[\"context\"][\"prescription_id\"]\n\nship = requests.post(\n    f\"{BASE_URL}/api/v1/shipments/request\",\n    headers={\"X-Api-Key\": API_KEY, \"Content-Type\": \"application/json\"},\n    json={\"patient_id\": patient_id, \"prescription_id\": prescription_id},\n)\nship.raise_for_status()\nshipment_id = ship.json()[\"order_id\"]\n\nsimulate(\n    event_type=\"shipment_delivered\",\n    patient_id=patient_id,\n    prescription_id=prescription_id,\n    shipment_id=shipment_id,\n)\nprint(f\"Fired terminal event for real shipment {shipment_id}\")\n```\n\n---\n\n## Troubleshooting\n\n**My webhook receiver isn't getting any POSTs.**\nCheck `results[]` in the response. If `success` is `false` and the error is `Request failed: ...`, your URL is unreachable. If `status_code` is `4xx` or `5xx`, your receiver responded but rejected the POST.\n\n**I'm getting 404 NO_WEBHOOK_CONFIGURED.**\nYou haven't configured a webhook URL on your partner record. See [Before you start](#before-you-start).\n\n**I'm getting 422 even though my body looks right.**\nCheck spelling on `event_type` and `scenario`. The valid values are listed in [Targeting a single event](#targeting-a-single-event) and [Scenarios](#scenarios) — anything else is rejected by validation.\n\n**`persist=true` is returning `SCENARIO_MISMATCH`.**\nYou passed a `prescription_id` that exists but whose live state doesn't match the requested `scenario`. The endpoint never silently mutates existing data. Either pick a different scenario, pick a different `prescription_id`, or omit `prescription_id` entirely so a fresh row is created.\n\n**My follow-up call to `/shipments/request` returns `rx_expired` even though I passed `scenario: rx_valid`.**\nMake sure you used `persist=true` on the `/simulate` call that returned the IDs. Without `persist=true`, the IDs are synthetic and don't exist in the database.\n\n**I want to clean up my test data.**\nSandbox is throwaway by convention — there is no cleanup endpoint. Test rows accumulate over time and are wiped periodically by Wyndly. Don't write integration tests that assume a clean schema.","operationId":"simulate_webhook_api_v1_sandbox_webhooks_simulate_post","parameters":[{"name":"x-api-key","in":"header","required":true,"schema":{"type":"string","description":"API Key for partner identification","title":"X-Api-Key"},"description":"API Key for partner identification"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimulateWebhookRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimulateWebhookResponse"}}}},"401":{"description":"Authentication failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden - sandbox mode only","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/webhooks":{"post":{"tags":["Webhooks"],"summary":"Create a webhook configuration","description":"Create a new webhook configuration for the authenticated partner","operationId":"create_webhook_api_v1_webhooks_post","parameters":[{"name":"x-api-key","in":"header","required":true,"schema":{"type":"string","description":"API Key","title":"X-Api-Key"},"description":"API Key"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookCreateRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookResponse"}}}},"401":{"description":"Authentication failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"get":{"tags":["Webhooks"],"summary":"List webhook configurations","description":"List webhook configurations for the authenticated partner","operationId":"list_webhooks_api_v1_webhooks_get","parameters":[{"name":"x-api-key","in":"header","required":true,"schema":{"type":"string","description":"API Key","title":"X-Api-Key"},"description":"API Key"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookListResponse"}}}},"401":{"description":"Authentication failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/webhooks/{webhook_id}":{"delete":{"tags":["Webhooks"],"summary":"Delete a webhook configuration","description":"Deactivate a webhook configuration for the authenticated partner","operationId":"delete_webhook_api_v1_webhooks__webhook_id__delete","parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"integer","title":"Webhook Id"}},{"name":"x-api-key","in":"header","required":true,"schema":{"type":"string","description":"API Key","title":"X-Api-Key"},"description":"API Key"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteResponse"}}}},"401":{"description":"Authentication failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Webhook not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}},"components":{"schemas":{"Address":{"properties":{"primary_line":{"type":"string","maxLength":255,"minLength":1,"title":"Primary Line","description":"Primary address line (street address)"},"secondary_line":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Secondary Line","description":"Secondary address line (apt, suite, etc.)"},"city":{"type":"string","maxLength":100,"minLength":1,"title":"City","description":"City"},"state":{"type":"string","maxLength":50,"minLength":1,"title":"State","description":"State"},"zip_code":{"type":"string","maxLength":20,"minLength":1,"title":"Zip Code","description":"ZIP/Postal code"}},"type":"object","required":["primary_line","city","state","zip_code"],"title":"Address","description":"Address information"},"Allergen":{"type":"string","enum":["acacia","alternaria_alternata","aspergillus_fumigatus","aureobasidium_pullulans","australian_pine","birch","cladosporium_herbarum","common_ragweed","cottonwood","d_farinae","d_pteronyssinus","dog_dander","elm","horse_dander","maple_box_elder","maple_leaf_sycamore","mouse_urine","mugwort","mulberry","nettle","oak","olive","pecan_hickory","penicillium_notatum","rough_marsh_elder","rough_pigweed","russian_thistle","sheep_sorrel","walnut","white_ash","cross_reactive_carbohydrate_determinants","feather_mix_canary_chicken_goose_parakeet","house_dust","candida_albicans","mesquite","privet","poplar","lambs_quarters","lenscale","false_ragweed","dandelion","cat_dander","timothy_grass","bahia_grass","perennial_ryegrass","cultivated_oat_grass","cockroach","johnson_grass","bermuda_grass","alder","mountain_cedar","blomia_tropicalis","careless_weed","smooth_pigweed","american_sycamore","aspergillus_flavus"],"title":"Allergen","description":"Valid allergen identifiers"},"AllergenResult":{"properties":{"allergen_name":{"$ref":"#/components/schemas/Allergen","description":"Allergen identifier"},"value":{"type":"number","minimum":0.0,"title":"Value","description":"Allergen measurement value (must be non-negative)"}},"type":"object","required":["allergen_name","value"],"title":"AllergenResult","description":"Individual allergen test result"},"AllergyMedication":{"type":"string","enum":["otc_antihistamines","nasal_treatments","allergy_eye_drops","allergy_shots","sublingual_immunotherapy","other"],"title":"AllergyMedication","description":"Types of allergy medications patient may have tried"},"DeleteResponse":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether delete was successful","default":true},"deleted_id":{"type":"string","title":"Deleted Id","description":"ID of deleted item"},"message":{"type":"string","title":"Message","default":"Item deleted successfully"}},"type":"object","required":["deleted_id"],"title":"DeleteResponse","description":"Response for delete operations"},"DiagnosticDetailResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Diagnostic request ID (UUID)"},"patient_id":{"type":"integer","title":"Patient Id","description":"Patient ID linked to this diagnostic"},"provider_name":{"type":"string","title":"Provider Name","description":"Name of the provider who submitted"},"provider_npi":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Provider Npi","description":"Provider NPI number"},"measurement_unit":{"type":"string","title":"Measurement Unit","description":"Unit of measurement: fsu or kU/L"},"status":{"type":"string","title":"Status","description":"Review status: pending, approved, or rejected"},"triggers":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Triggers","description":"Allergy triggers from intake"},"seasons":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Seasons","description":"Seasons from intake"},"tried_allergy_meds":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Tried Allergy Meds","description":"Has patient tried allergy medications before?"},"allergy_medications":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Allergy Medications","description":"Medication types tried"},"known_drug_allergies":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Known Drug Allergies","description":"Known drug allergies"},"contraindications_to_slit":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Contraindications To Slit","description":"Contraindications to SLIT"},"created_at":{"type":"string","format":"date-time","title":"Created At","description":"When the diagnostic was submitted"},"reviewed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Reviewed At","description":"When the diagnostic was reviewed"},"jotform_link":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Jotform Link","description":"Pre-filled JotForm prescription link generated for this diagnostic, if a recommendation was sent"},"jotform_completed":{"type":"boolean","title":"Jotform Completed","description":"Whether the patient has completed the prescription JotForm (an approved Rx was created)","default":false},"jotform_completed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Jotform Completed At","description":"When the patient most recently completed the prescription JotForm, if at all"}},"type":"object","required":["id","patient_id","provider_name","measurement_unit","status","created_at"],"title":"DiagnosticDetailResponse","description":"Response model for retrieving a diagnostic request"},"DiagnosticRequest":{"properties":{"id":{"anyOf":[{"type":"string","maxLength":100,"minLength":1},{"type":"null"}],"title":"Id","description":"Unique identifier for this diagnostic request (UUID generated if not provided). Requests made with the same uuid are idempotent."},"patient_id":{"anyOf":[{"type":"integer","exclusiveMinimum":0.0},{"type":"null"}],"title":"Patient Id","description":"Existing patient ID (required if patient_info not provided)"},"provider":{"$ref":"#/components/schemas/ProviderInfo","description":"Provider information"},"patient_info":{"anyOf":[{"$ref":"#/components/schemas/PatientInfo"},{"type":"null"}],"description":"Patient information (required if patient_id not provided)"},"results":{"items":{"$ref":"#/components/schemas/AllergenResult"},"type":"array","minItems":1,"title":"Results","description":"List of allergen test results"},"measurement_unit":{"type":"string","enum":["fsu","kU/L"],"title":"Measurement Unit","description":"Unit of measurement for allergen values"},"intake":{"anyOf":[{"$ref":"#/components/schemas/IntakeData"},{"type":"null"}],"description":"Patient intake questionnaire data"},"total_ige":{"anyOf":[{"type":"number","minimum":0.0},{"type":"null"}],"title":"Total Ige","description":"Total IgE measurement value"}},"type":"object","required":["provider","results","measurement_unit"],"title":"DiagnosticRequest","description":"Request model for creating a diagnostic submission"},"DiagnosticResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Diagnostic request ID (UUID)"},"patient_id":{"type":"integer","title":"Patient Id","description":"Patient ID linked to this diagnostic"},"success":{"type":"boolean","title":"Success","description":"Whether the diagnostic was queued successfully"},"message":{"type":"string","title":"Message","description":"Success or error message"},"results_count":{"type":"integer","minimum":0.0,"title":"Results Count","description":"Number of allergen results submitted"},"invalid_allergens":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Invalid Allergens","description":"List of invalid allergen names (if any)"}},"type":"object","required":["id","patient_id","success","message","results_count"],"title":"DiagnosticResponse","description":"Response model for diagnostic submission"},"DropsOrderStatus":{"type":"string","enum":["queued","sent_to_pharmacy","in_transit","delivered","cancelled"],"title":"DropsOrderStatus","description":"Valid drops_orders statuses — matches CHECK constraint in migration 020"},"ErrorDetail":{"properties":{"field":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Field","description":"Field that caused the error"},"message":{"type":"string","title":"Message","description":"Error message"},"type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Type","description":"Error type"}},"type":"object","required":["message"],"title":"ErrorDetail","description":"Detailed error information"},"ErrorResponse":{"properties":{"error_code":{"type":"string","title":"Error Code","description":"Machine-readable error code"},"message":{"type":"string","title":"Message","description":"Human-readable error message"},"details":{"anyOf":[{"items":{"$ref":"#/components/schemas/ErrorDetail"},"type":"array"},{"type":"null"}],"title":"Details","description":"Additional error details"}},"type":"object","required":["error_code","message"],"title":"ErrorResponse","description":"Standard error response"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"IntakeData":{"properties":{"triggers":{"items":{"$ref":"#/components/schemas/Trigger"},"type":"array","title":"Triggers","description":"List of allergy triggers: 'dust', 'mold', 'pollen', 'cat', 'dog'"},"seasons":{"items":{"$ref":"#/components/schemas/Season"},"type":"array","title":"Seasons","description":"List of seasons: 'spring', 'summer', 'fall'"},"tried_allergy_meds":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Tried Allergy Meds","description":"Has patient tried allergy medications before?"},"allergy_medications":{"items":{"$ref":"#/components/schemas/AllergyMedication"},"type":"array","title":"Allergy Medications","description":"Medication types tried: 'otc_antihistamines', 'nasal_treatments', 'allergy_eye_drops', 'allergy_shots', 'sublingual_immunotherapy', 'other'"},"known_drug_allergies":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Known Drug Allergies","description":"Known drug allergies (free text)"},"contraindications_to_slit":{"type":"boolean","title":"Contraindications To Slit","description":"Contraindications to sublingual immunotherapy"}},"type":"object","required":["contraindications_to_slit"],"title":"IntakeData","description":"Patient intake questionnaire data"},"PatientInfo":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name","description":"Patient full name"},"dob":{"type":"string","title":"Dob","description":"Date of birth in YYYY-MM-DD format"},"email":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}],"title":"Email","description":"Patient email address"},"phone_number":{"anyOf":[{"type":"string","maxLength":20,"minLength":10},{"type":"null"}],"title":"Phone Number","description":"Patient phone number"},"address":{"$ref":"#/components/schemas/Address","description":"Patient address"}},"type":"object","required":["name","dob","address"],"title":"PatientInfo","description":"Patient information for creating new patient records"},"PatientResponse":{"properties":{"id":{"type":"integer","title":"Id","description":"Patient ID"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name","description":"Patient full name"},"dob":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Dob","description":"Date of birth (YYYY-MM-DD)"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email","description":"Patient email address"},"phone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone","description":"Patient phone number"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At","description":"When the patient was created"}},"type":"object","required":["id"],"title":"PatientResponse","description":"Single patient in list response"},"PatientSearchRequest":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Name","description":"Filter by name (partial match, case-insensitive)"},"dob":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Dob","description":"Filter by DOB (YYYY-MM-DD)"},"email":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}],"title":"Email","description":"Filter by email (exact match)"},"limit":{"type":"integer","maximum":100.0,"minimum":1.0,"title":"Limit","description":"Results per page (max 100)","default":50},"offset":{"type":"integer","minimum":0.0,"title":"Offset","description":"Number of results to skip","default":0}},"type":"object","title":"PatientSearchRequest","description":"Request body for patient search - using POST to keep PHI out of URLs"},"PatientsListResponse":{"properties":{"patients":{"items":{"$ref":"#/components/schemas/PatientResponse"},"type":"array","title":"Patients","description":"List of patients"},"total":{"type":"integer","minimum":0.0,"title":"Total","description":"Total number of matching patients"},"limit":{"type":"integer","title":"Limit","description":"Results per page"},"offset":{"type":"integer","title":"Offset","description":"Number of results skipped"}},"type":"object","required":["patients","total","limit","offset"],"title":"PatientsListResponse","description":"Paginated list of patients"},"ProviderInfo":{"properties":{"name":{"type":"string","maxLength":100,"minLength":1,"title":"Name","description":"Provider name"},"npi":{"type":"integer","exclusiveMinimum":0.0,"title":"Npi","description":"Provider NPI (10-digit number)"}},"type":"object","required":["name","npi"],"title":"ProviderInfo"},"ReauthLinkRequest":{"properties":{"patient_id":{"type":"integer","title":"Patient Id","description":"Patient ID owned by the authenticated partner"}},"type":"object","required":["patient_id"],"title":"ReauthLinkRequest","description":"Body for POST /prescriptions/reauth-link."},"ReauthLinkResponse":{"properties":{"patient_id":{"type":"integer","title":"Patient Id","description":"Patient ID"},"jotform_link":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Jotform Link","description":"Pre-filled JotForm URL the partner can hand back to the patient to re-authorize the prescription. Null if the patient has no diagnostic on file or upstream link generation failed."},"rx_renewal_needed":{"type":"boolean","title":"Rx Renewal Needed","description":"True if refills are exhausted or the prescription expires within 80 days"},"refills_remaining":{"type":"integer","title":"Refills Remaining","description":"Refills remaining on the patient's prescription"},"rx_expiration_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rx Expiration Date","description":"Prescription expiration date (YYYY-MM-DD), if known"}},"type":"object","required":["patient_id","rx_renewal_needed","refills_remaining"],"title":"ReauthLinkResponse","description":"Response for POST /prescriptions/reauth-link."},"RxExpiredError":{"properties":{"detail":{"$ref":"#/components/schemas/RxExpiredErrorDetail"}},"type":"object","required":["detail"],"title":"RxExpiredError","description":"Error response when Rx is expired (matches FastAPI HTTPException shape)"},"RxExpiredErrorDetail":{"properties":{"error_code":{"type":"string","title":"Error Code","default":"rx_expired"},"message":{"type":"string","title":"Message","default":"Prescription expired — no remaining refills. Submit a new Rx before requesting shipment."},"patient_id":{"type":"integer","title":"Patient Id"}},"type":"object","required":["patient_id"],"title":"RxExpiredErrorDetail","description":"Inner detail of the Rx expired error response"},"Scenario":{"type":"string","enum":["rx_valid","refills_low","needs_renewal","rx_expired"],"title":"Scenario","description":"Rx-state scenarios for sandbox /simulate."},"Season":{"type":"string","enum":["spring","summer","fall","winter"],"title":"Season","description":"Valid seasons for allergy occurrence"},"ShipmentRequestAddress":{"properties":{"ship_to_name":{"type":"string","maxLength":255,"minLength":1,"title":"Ship To Name","description":"Recipient name"},"address_line_1":{"type":"string","maxLength":255,"minLength":1,"title":"Address Line 1","description":"Street line 1"},"address_line_2":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Address Line 2","description":"Street line 2 (apt/suite)"},"city":{"type":"string","maxLength":100,"minLength":1,"title":"City","description":"City"},"state":{"type":"string","maxLength":2,"minLength":2,"title":"State","description":"2-letter state code"},"zip":{"type":"string","maxLength":10,"minLength":5,"title":"Zip","description":"ZIP/postal code"},"country":{"anyOf":[{"type":"string","maxLength":2,"minLength":2},{"type":"null"}],"title":"Country","description":"2-letter country code","default":"US"}},"type":"object","required":["ship_to_name","address_line_1","city","state","zip"],"title":"ShipmentRequestAddress","description":"One-off shipping address override on a refill request.\n\nAll fields required when an override is provided. NULL on the column means\n'use the patient default'; an override carries every component."},"ShipmentRequestBody":{"properties":{"patient_id":{"type":"integer","exclusiveMinimum":0.0,"title":"Patient Id","description":"Patient ID to ship next refill to"},"prescription_id":{"anyOf":[{"type":"integer","exclusiveMinimum":0.0},{"type":"null"}],"title":"Prescription Id","description":"Explicit prescription to draw from. Defaults to the patient's most recent prescription when omitted."},"shipment_type":{"anyOf":[{"$ref":"#/components/schemas/ShipmentType"},{"type":"null"}],"description":"Optional cadence label echoed back in the response."},"address":{"anyOf":[{"$ref":"#/components/schemas/ShipmentRequestAddress"},{"type":"null"}],"description":"Optional one-off ship-to address override; omit to use the patient's default."}},"type":"object","required":["patient_id"],"title":"ShipmentRequestBody","description":"Request body for POST /shipments/request"},"ShipmentRequestResponse":{"properties":{"order_id":{"type":"string","title":"Order Id","description":"UUID of the queued or existing fulfillment order"},"status":{"$ref":"#/components/schemas/DropsOrderStatus","description":"Current status of the order"},"shipment_type":{"anyOf":[{"$ref":"#/components/schemas/ShipmentType"},{"type":"null"}],"description":"Cadence label echoed from request"},"idempotent":{"type":"boolean","title":"Idempotent","description":"True if a same-day duplicate request returned the existing order (either via duplicate-detection or race-condition recovery)."},"refills_remaining":{"type":"integer","minimum":0.0,"title":"Refills Remaining","description":"Refills available AFTER this order"},"total_refills":{"type":"integer","minimum":0.0,"title":"Total Refills","description":"Total refills the prescription was issued for"},"rx_expiration_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rx Expiration Date","description":"Rx expiration date (YYYY-MM-DD)"},"needs_renewal":{"type":"boolean","title":"Needs Renewal","description":"True when refills are exhausted or expiry is within 80 days (matches the rx_renewal_needed event trigger)."}},"type":"object","required":["order_id","status","idempotent","refills_remaining","total_refills","needs_renewal"],"title":"ShipmentRequestResponse","description":"Response for POST /shipments/request (v2)."},"ShipmentResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Shipment ID"},"diagnostic_request_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Diagnostic Request Id","description":"Linked diagnostic request ID"},"patient_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Patient Id","description":"Patient ID"},"status":{"type":"string","title":"Status","description":"Shipment status"},"tracking_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tracking Id","description":"Carrier tracking number"},"carrier":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Carrier","description":"Shipping carrier"},"tracking_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tracking Url","description":"Tracking URL"},"estimated_ship_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Estimated Ship Date","description":"Estimated ship date (YYYY-MM-DD)"},"ship_to_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ship To Name","description":"Recipient name"},"ship_to_city":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ship To City","description":"Destination city"},"ship_to_state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ship To State","description":"Destination state"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At","description":"When the shipment was created"},"refills_remaining":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Refills Remaining","description":"Refills remaining on the patient's latest Rx (null if no Rx on file)"},"total_refills":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total Refills","description":"Total refills issued on the patient's latest Rx (null if no Rx on file)"},"needs_renewal":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Needs Renewal","description":"True if the patient's Rx is exhausted or expiring soon. Null when the patient has no prescription on file."}},"type":"object","required":["id","status"],"title":"ShipmentResponse","description":"Single shipment in list response"},"ShipmentSearchRequest":{"properties":{"status":{"anyOf":[{"$ref":"#/components/schemas/ShipmentStatus"},{"type":"null"}],"description":"Filter by specific status"},"patient_id":{"anyOf":[{"type":"integer","exclusiveMinimum":0.0},{"type":"null"}],"title":"Patient Id","description":"Filter by patient ID"},"estimated_ship_date_from":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Estimated Ship Date From","description":"Filter by estimated ship date (from, YYYY-MM-DD)"},"estimated_ship_date_to":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Estimated Ship Date To","description":"Filter by estimated ship date (to, YYYY-MM-DD)"},"limit":{"type":"integer","maximum":100.0,"minimum":1.0,"title":"Limit","description":"Results per page (max 100)","default":50},"offset":{"type":"integer","minimum":0.0,"title":"Offset","description":"Number of results to skip","default":0}},"type":"object","title":"ShipmentSearchRequest","description":"Request body for upcoming shipments search"},"ShipmentStatus":{"type":"string","enum":["queued","processing","shipped","in_transit","out_for_delivery","delivered","failed","returned"],"title":"ShipmentStatus","description":"Valid shipment statuses (legacy shipments table)"},"ShipmentType":{"type":"string","enum":["biannual","quarterly"],"title":"ShipmentType","description":"Partner-supplied cadence label for a refill order."},"ShipmentsListResponse":{"properties":{"shipments":{"items":{"$ref":"#/components/schemas/ShipmentResponse"},"type":"array","title":"Shipments","description":"List of shipments"},"total":{"type":"integer","minimum":0.0,"title":"Total","description":"Total number of matching shipments"},"limit":{"type":"integer","title":"Limit","description":"Results per page"},"offset":{"type":"integer","title":"Offset","description":"Number of results skipped"}},"type":"object","required":["shipments","total","limit","offset"],"title":"ShipmentsListResponse","description":"Paginated list of upcoming shipments"},"SimulateContext":{"properties":{"event_types_fired":{"items":{"type":"string"},"type":"array","title":"Event Types Fired","description":"Webhook event types fired, in order."},"scenario":{"type":"string","title":"Scenario","description":"Scenario applied (defaulted to rx_valid if omitted)."},"patient_id":{"type":"integer","title":"Patient Id","description":"patient_id used across all fired events."},"prescription_id":{"type":"integer","title":"Prescription Id","description":"prescription_id used across rx-bearing events."},"shipment_id":{"type":"string","title":"Shipment Id","description":"shipment_id used across shipment terminal events."},"diagnostic_request_id":{"type":"string","title":"Diagnostic Request Id","description":"diagnostic_request_id used for recommendation_ready and treatment_declined."},"persisted":{"type":"boolean","title":"Persisted","description":"True if rows were written to the sandbox schema."}},"type":"object","required":["event_types_fired","scenario","patient_id","prescription_id","shipment_id","diagnostic_request_id","persisted"],"title":"SimulateContext","description":"IDs and metadata used by this /simulate call.\n\nSurfaced at the top level of the response so partners can copy them into\n/shipments/request and other endpoints without parsing results[].payload."},"SimulateWebhookRequest":{"properties":{"event_type":{"anyOf":[{"$ref":"#/components/schemas/WebhookEventType"},{"type":"null"}],"description":"Single event to fire. Omit to fire the full canonical lifecycle (recommendation_ready → ready_for_shipment → shipment_shipped → shipment_delivered)."},"scenario":{"anyOf":[{"$ref":"#/components/schemas/Scenario"},{"type":"null"}],"description":"Rx-state preset that drives payload values and persisted row state. Defaults to rx_valid."},"patient_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Patient Id","description":"Use this patient_id instead of a generated one."},"prescription_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Prescription Id","description":"Use this prescription_id instead of a generated one."},"shipment_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Shipment Id","description":"Use this shipment_id (drops_orders.id) instead of a generated one."},"diagnostic_request_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Diagnostic Request Id","description":"Use this diagnostic_request_id instead of a generated one."},"persist":{"type":"boolean","title":"Persist","description":"When true, write sandbox.patients + sandbox.prescriptions (+ backfill sandbox.drops_orders) so the returned IDs work against /shipments/request.","default":false}},"type":"object","title":"SimulateWebhookRequest","description":"Request model for simulating webhook events in sandbox mode.\n\nAll fields optional. Empty body fires the full canonical lifecycle\n(recommendation_ready → ready_for_shipment → shipment_shipped →\nshipment_delivered) with a generated id-set and scenario=rx_valid."},"SimulateWebhookResponse":{"properties":{"success":{"type":"boolean","title":"Success","description":"Whether all webhooks were delivered successfully"},"webhook_urls":{"items":{"type":"string"},"type":"array","title":"Webhook Urls","description":"Webhook URLs the events were sent to"},"results":{"items":{"$ref":"#/components/schemas/WebhookDeliveryResult"},"type":"array","title":"Results","description":"Results for each webhook delivery"},"context":{"$ref":"#/components/schemas/SimulateContext","description":"IDs and metadata used by this /simulate call."},"message":{"type":"string","title":"Message","description":"Overall success or error message"}},"type":"object","required":["success","webhook_urls","results","context","message"],"title":"SimulateWebhookResponse","description":"Response model for webhook simulation"},"Trigger":{"type":"string","enum":["dust","mold","pollen","cat","dog"],"title":"Trigger","description":"Valid allergy triggers"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"WebhookCreateRequest":{"properties":{"webhook_url":{"type":"string","maxLength":2048,"minLength":1,"title":"Webhook Url","description":"HTTPS URL to receive webhook events"},"webhook_secret":{"anyOf":[{"type":"string","maxLength":256},{"type":"null"}],"title":"Webhook Secret","description":"Optional secret for HMAC-SHA256 signature verification"}},"type":"object","required":["webhook_url"],"title":"WebhookCreateRequest","description":"Request model for creating a webhook configuration"},"WebhookDeliveryResult":{"properties":{"webhook_id":{"type":"integer","title":"Webhook Id","description":"Webhook configuration ID that received this delivery"},"webhook_url":{"type":"string","title":"Webhook Url","description":"Webhook URL targeted for this delivery"},"event_type":{"type":"string","title":"Event Type","description":"Type of webhook event"},"success":{"type":"boolean","title":"Success","description":"Whether this delivery succeeded"},"status_code":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Status Code","description":"HTTP status code from webhook endpoint"},"message":{"type":"string","title":"Message","description":"Success or error message"},"payload":{"$ref":"#/components/schemas/WebhookPayload","description":"The webhook payload that was sent"}},"type":"object","required":["webhook_id","webhook_url","event_type","success","message","payload"],"title":"WebhookDeliveryResult","description":"Result of a single webhook delivery attempt"},"WebhookEventType":{"type":"string","enum":["recommendation_ready","treatment_declined","ready_for_shipment","shipment_shipped","shipment_delivered","shipment_cancelled","rx_renewal_needed"],"title":"WebhookEventType","description":"Webhook event types for partner notifications"},"WebhookListResponse":{"properties":{"webhooks":{"items":{"$ref":"#/components/schemas/WebhookResponse"},"type":"array","title":"Webhooks","description":"Webhook configurations for the authenticated partner"}},"type":"object","required":["webhooks"],"title":"WebhookListResponse","description":"Response model for listing webhook configurations"},"WebhookPayload":{"properties":{"event_type":{"type":"string","title":"Event Type","description":"Event type: 'recommendation_ready', 'treatment_declined', 'ready_for_shipment', 'shipment_shipped', 'shipment_delivered', 'shipment_cancelled', or 'rx_renewal_needed'"},"timestamp":{"type":"string","title":"Timestamp","description":"ISO 8601 timestamp of the event"},"data":{"type":"object","title":"Data","description":"Event-specific data — see the per-event Data schemas"}},"type":"object","required":["event_type","timestamp","data"],"title":"WebhookPayload","description":"Union type for all webhook payloads - used in documentation","examples":[{"data":{"diagnostic_request_id":"test-abc123","jotform_link":"https://form.jotform.com/example?prefilled=test_data","mixes":["Tree Mix","Mold Mix","Cat"],"patient_id":12345},"event_type":"recommendation_ready","timestamp":"2026-04-29T10:30:00Z"},{"data":{"diagnostic_request_id":"test-abc123","patient_id":12345,"reason":"no_positive_results_and_no_reported_symptoms"},"event_type":"treatment_declined","timestamp":"2026-04-29T10:30:00Z"},{"data":{"carrier":"ups","patient_id":12345,"shipment_id":"ord-abc123","status":"in_transit","tracking_id":"1Z999AA10123456784","tracking_url":"https://www.ups.com/track?tracknum=1Z999AA10123456784"},"event_type":"shipment_shipped","timestamp":"2026-04-29T14:00:00Z"},{"data":{"carrier":"ups","current_rx_refills_remaining":2,"patient_id":12345,"rx_expiration_date":"2027-01-15","rx_renewal_needed":false,"shipment_id":"ord-abc123","status":"delivered","tracking_id":"1Z999AA10123456784","tracking_url":"https://www.ups.com/track?tracknum=1Z999AA10123456784"},"event_type":"shipment_delivered","timestamp":"2026-04-29T18:00:00Z"},{"data":{"carrier":"ups","current_rx_refills_remaining":2,"patient_id":12345,"rx_expiration_date":"2027-01-15","rx_renewal_needed":false,"shipment_id":"ord-abc123","status":"cancelled","tracking_id":"1Z999AA10123456784","tracking_url":"https://www.ups.com/track?tracknum=1Z999AA10123456784"},"event_type":"shipment_cancelled","timestamp":"2026-04-29T14:00:00Z"},{"data":{"current_rx_refills_remaining":0,"patient_id":12345,"rx_expiration_date":"2027-01-15"},"event_type":"rx_renewal_needed","timestamp":"2026-04-29T14:00:00Z"}]},"WebhookResponse":{"properties":{"id":{"type":"integer","title":"Id","description":"Webhook configuration ID"},"partner_id":{"type":"integer","title":"Partner Id","description":"Partner ID this webhook belongs to"},"webhook_url":{"type":"string","title":"Webhook Url","description":"HTTPS URL for webhook delivery"},"webhook_secret_configured":{"type":"boolean","title":"Webhook Secret Configured","description":"Whether a webhook secret is configured (secret value is not returned)"},"is_active":{"type":"boolean","title":"Is Active","description":"Whether the webhook is active"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At","description":"When the webhook was created"},"updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Updated At","description":"When the webhook was last updated"}},"type":"object","required":["id","partner_id","webhook_url","webhook_secret_configured","is_active"],"title":"WebhookResponse","description":"Response model for webhook configuration"},"RecommendationReadyData":{"description":"Data payload for recommendation_ready webhook event","properties":{"diagnostic_request_id":{"description":"Unique identifier for the diagnostic request","title":"Diagnostic Request Id","type":"string"},"patient_id":{"description":"Patient ID","title":"Patient Id","type":"integer"},"mixes":{"description":"List of recommended allergen mixes","items":{"type":"string"},"title":"Mixes","type":"array"},"jotform_link":{"description":"Pre-filled JotForm link for patient intake","title":"Jotform Link","type":"string"}},"required":["diagnostic_request_id","patient_id","mixes","jotform_link"],"title":"RecommendationReadyData","type":"object"},"RecommendationReadyPayload":{"description":"Full webhook payload for recommendation_ready event","properties":{"event_type":{"const":"recommendation_ready","description":"Event type identifier","title":"Event Type","type":"string"},"timestamp":{"description":"ISO 8601 timestamp of the event","title":"Timestamp","type":"string"},"data":{"$ref":"#/components/schemas/RecommendationReadyData","description":"Event-specific data"}},"required":["event_type","timestamp","data"],"title":"RecommendationReadyPayload","type":"object"},"TreatmentDeclinedData":{"description":"Data payload for treatment_declined webhook event.\n\nFires INSTEAD of recommendation_ready when no treatment is recommended. No JotForm\nlink is generated for these diagnostics — there is no treatment to sign. The\n`reason` distinguishes the two decline paths:\n  - no_positive_results_and_no_reported_symptoms: zero recommended mixes AND the\n    patient reported no triggers/seasons (default behavior for all partners).\n  - no_positive_lab_results: the lab panel showed no positive allergen, for a\n    partner configured to require a validated lab result before prescribing\n    (partners.require_positive_lab_results); fires even when symptoms were reported.","properties":{"diagnostic_request_id":{"description":"Unique identifier for the diagnostic request","title":"Diagnostic Request Id","type":"string"},"patient_id":{"description":"Patient ID","title":"Patient Id","type":"integer"},"reason":{"description":"Why treatment was declined. 'no_positive_results_and_no_reported_symptoms' when nothing was recommended and no symptoms were reported; 'no_positive_lab_results' when a require_positive_lab_results partner's lab panel showed no positive allergen.","enum":["no_positive_results_and_no_reported_symptoms","no_positive_lab_results"],"title":"Reason","type":"string"}},"required":["diagnostic_request_id","patient_id","reason"],"title":"TreatmentDeclinedData","type":"object"},"TreatmentDeclinedPayload":{"description":"Full webhook payload for treatment_declined event","properties":{"event_type":{"const":"treatment_declined","description":"Event type identifier","title":"Event Type","type":"string"},"timestamp":{"description":"ISO 8601 timestamp of the event","title":"Timestamp","type":"string"},"data":{"$ref":"#/components/schemas/TreatmentDeclinedData","description":"Event-specific data"}},"required":["event_type","timestamp","data"],"title":"TreatmentDeclinedPayload","type":"object"},"ReadyForShipmentData":{"description":"Data payload for ready_for_shipment webhook event.\n\nFires once per prescription, immediately after the doctor approves the\nJotForm-submitted prescription. Idempotent per prescription_id.\nEmitted by lively's 1-Click RX webhook (webhook_jotform_one_click_rx ->\none_click_rx.process_one_click_rx_submission) when a provider-signed\n253075842740155 submission is received, and by testTracker's\nready_for_shipment_notifier on the DrChrono approval path.","properties":{"patient_id":{"description":"Patient ID","title":"Patient Id","type":"integer"},"partner_id":{"description":"Partner ID owning the patient","title":"Partner Id","type":"integer"},"prescription_id":{"description":"Prescription ID","title":"Prescription Id","type":"integer"},"date_approved":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"Date the prescription was approved by the doctor (YYYY-MM-DD)","title":"Date Approved"},"rx_state":{"$ref":"#/components/schemas/ReadyForShipmentRxState","description":"Snapshot of prescription state at delivery time"}},"required":["patient_id","partner_id","prescription_id","rx_state"],"title":"ReadyForShipmentData","type":"object"},"ReadyForShipmentRxState":{"description":"Snapshot of prescription state included in the ready_for_shipment payload.","properties":{"rx_valid":{"description":"True if the prescription has refills remaining and has not expired","title":"Rx Valid","type":"boolean"},"rx_expired":{"description":"True if the prescription has passed its 366-day expiry","title":"Rx Expired","type":"boolean"},"refills_used":{"description":"Refills consumed so far","title":"Refills Used","type":"integer"},"refills_remaining":{"description":"Refills remaining (max 4 minus refills_used)","title":"Refills Remaining","type":"integer"},"total_refills":{"description":"Total refills allowed on a prescription (currently 4)","title":"Total Refills","type":"integer"},"days_until_expiry":{"description":"Days until the prescription expires (negative if expired)","title":"Days Until Expiry","type":"integer"},"rx_expiration_date":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"Prescription expiration date (YYYY-MM-DD), or null if no date_approved","title":"Rx Expiration Date"},"needs_renewal":{"description":"True if the prescription is exhausted or expiring soon (within 80 days). Same trigger as the rx_renewal_needed event.","title":"Needs Renewal","type":"boolean"}},"required":["rx_valid","rx_expired","refills_used","refills_remaining","total_refills","days_until_expiry","needs_renewal"],"title":"ReadyForShipmentRxState","type":"object"},"ReadyForShipmentPayload":{"description":"Full webhook payload for ready_for_shipment event","properties":{"event_type":{"const":"ready_for_shipment","description":"Event type identifier","title":"Event Type","type":"string"},"timestamp":{"description":"ISO 8601 timestamp of the event","title":"Timestamp","type":"string"},"data":{"$ref":"#/components/schemas/ReadyForShipmentData","description":"Event-specific data"}},"required":["event_type","timestamp","data"],"title":"ReadyForShipmentPayload","type":"object"},"ShipmentShippedData":{"description":"Data payload for shipment_shipped webhook event (dispatch / in-transit).\n\nFires when the kit ships and a tracking number is known (the order advances\nto in_transit). Intentionally lean — carries only shipping/tracking details\nso the partner can notify the member and stamp tracking. Rx-renewal facts and\nthe companion rx_renewal_needed event arrive later on shipment_delivered, the\n\"shipment complete\" moment.","properties":{"shipment_id":{"description":"Drop order ID for this shipment","title":"Shipment Id","type":"string"},"patient_id":{"description":"Patient ID","title":"Patient Id","type":"integer"},"status":{"const":"in_transit","description":"Always 'in_transit' for shipment_shipped events","title":"Status","type":"string"},"tracking_id":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"Carrier tracking number","title":"Tracking Id"},"carrier":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"Shipping carrier (e.g., 'ups', 'usps', 'fedex')","title":"Carrier"},"tracking_url":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"URL to track the shipment","title":"Tracking Url"}},"required":["shipment_id","patient_id","status"],"title":"ShipmentShippedData","type":"object"},"ShipmentShippedPayload":{"description":"Full webhook payload for shipment_shipped event (dispatch / in-transit)","properties":{"event_type":{"const":"shipment_shipped","description":"Event type identifier","title":"Event Type","type":"string"},"timestamp":{"description":"ISO 8601 timestamp of the event","title":"Timestamp","type":"string"},"data":{"$ref":"#/components/schemas/ShipmentShippedData","description":"Event-specific data"}},"required":["event_type","timestamp","data"],"title":"ShipmentShippedPayload","type":"object"},"ShipmentDeliveredData":{"description":"Data payload for shipment_delivered webhook event (terminal: delivered).\n\nFires when the carrier confirms the member received their medication. This is\nthe event formerly named shipment_shipped — same shape and renewal semantics,\nrenamed to make room for the dispatch-time shipment_shipped event.","properties":{"shipment_id":{"description":"Drop order ID for this shipment","title":"Shipment Id","type":"string"},"patient_id":{"description":"Patient ID","title":"Patient Id","type":"integer"},"status":{"const":"delivered","description":"Always 'delivered' for shipment_delivered events","title":"Status","type":"string"},"tracking_id":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"Carrier tracking number","title":"Tracking Id"},"carrier":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"Shipping carrier (e.g., 'ups', 'usps', 'fedex')","title":"Carrier"},"tracking_url":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"URL to track the shipment","title":"Tracking Url"},"rx_renewal_needed":{"description":"True if the patient's prescription is exhausted or expiring soon. A separate rx_renewal_needed event is also fired in this case.","title":"Rx Renewal Needed","type":"boolean"},"current_rx_refills_remaining":{"description":"Refills remaining on the patient's prescription","title":"Current Rx Refills Remaining","type":"integer"},"rx_expiration_date":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"Prescription expiration date (YYYY-MM-DD), if known","title":"Rx Expiration Date"},"jotform_link":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"Pre-filled JotForm URL the partner can hand back to the patient to re-authorize the prescription. Populated only when rx_renewal_needed=true; null otherwise.","title":"Jotform Link"}},"required":["shipment_id","patient_id","status","rx_renewal_needed","current_rx_refills_remaining"],"title":"ShipmentDeliveredData","type":"object"},"ShipmentDeliveredPayload":{"description":"Full webhook payload for shipment_delivered event (delivery confirmation)","properties":{"event_type":{"const":"shipment_delivered","description":"Event type identifier","title":"Event Type","type":"string"},"timestamp":{"description":"ISO 8601 timestamp of the event","title":"Timestamp","type":"string"},"data":{"$ref":"#/components/schemas/ShipmentDeliveredData","description":"Event-specific data"}},"required":["event_type","timestamp","data"],"title":"ShipmentDeliveredPayload","type":"object"},"ShipmentCancelledData":{"description":"Data payload for shipment_cancelled webhook event (terminal: returned/failed/expired).","properties":{"shipment_id":{"description":"Drop order ID for this shipment","title":"Shipment Id","type":"string"},"patient_id":{"description":"Patient ID","title":"Patient Id","type":"integer"},"status":{"const":"cancelled","description":"Always 'cancelled' for shipment_cancelled events. Returned, failed, and expired carrier outcomes all collapse into this single status.","title":"Status","type":"string"},"tracking_id":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"Carrier tracking number","title":"Tracking Id"},"carrier":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"Shipping carrier (e.g., 'ups', 'usps', 'fedex')","title":"Carrier"},"tracking_url":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"URL to track the shipment","title":"Tracking Url"},"rx_renewal_needed":{"description":"True if the patient's prescription is exhausted or expiring soon. A separate rx_renewal_needed event is also fired in this case.","title":"Rx Renewal Needed","type":"boolean"},"current_rx_refills_remaining":{"description":"Refills remaining on the patient's prescription","title":"Current Rx Refills Remaining","type":"integer"},"rx_expiration_date":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"Prescription expiration date (YYYY-MM-DD), if known","title":"Rx Expiration Date"},"jotform_link":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"Pre-filled JotForm URL the partner can hand back to the patient to re-authorize the prescription. Populated only when rx_renewal_needed=true; null otherwise.","title":"Jotform Link"}},"required":["shipment_id","patient_id","status","rx_renewal_needed","current_rx_refills_remaining"],"title":"ShipmentCancelledData","type":"object"},"ShipmentCancelledPayload":{"description":"Full webhook payload for shipment_cancelled event","properties":{"event_type":{"const":"shipment_cancelled","description":"Event type identifier","title":"Event Type","type":"string"},"timestamp":{"description":"ISO 8601 timestamp of the event","title":"Timestamp","type":"string"},"data":{"$ref":"#/components/schemas/ShipmentCancelledData","description":"Event-specific data"}},"required":["event_type","timestamp","data"],"title":"ShipmentCancelledPayload","type":"object"},"RxRenewalNeededData":{"description":"Data payload for rx_renewal_needed webhook event.\n\nFires alongside the terminal shipment event when the prescription has zero\nrefills remaining or expires within 80 days. Use this as the trigger to\nre-engage the clinician for a new prescription submission.","properties":{"patient_id":{"description":"Patient ID","title":"Patient Id","type":"integer"},"current_rx_refills_remaining":{"description":"Refills remaining (0 means exhausted)","title":"Current Rx Refills Remaining","type":"integer"},"rx_expiration_date":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"Prescription expiration date (YYYY-MM-DD)","title":"Rx Expiration Date"},"jotform_link":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"Pre-filled JotForm URL the partner can hand back to the patient to re-authorize the prescription. May be null if upstream link generation failed; partners should retry via the on-demand /prescriptions/reauth-link endpoint in that case.","title":"Jotform Link"}},"required":["patient_id","current_rx_refills_remaining"],"title":"RxRenewalNeededData","type":"object"},"RxRenewalNeededPayload":{"description":"Full webhook payload for rx_renewal_needed event","properties":{"event_type":{"const":"rx_renewal_needed","description":"Event type identifier","title":"Event Type","type":"string"},"timestamp":{"description":"ISO 8601 timestamp of the event","title":"Timestamp","type":"string"},"data":{"$ref":"#/components/schemas/RxRenewalNeededData","description":"Event-specific data"}},"required":["event_type","timestamp","data"],"title":"RxRenewalNeededPayload","type":"object"}}},"tags":[{"name":"Diagnostics","description":"All endpoints related to submitting and retrieving diagnostic results from Wyndly"},{"name":"Patients","description":"Search existing patient records"},{"name":"Shipments","description":"View upcoming shipments and track delivery status"},{"name":"Prescriptions","description":"Generate on-demand prescription re-authorization links for patients"},{"name":"Webhooks","description":"Configure webhook endpoints to receive event notifications."},{"name":"Sandbox","description":"Test webhook integrations in development and sandbox environments."}]}