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_<your_key>
    ```

    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: <uuid>` 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": "<message>", "code": <int> }`.
    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/<token>` 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/<token>`, 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/<token>`. 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" }}}}
