# SNOW API — Full Reference for AI Assistants > Clinical-summarization API. Turn patient history into structured actionable summaries. > This is the single-file flattened version of https://docs.snowmed.health for paste-into-LLM-context use. > Last generated: 2026-05-18T03:43:46.723Z --- # Section 1: Integration Guide # SNOW integration guide This guide is everything you need to integrate any clinical app with SNOW. One API key, one webhook receiver, four supported flows. > Reading this top-to-bottom should answer every integration question. If > something isn't covered, it's a docs gap — please flag it. --- ## What SNOW is SNOW is a clinical-summarization platform. You give it some combination of **patient questions, uploaded reports, or pre-collected structured data**; SNOW returns a structured clinical summary suitable for a doctor's review. There are two orthogonal axes you'll work with: ### Axis 1 — the *flow* (how data gets in) A small, finite set of integration shapes. You will use one or more: | # | Flow | Who enters the data | |---|---|---| | 1 | **Hosted form, fixed questions** — patient gets a link, fills a single-submit form | Patient (via SNOW-hosted UI) | | 2 | **Hosted form, full UX** — questions + uploads + voice, iterative | Patient (via SNOW-hosted UI) | | 3 | **Direct API** — you POST everything you have | Your app's backend | | 4 | **Hosted form, dynamic questions** — LLM asks questions one at a time | Patient + LLM (via SNOW-hosted UI) | The same API key covers all four. ### Axis 2 — the *summary type* (what comes out) An **open and growing list** of summarization products. Each is selected by the `type` field on `/v1/sessions` (or by the specialized endpoints under `/v1/summarize/*`). New types ship without breaking existing integrations: | Type | Purpose | Specialized endpoint | |---|---|---| | `pre-consult` | Pre-visit summary before an appointment | (use `/v1/sessions`) | | `clinical-note` | SOAP note from doctor input | (use `/v1/sessions`) | | `emergency` | Emergency triage card | `/v1/emergency` | | `discharge` | Discharge summary | `/v1/discharge` | | `referral` | Referral letter to a specialist | `/v1/referral` | | `lab-requisition` | Lab order | `/v1/lab-requisition` | | `medication` | Medication / pharmacy order | `/v1/pharmacy-requisition` | | `patient-instructions` | Plain-language patient handout (10+ Indian languages) | `/v1/patient-instructions` | | `radiology` | Radiology summary | (use `/v1/sessions`) | | `care-plan` | Longitudinal care plan | (use `/v1/sessions`) | | `transfer` | Inter-facility transfer summary | (use `/v1/sessions`) | | `history` | Uploads-only history extraction | (use `/v1/sessions`) | | `international` | International Patient Summary (IPS, FHIR-bundled) | `/v1/patient-summary?standard=ips` | This list grows. As SNOW ships new summary products, they appear here as new `type` values and (where useful) new specialized endpoints. **Existing integrations continue working unchanged** when new types are added — you only need to opt in to a new type when you want to use it. Per-type recipe pages live in `docs/integrations/recipes/.md` (one page per summary type, added as each ships). This document covers the shared mechanics (auth, flows, webhooks, error model). Use this guide *plus* the recipe(s) you need. --- ## Authentication ### One key for all endpoints ``` Authorization: Bearer sn_live_ ``` Mint the key in the SNOW developer console (`platform.snowmed.health` → API keys). Console access is gated by Google login. The key itself, once minted, is what you pass on every server-to-server call — never a Google token. **Test vs live:** - `sn_test_*` — sandbox; emits webhooks, does not run the real LLM pipeline. Use during development. - `sn_live_*` — production; real summarization, real billing. **Never expose the key in your mobile or web client.** All calls must go through your backend. ### Optional but recommended headers | Header | Purpose | |---|---| | `Idempotency-Key: ` | Replays return the cached response (24h TTL). Body mismatches return 409. | | `Content-Type: application/json` | Required on POST/PUT | ### Tenant context Your API key is tenant-scoped. Every call is attributed to your tenant automatically. You don't pass a tenant ID. --- ## The 4 flows — pick yours ### Decision tree ``` Does your app already have all the data you want summarized? ├── Yes → Flow 3 (direct API, no patient UI) └── No → patient enters data via a SNOW-hosted form │ ├── Just questions, no uploads, no voice → Flow 1 (intake-sessions) ├── Questions + uploads + voice, iterative → Flow 2 (sessions, patient-link) └── Dynamic LLM-driven questions + uploads → Flow 4 (sessions, patient-link, no template) ``` You can use multiple flows. Example: emergency triage = Flow 3, scheduled pre-visit = Flow 1, doctor-driven deep-dive = Flow 4. --- ## Flow 1 — hosted form, fixed questions, single-submit **Use when:** you want a low-friction WhatsApp/SMS/email link that drops the patient into a one-page form. No uploads, no voice. ### Request ```bash POST /v1/intake-sessions Authorization: Bearer sn_live_ Content-Type: application/json Idempotency-Key: # optional { "specialty": "obstetrics", "patient_id": "", # optional "appointment_id": "", # optional "expires_in_minutes": 60, # optional, default 60 "surface": "previsit" # optional, default previsit } ``` ### Response ```json { "session_id": "aae5c6ed-51ad-4773-b73b-62101b82e29a", "token": "9jiGQhmCV9VEDNR0bK4UT6gI", "intake_url": "https://previsit.summary.to/intake/9jiGQhmCV9VEDNR0bK4UT6gI", "specialty": "obstetrics", "expires_at": "2026-05-08T09:09:22.706Z", "created_at": "2026-05-08T08:09:22.709Z" } ``` You then deliver `intake_url` to the patient via WhatsApp / SMS / email. ### What the patient sees A single-page form rendered from a SNOW-catalog template matching the specialty (`obstetrics` → `anc_first_visit`, multilingual). Patient fills, submits, sees a thank-you screen. ### Webhooks you receive | Event | When | |---|---| | `intake_session.created` | Right after your POST | | `intake_session.submitted` | Patient submits the form | | `intake_session.completed` | SNOW finishes summarization (~30-60s after submit) | | `intake_session.failed` | Processing fails | Then `GET /v1/intake-sessions/` returns the full result. ### Specialties supported (form templates seeded today) `obstetrics`, `gynecology`, `pediatrics`, `dermatology`, `ent`, `psychiatry`, `ophthalmology`, `dentistry`, `cardiology`, `orthopedics`, `general`. Unknown specialties fall back to `general`. --- ## Flow 2 — hosted form, full UX (questions + uploads + voice) **Use when:** patients should be able to upload prior reports (PDFs, lab images, USG scans) and/or speak their answers via mic. ### Request ```bash POST /v1/sessions Authorization: Bearer sn_live_ Content-Type: application/json { "type": "pre-consult", "specialty": "obstetrics", "initiator": "patient", "template_code": "anc_first_visit", # optional — pin a specific template "patient_id": "", "appointment_id":"", "metadata": { "source": "" } } ``` ### Response ```json { "session_id": "3b6f1970-9528-49e9-9d96-7934b8b6b2de", "token": "bRJIY1oc8BgtMCucjWfPJk3P", "type": "pre-consult", "specialty": "obstetrics", "session_url": "https://previsit.summary.to/obstetrics/bRJIY1oc8BgtMCucjWfPJk3P", "expires_at": "2026-05-08T11:24:59.665Z", "ui_type": "patient" } ``` ### What the patient sees A multi-step page: 1. **Intro** — clinic + doctor info 2. **Iterative Q&A** — one question at a time, voice input optional 3. **Uploads** — camera + file picker for prior reports 4. **Review + submit** Voice transcription, file uploads to R2, idempotent submit — all built in. ### Webhooks `session.completed` (or `.failed`) with the structured summary. --- ## Flow 3 — direct API, you have the data **Use when:** your app already collected the data — from your own UI, your DB, your AVI red-flag detection — and you just want a summary back. No patient-facing URL. ### Request ```bash POST /v1/sessions { "type": "pre-consult", # or "emergency", "discharge", etc. "specialty": "obstetrics", "initiator": "system", # or "doctor", "patient" "patient": { "id": "", "name": "Optional", "age": 28, "gravida": 2, "para": 1, "lmp": "2026-02-14" }, "document_ids": [ "", "" ], # see Documents API "custom_questions": [ { "id": "chief_complaint", "question": "Reason for visit", "answer": "Routine ANC checkup" } ], "metadata": { "source": "", "user_id": "", "urgency": "emergency" # only for emergency type }, "red_flags": [ # optional, accepted for emergency { "type": "bleeding", "severity": "high" } ] } ``` ### Response ```json { "session_id": "...", "token": "...", # included even though you may not use it "type": "pre-consult", "specialty": "obstetrics", "expires_at": "...", "ui_type": "clinician" } ``` You don't need to share the URL with anyone. Either wait for the `session.completed` webhook, or poll: ```bash GET /v1/sessions/ ``` ### Valid `type` values ``` pre-consult, clinical-note, discharge, referral, transfer, medication, lab-requisition, history, international, emergency, emergency-summary, pre-anc-visit, pre-visit-clinic, care-plan, radiology ``` Aliases auto-mapped server-side: `emergency-summary → emergency`, `pre-anc-visit → pre-consult`. ### Emergency lane Set `metadata.urgency: "emergency"` to route the session through the priority queue (target SLA < 30s). --- ## Flow 4 — hosted form, LLM-driven dynamic questions **Use when:** you want the question list to adapt based on prior answers, rather than being fixed. Same hosted UI as Flow 2; the difference is **no `template_code` and no `custom_questions[]`** — the server generates each next question via LLM. ### Request ```bash POST /v1/sessions { "type": "pre-consult", "specialty": "obstetrics", "purpose": "second-trimester checkup", # optional, biases the LLM "language": "en", # or "hi", "te", "mr" "initiator": "doctor", "patient_id": "", "metadata": { "source": "" } } ``` Response and patient UX are identical to Flow 2. Internally, each turn calls `GET /v1/sessions/:token/next-question` which produces the next question conditioned on prior answers. --- ## Documents API — uploading reports from your backend Used in Flow 3 to attach prior reports to a session. ### Step 1 — get a presigned URL ```bash POST /v1/documents/upload-url { "filename": "prior_usg.pdf", "mimeType": "application/pdf", "size_bytes": 245678 } ``` **Response:** ```json { "upload_url": "https://snow-files.<...>.r2.cloudflarestorage.com/...?X-Amz-Signature=...", "document_id": "99382880-cdde-4dae-8174-d03984fec4e7", "storage_key": "//" } ``` ### Step 2 — upload the file ```bash PUT Content-Type: application/pdf ``` No auth header — the presigned URL is the credential. ### Step 3 — trigger processing ```bash POST /v1/documents//process ``` ### Step 4 — fetch result ```bash GET /v1/documents/ ``` Status: `pending` → `processing` → `completed`. Once complete, pass `document_id` into a `/v1/sessions` body (Flow 3). ### Patient-driven uploads (Flow 2) When the patient uploads from the hosted form, they hit `POST /v1/sessions/:token/uploads/presign` directly — no auth needed; the session token authenticates. Files attach to the session automatically. --- ## Webhooks ### Configure once Via the developer console (`platform.snowmed.health` → Webhooks) or via API: ```bash PUT /v1/me/webhook { "url": "https://your-app.example.com/webhooks/snow", "secret": "whsec_" } ``` You receive a `whsec_*` secret on creation. **Save it immediately** — it won't be shown again. Used for HMAC verification. ### Headers on every delivery ``` X-Snow-Signature: v1,t=,sig= X-Snow-Event: session.completed X-Snow-Delivery-Id: Content-Type: application/json ``` ### Verifying the signature ```typescript import crypto from "crypto" function verify(body: string, header: string, secret: string): boolean { const m = header.match(/v1,t=([^,]+),sig=([a-f0-9]+)/) if (!m) return false const [, ts, sig] = m const age = Date.now() / 1000 - Number(ts) if (age > 300) return false // 5-minute replay window const expected = crypto .createHmac("sha256", secret) .update(`${ts}.${body}`) .digest("hex") return crypto.timingSafeEqual( Buffer.from(sig, "hex"), Buffer.from(expected, "hex"), ) } ``` Body MUST be the raw bytes (don't `JSON.parse` then re-stringify). ### Events | Event | Fires when | Key fields | |---|---|---| | `session.completed` | `/v1/sessions` summarization done | `session_id`, `type`, `summary`, `triage_tier` | | `session.failed` | Same, on failure | `session_id`, `error` | | `session.test` | Daily test cron (opt-in) | static test payload | | `intake_session.created` | After `POST /v1/intake-sessions` | `session_id`, `token`, `intake_url` | | `intake_session.submitted` | Patient submits hosted form | `session_id` | | `intake_session.processing` | Worker picks up job | `session_id` | | `intake_session.completed` | Summarization done | `session_id`, `summary`, `triage_tier` | | `intake_session.failed` | Processing fails | `session_id`, `error` | ### Idempotency on your end Every delivery has `X-Snow-Delivery-Id`. Dedupe on it — SNOW retries with the same ID on transient failures. ### Delivery model - **Inline first attempt** at event-emit time - **Recovery sweep** retries every 5 min for failed/in-flight deliveries up to 24h - **Delivery log** visible in the developer console - Replay manually via `POST /v1/me/webhook/deliveries/:id/replay` --- ## Lifecycle examples ### Flow 1 (intake-sessions, single-submit) ``` Your backend SNOW ───────────── ───────── 1. POST /v1/intake-sessions ──────────────────────► 201 + token + intake_url ◄─── webhook intake_session.created 2. Send WhatsApp link 3. (patient fills form, submits) ◄─── webhook intake_session.submitted ◄─── webhook intake_session.completed 4. GET /v1/intake-sessions/ ────────────────► 200 + result 5. Render summary in your UI ``` ### Flow 3 (direct, emergency) ``` 1. (your app detects emergency from AVI / red-flags) 2. POST /v1/sessions {type:"emergency",...} ─────────► 201 + session_id ◄─── webhook session.completed (priority lane, ~30s) 3. Render triage card in patient + doctor views ``` --- ## Common gotchas - **Don't expose the API key client-side.** Mobile/web clients must call your backend, which calls SNOW. - **Idempotency-Key on POSTs.** Network retries are common; without Idempotency-Key you may double-create sessions. - **Use raw body for HMAC verification.** Re-stringifying parsed JSON gives a different byte sequence and breaks verification. - **Token expiry is 60 minutes by default.** Patients on slow flows may need extension; pass `expires_in_minutes` to override. - **Specialty fallback.** If you pass an unsupported specialty, SNOW falls back to the `general` template silently. Pass a known one (see the specialty list above). - **Webhook URL must be HTTPS** in production. HTTP is rejected. - **Replay window:** 5 min. Webhook deliveries older than 5 min should be rejected by your verifier. --- ## Smoke-test checklist Before going live, confirm each of these end-to-end: ### Auth + console - [ ] Logged into `platform.snowmed.health` via Google - [ ] Minted a `sn_test_*` key - [ ] Minted a `sn_live_*` key (separate label) - [ ] Configured webhook URL + saved the `whsec_*` secret ### Flow 1 (if you'll use intake-sessions) - [ ] `POST /v1/intake-sessions` returns 201 + token + URL - [ ] Opening `intake_url` in a browser renders the form - [ ] Submitting the form returns the thank-you screen - [ ] `intake_session.submitted` webhook arrives within 5s - [ ] HMAC signature verifies with your secret - [ ] `intake_session.completed` webhook arrives within 60s - [ ] `GET /v1/intake-sessions/` returns the result - [ ] Idempotency-Key replay returns the same body ### Flow 2 / Flow 4 (if you'll use sessions patient-link) - [ ] `POST /v1/sessions` returns 201 + `session_url` - [ ] Opening `session_url` renders the iterative form - [ ] Voice/upload steps work on a real device - [ ] `session.completed` webhook arrives ### Flow 3 (if you'll use sessions direct mode) - [ ] `POST /v1/sessions` (with `patient`, `document_ids`, `metadata`) returns 201 - [ ] `session.completed` webhook arrives (or `GET /v1/sessions/` returns the result) ### Webhook receiver - [ ] Receives `session.test` from the daily cron (or via `POST /v1/me/webhook/test`) - [ ] HMAC verifies - [ ] Handler is idempotent on `X-Snow-Delivery-Id` - [ ] Returns 2xx within 5s (otherwise SNOW retries) --- ## API reference (quick) | Endpoint | Auth | Used in | |---|---|---| | `POST /v1/intake-sessions` | API key | Flow 1 | | `GET /v1/intake-sessions/:token` | API key | Flow 1 (poll result) | | `GET /v1/intake-sessions/:token/info` | none | Flow 1 (browser, public) | | `POST /v1/intake-sessions/:token/submit` | none | Flow 1 (browser, public) | | `POST /v1/sessions` | API key | Flows 2, 3, 4 | | `GET /v1/sessions/:id` | API key | Flow 3 (poll result) | | `GET /v1/sessions/:token/info` | none | Flows 2, 4 (browser, public) | | `GET /v1/sessions/:token/next-question` | none | Flow 4 (LLM dynamic) | | `POST /v1/sessions/:token/answer` | none | Flow 2/4 (per turn) | | `POST /v1/sessions/:token/uploads/presign` | none | Flow 2 (browser uploads) | | `POST /v1/sessions/:token/submit` | none | Flow 2/4 final submit | | `POST /v1/documents/upload-url` | API key | Flow 3 prep | | `POST /v1/documents/:id/process` | API key | Flow 3 prep | | `GET /v1/documents/:id` | API key | Flow 3 prep | | `PUT /v1/me/webhook` | API key + dev role | Setup | | `GET /v1/me/webhook/deliveries` | API key | Debugging | | `POST /v1/me/webhook/deliveries/:id/replay` | API key | Recovery | Full reference: see per-endpoint pages in the developer console. --- ## Pilot validation matrix This guide is being validated against three live integrators. Each one exercises a different combination of flows. If any of them can't fully self-serve from this doc, the doc has a gap. | Integrator | Flow 1 | Flow 2 | Flow 3 | Flow 4 | Status | |---|---|---|---|---|---| | preg.care (PregCare patient app) | ✓ ANC pre-visit | optional | ✓ Emergency triage | optional | Pilot | | PHR-Connect (care.healthreports.online) | optional | ✓ Patient uploads + voice | ✓ Direct mode | optional | Pilot | | clinic.preg.care (Klarity Gynaec) | optional | ✓ Hosted form with custom_questions | ✓ Doctor-typed clinical-note | ✓ Dynamic deep-dive | Pilot | Track per-pilot blockers in `docs/integrations/integration-readiness-status.md`. --- ## How this guide extends as SNOW ships new summary types This document describes the **mechanics** — auth, the 4 flows, webhooks, documents, error model. These don't change as new summary types ship. Each new summary type ships with **one new recipe page** under `docs/integrations/recipes/`: ``` docs/integrations/ ├── integration-guide.md ← you are here (mechanics, stable) └── recipes/ ├── pre-consult.md ← per-type recipe (request body, output schema, examples) ├── emergency.md ├── discharge.md ├── referral.md ├── lab-requisition.md ├── medication.md ├── patient-instructions.md └── ... ← new types added here without touching this guide ``` A recipe page covers what's **specific to that summary type**: - Recommended `type` value and any aliases - Required fields beyond the shared ones (e.g., `discharge` may need `admit_date`, `discharge_date`) - Output schema (what the `summary` field looks like for this type) - Example request + example response - Specialty-specific notes (if applicable) - Pricing tier (if different from base) When a new summary product ships, integrators get an update like: > *"SNOW just shipped `lab-requisition` summarization. See > `docs/integrations/recipes/lab-requisition.md`. Existing integrations > are unaffected — opt in by passing `type: "lab-requisition"` on > `/v1/sessions` (or use the `/v1/lab-requisition` endpoint directly)."* You never need to re-read this whole guide when a new type ships. Read the recipe; everything else stays the same. ## When this doc is wrong or incomplete - Wrong endpoint shape → file an issue with `session_id` + curl reproducer - Missing flow → describe the use case; we'll either map it to an existing flow or add a fifth - Webhook delivery problem → share `delivery_id`; we can trace through the delivery log This doc is the source of truth. Per-integrator notes (e.g., `pregcare-cutover-2026-05-07.md`) are negotiation snapshots, not contracts. --- # Section 2: Pre-Consult Recipe # Recipe — `pre-consult` > Read [`../integration-guide.md`](../integration-guide.md) first for auth, > webhook verification, and the 4 flows. This page only covers what's > specific to the `pre-consult` summary type. ## What it is A pre-visit clinical summary intended for the doctor to read **before** the patient walks in (or before the tele-consult begins). Built from any combination of: - Patient questionnaire answers (typed, selected, or voice-transcribed) - Uploaded prior reports (lab work, imaging, discharge papers) - Pre-collected structured data the integrator already holds (DB rows, vital signs, prior visit notes) Output emphasizes **chief complaint, relevant history, current medications, red flags, and a one-paragraph clinical narrative.** ## Recommended flows Any flow works. Common combinations: | Use case | Flow | |---|---| | WhatsApp pre-visit form, no uploads | Flow 1 (`/v1/intake-sessions`) | | WhatsApp pre-visit form, with uploads + voice | Flow 2 (`/v1/sessions` patient-link) | | Integrator's own form already collected the data | Flow 3 (direct mode) | | Doctor wants the LLM to ask follow-up questions | Flow 4 (dynamic) | ## Type aliases | Alias | Mapped to | |---|---| | `pre-anc-visit` | `pre-consult` (PregCare/Klarity backward compat) | | `pre-visit-clinic` | `pre-consult` | ## Specialty handling `specialty` is **highly recommended** — it determines which template SNOW resolves for the form (Flows 1, 2) and how the LLM is biased (all flows). | Specialty | Default template | Notes | |---|---|---| | `obstetrics` | `anc_first_visit` | Multilingual ANC form (English/Hindi/Telugu/Marathi) | | `gynecology` | `gyn_pre_consult` | Standard gyn outpatient | | `pediatrics` | `peds_pre_consult` | Parent-filled, child-focused | | `cardiology` | `cardio_pre_consult` | — | | `orthopedics` | `ortho_pre_consult` | — | | `dermatology` | `derm_pre_consult` | — | | `ent` | `ent_pre_consult` | — | | `psychiatry` | `psych_pre_consult` | — | | `ophthalmology` | `ophthal_pre_consult` | — | | `dentistry` | `dental_pre_consult` | — | | `general` | `general_pre_consult` | Fallback | Override the resolved template by passing `template_code` explicitly. ## Request body — Flow 3 (direct mode) ```json { "type": "pre-consult", "specialty": "obstetrics", "initiator": "system", "patient": { "id": "", "name": "Priya Sharma", "age": 28, "gravida": 2, "para": 1, "lmp": "2026-02-14" }, "document_ids": [ "", "" ], "custom_questions": [ { "id": "chief_complaint", "question": "Reason for visit", "answer": "Routine ANC checkup, mild back pain" } ], "metadata": { "source": "", "user_id": "", "visit_type": "anc_routine" } } ``` For Flows 1, 2, 4 see the integration guide; the body is simpler (mostly just `type` + `specialty` + identifiers). ## Output schema (in `session.completed` webhook) ```json { "session_id": "...", "type": "pre-consult", "specialty": "obstetrics", "triage_tier": "routine", // "emergency" | "urgent" | "routine" "patient_id": "...", "patient_name": "Priya Sharma", "summary": { "chief_complaint": "Mild lower back pain x 1 week", "history_of_presenting_illness": "...", "relevant_history": { "obstetric": "G2P1, 16 wks GA by LMP 2026-02-14", "medical": "No HTN, no diabetes", "surgical": "Prior C-section 2024", "family": "...", "social": "..." }, "current_medications": [ "Folic acid 5mg OD", "Iron-folate" ], "allergies": [ "None" ], "red_flags": [], "narrative": "28-year-old G2P1 at 16 weeks ...", "recommended_evaluations": [ "Routine ANC labs", "USG" ], "language": "en" }, "metadata": { "template_code": "anc_first_visit", "template_version": 1, "model_version": "...", "latency_ms": 8421 }, "citations": [ /* links back to source documents/answers */ ] } ``` ## Triage tier The `triage_tier` field is set by SNOW's safety-net rules: - `emergency` — red flags present requiring same-day attention - `urgent` — needs evaluation within 24-48h - `routine` — standard scheduled visit Use this to prioritize the doctor's worklist or route emergency cases. ## Example request (Flow 1, intake-sessions) ```bash curl -X POST https://api.summary.to/v1/intake-sessions \ -H "Authorization: Bearer sn_live_" \ -H "Content-Type: application/json" \ -d '{"specialty":"obstetrics"}' ``` Response: token + `intake_url` to deliver to the patient. Patient submits. You receive `intake_session.completed` webhook with the same `summary` shape as above. ## Pricing tier Same as base platform pricing. Heavy LLM use (long iterative Flow 4 sessions, ~10 turns) costs more than a one-shot Flow 1. See `/v1/billing/credits/balance` for current consumption. ## Limitations / known gaps - Tenant branding (logo, doctor name, primary color) is returned by `/info` on Flows 1, 2, 4 but **not yet rendered** on the hosted form. v2. - Custom multi-page forms aren't supported on Flow 1 (single-submit). Use Flow 2 if you need multi-page. - LLM dynamic Q&A (Flow 4) does not yet support custom_questions pre-seeding mid-conversation; the LLM owns the flow once started. - Recipes for other types (`emergency`, `discharge`, `referral`, `lab-requisition`, `medication`, `patient-instructions`) are not yet written; see the integration guide for the inline list of supported `type` values until they're broken out. ## Related - [`../integration-guide.md`](../integration-guide.md) — auth, flows, webhooks - [`../sessions-vs-intake-sessions.md`](../sessions-vs-intake-sessions.md) — deeper dive on Flow 1 vs Flow 2 semantic differences - [`../intake-session-sample-payloads.md`](../intake-session-sample-payloads.md) — full sample payloads for Flow 1 --- # Section 3: Webhooks (v1) Authoritative. All tenants receive this payload shape with this signature scheme. ## Endpoint registration Register your webhook URL in the SNOW portal (Integration → Webhooks). On registration, SNOW issues a `whsec_<64hex>` secret. Store it in your secret manager. ## Request SNOW sends: ``` POST Content-Type: application/json X-Snow-Signature: t=,v1=[,v1=] X-Snow-Delivery: ``` Body: ```json { "id": "", "event": "session.completed", "created_at": "2026-04-23T10:15:00.000Z", "data": { ... event-specific payload ... } } ``` ## Events Every async resource emits the same four-verb lifecycle: `.{created,processing,completed,failed}`. New resources added to the platform inherit this contract — handlers can route on `event.split(".")[0]` and switch on `event.split(".")[1]` without further changes. | Event | Fires when | |--------------------------------|-------------------------------------------------------------------| | `session.created` | `POST /v1/sessions` accepted | | `session.processing` | Pipeline started | | `session.completed` | Summary ready | | `session.failed` | Summarization failed (`data.error` present) | | `session.test` | Test event triggered from the portal | | `intake_session.created` | Doctor created an intake-session token via `POST /v1/intake-sessions` | | `intake_session.submitted` | Patient submitted form via `POST /v1/intake-sessions/:token/submit` | | `intake_session.processing` | Pipeline started | | `intake_session.completed` | Intake summary ready | | `intake_session.failed` | Intake summarization failed (`data.error` present) | | `document.completed` | Document pipeline completed | | `document.failed` | Document pipeline failed | `data` always includes the client-supplied `metadata` object echoed back verbatim — use it to correlate deliveries with your own records (appointment_id, patient_id, etc.). ## Signature verification 1. Parse `X-Snow-Signature` into `t` and one or more `v1` values. 2. Reject if `|now - t| > 300s` (replay window). 3. Compute `HMAC_SHA256(secret, "${t}.${rawBody}")` hex. 4. `crypto.timingSafeEqual` against each `v1` value — accept if any matches. During a 24h secret rotation window, SNOW sends two `v1` values (old + new). Verify against your current secret; accept either. Reference Node.js verifier: ```js import crypto from "node:crypto" export function verifySnowSignature(secret, rawBody, header) { const parts = header.split(",").map(s => s.trim()) const t = Number(parts.find(p => p.startsWith("t="))?.slice(2)) const sigs = parts.filter(p => p.startsWith("v1=")).map(p => p.slice(3)) if (!Number.isFinite(t) || sigs.length === 0) return false if (Math.abs(Math.floor(Date.now()/1000) - t) > 300) return false const expected = crypto.createHmac("sha256", secret) .update(`${t}.${rawBody}`).digest("hex") const exp = Buffer.from(expected, "hex") return sigs.some(v => { const got = Buffer.from(v, "hex") return got.length === exp.length && crypto.timingSafeEqual(got, exp) }) } ``` ## Retry + idempotency - **Retry schedule:** 10s → 60s → 5m → 30m (4 attempts total). - **Success:** any 2xx response. - **Failure:** non-2xx, timeout (>10s), or connection error triggers retry. - **Idempotency:** `id` is stable across retries. Dedupe on your side by persisting `id` on first successful processing. - **Dead letters:** after 4 failed attempts, delivery is marked `dead`. Visible in `GET /v1/me/webhook/deliveries`; replay via `POST /v1/me/webhook/deliveries/:id/replay`. ## Delivery guarantees - At-least-once within the retry window. - Order is not guaranteed — process by `id` + payload content, not arrival order. - Replay window: 300 seconds. - No payload larger than 256 KB. ## Tenant ops - `GET /v1/me/webhook/deliveries` — recent deliveries with status, attempts, latency. - `POST /v1/me/webhook/deliveries/:id/replay` — reset a delivery to `pending`. - `PUT /v1/me/webhook` — rotate secret (24h grace window; both old and new valid). --- # Section 4: OpenAPI Specification (YAML) ```yaml openapi: 3.0.3 info: title: SNOW API description: | Clinical-summarization API. Turn patient history into structured actionable summaries. SNOW takes a combination of patient questions, uploaded reports, and pre-collected structured data, and returns a clinician-grade structured summary. One API key, four integration flows, real-time webhooks. ## Authentication All `/v1/*` endpoints require an API key: ``` Authorization: Bearer sn_live_ ``` Mint keys via the developer console at `platform.snowmed.health` (Google login). Test keys (`sn_test_*`) emit webhooks but do not run the real LLM pipeline — use them during integration development. Live keys (`sn_live_*`) cost real money per call. Public token-bound endpoints (e.g. `/v1/sessions/:token/info`) require no auth — the unguessable token is itself the credential. ## Integration flows Pick one or more based on who enters the data: | Flow | Endpoint | Who enters data | |---|---|---| | 1. Hosted form, fixed questions, single-submit | `POST /v1/intake-sessions` | Patient (SNOW-hosted UI) | | 2. Hosted form with iterative Q&A + uploads + voice | `POST /v1/sessions` (patient-link mode) | Patient (SNOW-hosted UI) | | 3. Direct API — you POST what you have | `POST /v1/sessions` | Your backend | | 4. Hosted form with LLM-driven dynamic questions | `POST /v1/sessions` (no template) | Patient + LLM | See the [integration guide](https://docs.snowmed.health/guides/integration-guide) for full lifecycle examples. ## Idempotency Pass `Idempotency-Key: ` on POST requests. Replays return the cached response body for 24h. Body-mismatch on the same key returns 409. ## Error model All errors return JSON of shape `{ "error": "", "code": }`. HTTP status codes follow standard semantics (4xx client error, 5xx server error). Common codes: 400 (validation), 401 (auth), 404 (not found), 409 (conflict / already submitted), 410 (expired), 429 (rate limit), 500 (server error). version: "1.0" contact: name: SNOW Docs url: https://docs.snowmed.health servers: - url: https://api.summary.to description: Production - url: http://localhost:3001 description: Local development security: - BearerAuth: [] tags: - name: Sessions description: | Session creation + lifecycle. Use for emergency triage (`type: emergency`), pre-consult (`type: pre-consult`), discharge, referral, and other summary types. Powers Flows 2, 3, and 4. - name: Sessions (public) description: | Public token-bound endpoints used by the patient-facing hosted form on `previsit.summary.to`. Auth is the unguessable token in the URL — no API key. - name: Intake Sessions description: | Single-submit hosted form for fixed-questionnaire pre-visit flows. Powers Flow 1. Patient lands on `previsit.summary.to/intake/` and submits once. - name: Intake Sessions (public) description: | Public token-bound endpoints used by the patient-facing intake form. - name: Documents description: | Upload + process medical documents. Returned `document_id` can be passed into `POST /v1/sessions` (Flow 3) to attach prior reports to a summary. - name: Webhooks description: | Configure your webhook receiver and inspect deliveries. SNOW emits `session.completed`, `intake_session.completed`, and related events. components: securitySchemes: BearerAuth: type: http scheme: bearer description: "API key (`sn_live_*` or `sn_test_*`) issued via the developer console" schemas: Error: type: object required: [error] properties: error: { type: string, example: "Invalid link" } code: { type: integer, example: 1003 } Idempotency-Key: type: string format: uuid description: Optional UUID to deduplicate retries. 24h TTL. Session: type: object properties: id: { type: string, format: uuid } session_id: { type: string, format: uuid } token: { type: string, description: "Opaque public token used in patient-facing URLs" } type: { $ref: "#/components/schemas/SessionType" } specialty: { type: string, nullable: true, example: "obstetrics" } session_url: { type: string, format: uri, example: "https://previsit.summary.to/obstetrics/bRJIY1oc8BgtMCucjWfPJk3P" } expires_at: { type: string, format: date-time } created_at: { type: string, format: date-time } ui_type: { type: string, enum: [patient, clinician], description: "Hint for which UI mode the link is intended for" } SessionType: type: string enum: - pre-consult - clinical-note - discharge - referral - transfer - medication - lab-requisition - history - international - emergency - emergency-summary - pre-anc-visit - pre-visit-clinic - care-plan - radiology description: | Server-side aliases: `emergency-summary` → `emergency`, `pre-anc-visit` → `pre-consult`. SessionInfo: type: object properties: status: { type: string, enum: [pending, active, completed, expired] } type: { $ref: "#/components/schemas/SessionType" } specialty: { type: string, nullable: true } language: { type: string, example: "en" } expires_at: { type: string, format: date-time } patient_first_name: { type: string, nullable: true } doctor: { type: string, nullable: true } doctor_credentials: { type: string, nullable: true } clinic_name: { type: string, nullable: true } logo_url: { type: string, format: uri, nullable: true } primary_color: { type: string, nullable: true } appointment_date: { type: string, format: date-time, nullable: true } NextQuestion: type: object properties: question: { type: string, example: "What brings you to the obstetrics clinic today?" } question_type: { type: string, enum: [text, number, yes_no, scale_1_10, date, choice] } choices: { type: array, items: { type: string }, nullable: true } clinical_purpose: { type: string, nullable: true } is_final_question: { type: boolean } questions_remaining_estimate: { type: integer } done: { type: boolean, nullable: true } SessionResult: type: object properties: session_id: { type: string, format: uuid } type: { $ref: "#/components/schemas/SessionType" } specialty: { type: string, nullable: true } triage_tier: { type: string, enum: [emergency, urgent, routine], description: "Triage classification produced by SNOW's safety-net rules" } patient_id: { type: string, format: uuid, nullable: true } patient_name: { type: string, nullable: true } summary: { type: object, description: "Structured summary; shape varies by `type` (see recipe pages)" } metadata: type: object properties: template_code: { type: string, nullable: true } template_version: { type: integer, nullable: true } model_version: { type: string, nullable: true } latency_ms: { type: integer, nullable: true } citations: { type: array, items: { type: object } } PresignResponse: type: object properties: upload_url: { type: string, format: uri, description: "Pre-signed R2 PUT URL, expires in 15 minutes" } document_id: { type: string, format: uuid } storage_key: { type: string } # Legacy field aliases (kept for backward compat — prefer the snake_case ones above) uploadUrl: { type: string, format: uri } r2Key: { type: string } documentId: { type: string, format: uuid } IntakeSession: type: object properties: session_id: { type: string, format: uuid } token: { type: string } intake_url: { type: string, format: uri, example: "https://previsit.summary.to/intake/9jiGQhmCV9VEDNR0bK4UT6gI" } specialty: { type: string, example: "obstetrics" } expires_at: { type: string, format: date-time } created_at: { type: string, format: date-time } IntakeSessionInfo: type: object properties: status: { type: string, enum: [pending], description: "Always `pending` when /info returns 200; expired/submitted return 410/409" } specialty: { type: string, nullable: true } expires_at: { type: string, format: date-time } template: type: object nullable: true properties: code: { type: string, example: "anc_first_visit" } version: { type: integer } name: { type: string } source: { type: string, enum: [tenant_visit_type, tenant_specialty, catalog_specialty, catalog_general] } form_schema: type: object properties: sections: type: array items: type: object properties: title: { type: string } questions: type: array items: type: object properties: id: { type: string } type: { type: string, enum: [text, number, yes_no, scale, date, choice, multi_choice] } label: { type: string } required: { type: boolean } options: { type: array, items: { type: string }, nullable: true } doctor_name: { type: string, nullable: true } clinic_name: { type: string, nullable: true } logo_url: { type: string, format: uri, nullable: true } IntakeSessionRecord: type: object properties: id: { type: string, format: uuid } token: { type: string } specialty: { type: string } patient_id: { type: string, format: uuid, nullable: true } appointment_id: { type: string, format: uuid, nullable: true } status: { type: string, enum: [pending, submitted, processing, completed, failed, expired] } form_responses: { type: object, nullable: true } patient_name: { type: string, nullable: true } phone: { type: string, nullable: true } result: { type: object, nullable: true } summary_id: { type: string, format: uuid, nullable: true } document_id: { type: string, format: uuid, nullable: true } expires_at: { type: string, format: date-time } submitted_at: { type: string, format: date-time, nullable: true } completed_at: { type: string, format: date-time, nullable: true } created_at: { type: string, format: date-time } DocumentPresign: type: object properties: upload_url: { type: string, format: uri } document_id: { type: string, format: uuid } storage_key: { type: string } Document: type: object properties: id: { type: string, format: uuid } mimeType: { type: string } docType: { type: string, nullable: true } isHandwritten: { type: boolean } status: { type: string, enum: [pending, processing, completed, failed] } ocrConfidence: { type: number, nullable: true } ocrEngine: { type: string, nullable: true } parsed_text: { type: string, nullable: true } summary: { type: object, nullable: true } createdAt: { type: string, format: date-time } WebhookConfig: type: object properties: url: { type: string, format: uri, example: "https://api.preg.care/api/v1/webhooks/snow" } secret: { type: string, example: "whsec_<32-byte-hex>", description: "HMAC signing secret. Returned ONCE on creation." } active: { type: boolean } created_at: { type: string, format: date-time } WebhookDelivery: type: object properties: id: { type: string, format: uuid } event: { type: string, example: "session.completed" } url: { type: string, format: uri } request_body: { type: object } response_status: { type: integer, nullable: true } response_body: { type: string, nullable: true } attempts: { type: integer } status: { type: string, enum: [pending, in_flight, delivered, failed, dead] } created_at: { type: string, format: date-time } delivered_at: { type: string, format: date-time, nullable: true } paths: # ── Sessions ───────────────────────────────────────────────────────────────── /v1/sessions: post: tags: [Sessions] summary: Create a session description: | Creates a clinical-summarization session. Powers Flows 2, 3, and 4 depending on which fields you pass: - **Flow 2 (patient-link with full UX):** pass `template_code` or `custom_questions[]` — patient gets a hosted URL with iterative Q&A + uploads + voice - **Flow 3 (direct mode):** pass `patient`, `document_ids[]`, and complete `custom_questions[]` (with answers) — your backend has the data, no patient UI needed - **Flow 4 (LLM-driven dynamic):** omit `template_code` and `custom_questions[]` — LLM generates each next question based on prior answers operationId: createSession security: - BearerAuth: [] parameters: - in: header name: Idempotency-Key required: false schema: { $ref: "#/components/schemas/Idempotency-Key" } requestBody: required: true content: application/json: schema: type: object required: [type] properties: type: { $ref: "#/components/schemas/SessionType" } specialty: { type: string, example: "obstetrics" } purpose: { type: string, nullable: true, description: "Visit purpose (e.g. 'second-trimester checkup'); biases LLM" } language: { type: string, example: "en", description: "ISO 639-1 code; supported: en, hi, te, mr" } initiator: { type: string, enum: [patient, doctor, system], default: system } template_code: { type: string, nullable: true, description: "Pin a specific template; omit for catalog auto-resolve or LLM dynamic" } custom_questions: type: array description: "Pre-defined questions; if all have `answer`, this is Flow 3 direct mode" items: type: object properties: id: { type: string } question: { type: string } answer: { type: string, nullable: true } patient_id: { type: string, format: uuid, nullable: true } appointment_id: { type: string, format: uuid, nullable: true } patient: { type: object, nullable: true, description: "Structured patient data for Flow 3 direct mode" } document_ids: { type: array, items: { type: string, format: uuid }, nullable: true } metadata: { type: object, additionalProperties: true, description: "Free-form key-value pairs; common keys: source, user_id, urgency, visit_type" } red_flags: { type: array, items: { type: object }, nullable: true, description: "Pre-detected red flags (Flow 3 emergency)" } clinic_info: { type: object, nullable: true } expires_in_minutes: { type: integer, default: 60 } example: type: pre-consult specialty: obstetrics initiator: patient template_code: anc_first_visit patient_id: c2c5... metadata: { source: preg.care } responses: '201': description: Session created content: application/json: schema: { $ref: "#/components/schemas/Session" } '400': { description: Validation error, content: { application/json: { schema: { $ref: "#/components/schemas/Error" }}}} '401': { description: Invalid API key, content: { application/json: { schema: { $ref: "#/components/schemas/Error" }}}} '409': { description: Idempotency-Key body mismatch, content: { application/json: { schema: { $ref: "#/components/schemas/Error" }}}} '429': { description: Rate limit / quota exceeded, content: { application/json: { schema: { $ref: "#/components/schemas/Error" }}}} /v1/sessions/{id}: get: tags: [Sessions] summary: Fetch a session by id description: Returns the full session record including `result` (the structured summary) once `status` is `completed`. operationId: getSession security: - BearerAuth: [] parameters: - in: path name: id required: true schema: { type: string, format: uuid } responses: '200': description: Session record content: application/json: schema: type: object allOf: - $ref: "#/components/schemas/Session" - type: object properties: status: { type: string, enum: [pending, active, completed, failed, expired] } result: { $ref: "#/components/schemas/SessionResult" } '404': { description: Session not found, content: { application/json: { schema: { $ref: "#/components/schemas/Error" }}}} # ── Sessions — public token-bound (no auth) ────────────────────────────────── /v1/sessions/{token}/info: get: tags: [Sessions (public)] summary: Patient form intro info description: | Public endpoint. Returns intro/branding info for the patient-facing hosted form. No auth required — the unguessable token IS the credential. operationId: getSessionInfo security: [] parameters: - in: path name: token required: true schema: { type: string } responses: '200': { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/SessionInfo" }}}} '404': { description: Invalid link, content: { application/json: { schema: { $ref: "#/components/schemas/Error" }}}} '410': { description: Session expired, content: { application/json: { schema: { $ref: "#/components/schemas/Error" }}}} /v1/sessions/{token}/next-question: get: tags: [Sessions (public)] summary: Get the next question (LLM-driven) description: | Returns the next question in the iterative Q&A flow. LLM-generated, conditioned on prior answers. Returns `{done: true}` when all questions answered. operationId: getNextQuestion security: [] parameters: - in: path name: token required: true schema: { type: string } responses: '200': { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/NextQuestion" }}}} '404': { description: Session not found or expired, content: { application/json: { schema: { $ref: "#/components/schemas/Error" }}}} '500': { description: Could not generate question (LLM failure), content: { application/json: { schema: { $ref: "#/components/schemas/Error" }}}} /v1/sessions/{token}/answer: post: tags: [Sessions (public)] summary: Submit an answer (per-turn) description: | Records the patient's answer to question N and triggers generation of question N+1. Used by the iterative Q&A hosted form. operationId: submitAnswer security: [] parameters: - in: path name: token required: true schema: { type: string } requestBody: required: true content: application/json: schema: type: object required: [question_index, answer] properties: question_index: { type: integer } answer: { type: string } responses: '200': description: Next question or done content: application/json: schema: { $ref: "#/components/schemas/NextQuestion" } /v1/sessions/{token}/uploads/presign: post: tags: [Sessions (public)] summary: Get a presigned R2 URL for patient upload description: | Public endpoint used by the patient-facing hosted form to upload prior reports (PDFs, images). Returns an R2 PUT URL. Files attach to the session automatically. operationId: presignSessionUpload security: [] parameters: - in: path name: token required: true schema: { type: string } requestBody: required: true content: application/json: schema: type: object required: [filename, mimeType] properties: filename: { type: string, example: "prior_usg.pdf" } mimeType: { type: string, example: "application/pdf" } size_bytes: { type: integer, example: 245678, description: "Optional; max 50MB" } responses: '200': { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/PresignResponse" }}}} '400': { description: Validation error, content: { application/json: { schema: { $ref: "#/components/schemas/Error" }}}} '413': { description: File too large, content: { application/json: { schema: { $ref: "#/components/schemas/Error" }}}} /v1/sessions/{token}/submit: post: tags: [Sessions (public)] summary: Final submit — patient finishes the form description: | Marks the session as submitted. Triggers async summarization. Patient gets instant ACK; webhook fires when summary is ready. operationId: submitSession security: [] parameters: - in: path name: token required: true schema: { type: string } responses: '200': description: Submitted; summarization queued content: application/json: schema: type: object properties: ok: { type: boolean } message: { type: string } # ── Intake Sessions ────────────────────────────────────────────────────────── /v1/intake-sessions: post: tags: [Intake Sessions] summary: Create a single-submit intake session (Flow 1) description: | Mints a token + hosted URL for a single-submit pre-visit form. Patient lands on `previsit.summary.to/intake/`, fills the form, submits once. Form schema is resolved server-side from the catalog by `specialty`. Use this for low-friction WhatsApp/SMS pre-visit forms with no uploads or voice. For uploads/voice, use `POST /v1/sessions` (Flow 2) instead. operationId: createIntakeSession security: - BearerAuth: [] parameters: - in: header name: Idempotency-Key required: false schema: { $ref: "#/components/schemas/Idempotency-Key" } requestBody: required: true content: application/json: schema: type: object required: [specialty] properties: specialty: { type: string, example: "obstetrics" } patient_id: { type: string, format: uuid, nullable: true } appointment_id: { type: string, format: uuid, nullable: true } expires_in_minutes: { type: integer, default: 60 } surface: { type: string, enum: [previsit, intake, discharge, referral, emergency], default: previsit } example: specialty: obstetrics patient_id: c2c5... appointment_id: 7f4a... responses: '201': description: Intake session created content: application/json: schema: { $ref: "#/components/schemas/IntakeSession" } '400': { description: Validation error, content: { application/json: { schema: { $ref: "#/components/schemas/Error" }}}} '401': { description: Invalid API key, content: { application/json: { schema: { $ref: "#/components/schemas/Error" }}}} '404': { description: Patient not found in this tenant, content: { application/json: { schema: { $ref: "#/components/schemas/Error" }}}} '429': { description: Daily quota exceeded, content: { application/json: { schema: { $ref: "#/components/schemas/Error" }}}} get: tags: [Intake Sessions] summary: List intake sessions for this tenant description: Returns up to 50 most recent intake sessions. Optionally filter by `status`. operationId: listIntakeSessions security: - BearerAuth: [] parameters: - in: query name: status schema: { type: string, enum: [pending, submitted, processing, completed, failed, expired] } responses: '200': description: List content: application/json: schema: type: object properties: sessions: type: array items: { $ref: "#/components/schemas/IntakeSessionRecord" } /v1/intake-sessions/{token}: get: tags: [Intake Sessions] summary: Fetch an intake session by token description: Returns the full intake session record including `result` once status is `completed`. operationId: getIntakeSession security: - BearerAuth: [] parameters: - in: path name: token required: true schema: { type: string } responses: '200': { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/IntakeSessionRecord" }}}} '404': { description: Session not found, content: { application/json: { schema: { $ref: "#/components/schemas/Error" }}}} /v1/intake-sessions/{token}/info: get: tags: [Intake Sessions (public)] summary: Form schema for the patient-facing intake form description: | Public endpoint. Returns the form schema (sections + questions) for the patient to render at `previsit.summary.to/intake/`. Schema is resolved from the catalog template chain (tenant default → SNOW catalog by specialty → `general` fallback). Status codes are meaningful: - 200 — form is renderable (status `pending`) - 404 — token doesn't exist - 410 — token expired - 409 — already submitted operationId: getIntakeSessionInfo security: [] parameters: - in: path name: token required: true schema: { type: string } responses: '200': { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/IntakeSessionInfo" }}}} '404': { description: Invalid link, content: { application/json: { schema: { $ref: "#/components/schemas/Error" }}}} '410': { description: Expired, content: { application/json: { schema: { $ref: "#/components/schemas/Error" }}}} '409': { description: Already submitted, content: { application/json: { schema: { $ref: "#/components/schemas/Error" }}}} /v1/intake-sessions/{token}/submit: post: tags: [Intake Sessions (public)] summary: Submit the intake form description: | Public endpoint. Patient submits all `form_responses` in one POST. Triggers async summarization; patient gets instant ACK and a thank-you screen. operationId: submitIntakeSession security: [] parameters: - in: path name: token required: true schema: { type: string } requestBody: required: true content: application/json: schema: type: object required: [form_responses] properties: form_responses: { type: object, additionalProperties: true } patient_name: { type: string, nullable: true } phone: { type: string, nullable: true } responses: '200': description: Submitted content: application/json: schema: type: object properties: ok: { type: boolean } message: { type: string } '400': { description: form_responses missing or empty, content: { application/json: { schema: { $ref: "#/components/schemas/Error" }}}} '404': { description: Invalid link, content: { application/json: { schema: { $ref: "#/components/schemas/Error" }}}} '409': { description: Already submitted, content: { application/json: { schema: { $ref: "#/components/schemas/Error" }}}} '410': { description: Expired, content: { application/json: { schema: { $ref: "#/components/schemas/Error" }}}} # ── Documents ──────────────────────────────────────────────────────────────── /v1/documents/upload-url: post: tags: [Documents] summary: Get a presigned URL for document upload description: | Returns a pre-signed R2 PUT URL valid for 15 minutes. Upload the file binary directly to the URL, then call `POST /v1/documents/{id}/process` to trigger OCR + extraction. operationId: getDocumentUploadUrl security: - BearerAuth: [] requestBody: required: true content: application/json: schema: type: object required: [filename, mimeType] properties: filename: { type: string } mimeType: { type: string } size_bytes: { type: integer, nullable: true } responses: '200': { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/DocumentPresign" }}}} '400': { description: Validation error, content: { application/json: { schema: { $ref: "#/components/schemas/Error" }}}} '429': { description: Rate limit, content: { application/json: { schema: { $ref: "#/components/schemas/Error" }}}} /v1/documents/{id}: get: tags: [Documents] summary: Fetch a document description: Returns the document record with status, parsed text, and summary (when processing is complete). operationId: getDocument security: - BearerAuth: [] parameters: - in: path name: id required: true schema: { type: string, format: uuid } responses: '200': { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Document" }}}} '404': { description: Not found, content: { application/json: { schema: { $ref: "#/components/schemas/Error" }}}} # ── Webhooks ───────────────────────────────────────────────────────────────── /v1/me/webhook: get: tags: [Webhooks] summary: Get current webhook config operationId: getWebhookConfig security: - BearerAuth: [] responses: '200': { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/WebhookConfig" }}}} '404': { description: No webhook configured, content: { application/json: { schema: { $ref: "#/components/schemas/Error" }}}} put: tags: [Webhooks] summary: Set webhook URL + signing secret description: | Configure your receiver URL. Returns a `whsec_*` secret on creation — save it immediately; it's not shown again. Use it for HMAC-SHA256 signature verification on incoming deliveries. operationId: setWebhookConfig security: - BearerAuth: [] requestBody: required: true content: application/json: schema: type: object required: [url] properties: url: { type: string, format: uri, example: "https://api.preg.care/api/v1/webhooks/snow" } secret: { type: string, nullable: true, description: "Optional; SNOW generates one if omitted" } responses: '200': { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/WebhookConfig" }}}} /v1/me/webhook/test: post: tags: [Webhooks] summary: Send a test webhook description: Fires a `session.test` event to your configured receiver. Useful for verifying signature handling. operationId: testWebhook security: - BearerAuth: [] responses: '200': description: Test queued content: application/json: schema: type: object properties: ok: { type: boolean } delivery_id: { type: string, format: uuid } /v1/me/webhook/deliveries: get: tags: [Webhooks] summary: List recent webhook deliveries description: Returns up to 50 most recent deliveries for debugging. operationId: listWebhookDeliveries security: - BearerAuth: [] parameters: - in: query name: status schema: { type: string, enum: [pending, in_flight, delivered, failed, dead] } responses: '200': description: List content: application/json: schema: type: object properties: deliveries: type: array items: { $ref: "#/components/schemas/WebhookDelivery" } /v1/me/webhook/deliveries/{id}/replay: post: tags: [Webhooks] summary: Manually replay a webhook delivery description: Re-enqueues a previously-failed delivery. Same `X-Snow-Delivery-Id` for dedup. operationId: replayWebhookDelivery security: - BearerAuth: [] parameters: - in: path name: id required: true schema: { type: string, format: uuid } responses: '200': description: Re-queued content: application/json: schema: type: object properties: ok: { type: boolean } delivery_id: { type: string, format: uuid } '404': { description: Delivery not found, content: { application/json: { schema: { $ref: "#/components/schemas/Error" }}}} ```