openapi: 3.1.0

info:
  title: Pain2Care API
  version: "1.1"
  summary: Partner API for chronic pain & wellness data
  description: |
    The Pain2Care API lets authorized healthcare partners and B2B integrators
    securely access self-tracked pain, wellbeing, and vitals data that patients
    have explicitly consented to share.

    **Key principles**
    - Patient consent is mandatory — no data is accessible without an explicit consent grant
    - All data is de-identified at the API layer (no PII in responses)
    - Scopes are granular — request only what your integration needs
    - Patients can revoke consent at any time from within the app

    **Patient linking flow (recommended)**

    Pain2Care uses a push-based consent model — your system sends a link request
    directly to the patient's app, the patient reviews and accepts or rejects it
    in their own time. No redirect, no OAuth dance needed on the patient side.

    ```
    1. POST /v1/link-requests   ← your system sends request with patient email + scopes
    2. Patient opens Pain2Care  ← sees pending request from your organisation
    3. Patient accepts           ← you receive consent.granted webhook + patient_id
    4. GET /v1/patients/{patient_id}/pain-entries  ← access data with your API key
    ```

    Requests expire after **7 days** if not responded to.
    Patients can revoke at any time — you receive a `consent.revoked` webhook.

    **Getting access**
    Contact [api@pain2care.com](mailto:api@pain2care.com) to request partner credentials.
    A signed Data Processing Agreement (DPA) is required before live API keys are issued.

  contact:
    name: Pain2Care API Support
    email: api@pain2care.com
    url: https://pain2care.com/developers

  license:
    name: Proprietary — partner use only
    url: https://pain2care.com/terms

servers:
  - url: https://api.pain2care.com/v1
    description: Production
  - url: https://api-sandbox.pain2care.com/v1
    description: Sandbox (test data, no real patients)

# ── Security schemes ────────────────────────────────────────────────────────

components:
  securitySchemes:

    ApiKey:
      type: apiKey
      in: header
      name: X-API-Key
      description: |
        Partner-level API key issued after DPA signing.
        Required on every request alongside the patient OAuth token.

    PatientOAuth:
      type: oauth2
      description: |
        Patient-authorized access via OAuth 2.0 Authorization Code + PKCE.
        The patient is redirected to Pain2Care to grant consent, then
        redirected back to your `redirect_uri` with an authorization code.
      flows:
        authorizationCode:
          authorizationUrl: https://pain2care.com/oauth/authorize
          tokenUrl: https://api.pain2care.com/v1/oauth/token
          refreshUrl: https://api.pain2care.com/v1/oauth/token
          scopes:
            pain:read: "Pain diary — pain level, body areas, qualities, notes"
            wellbeing:read: "Wellbeing logs — mood, sleep quality, energy, anxiety, movement"
            vitals:read: "Device measurements — HR, HRV, SpO2, blood pressure, sleep, stress (Polar, Withings, Oura, Health Connect)"
            analysis:read: "AI Pattern Analysis — AI-generated summaries of pain history and patterns"
            trends:read: Read computed trend summaries
            reports:read: Download PDF reports
            incidents:read: |
              Accident & incident reports. Requires explicit patient consent.
              Partners must hold a signed Data Processing Agreement and, where
              applicable, a legal basis under GDPR Art. 9 (sensitive data).
              Law-enforcement access requires a valid legal order presented
              to Pain2Care before API credentials are issued for this scope.

# ── Shared schemas ───────────────────────────────────────────────────────────

  schemas:

    Error:
      type: object
      required: [error, message]
      properties:
        error:
          type: string
          example: unauthorized
        message:
          type: string
          example: Missing or invalid API key

    Pagination:
      type: object
      properties:
        total:
          type: integer
          example: 42
        page:
          type: integer
          example: 1
        per_page:
          type: integer
          example: 20
        next_cursor:
          type: string
          nullable: true
          example: "eyJpZCI6MTcxNDgwMDAwMH0"

    BodyArea:
      type: object
      properties:
        id:
          type: string
          example: left_knee
        label:
          type: string
          example: Left knee
        max_intensity:
          type: integer
          minimum: 0
          maximum: 10
        subareas:
          type: object
          additionalProperties:
            type: integer
          example:
            upper_knee: 6
            lower_knee: 4

    VitalsMetrics:
      type: object
      description: All fields are nullable — only populated when the patient's device provided data
      properties:
        hr:
          type: number
          nullable: true
          description: Heart rate (bpm)
          example: 62
        hrv:
          type: number
          nullable: true
          description: Heart rate variability — RMSSD (ms)
          example: 45
        spo2:
          type: number
          nullable: true
          description: Blood oxygen saturation (%)
          example: 97.5
        blood_pressure_sys:
          type: number
          nullable: true
          description: Systolic blood pressure (mmHg)
          example: 118
        blood_pressure_dia:
          type: number
          nullable: true
          description: Diastolic blood pressure (mmHg)
          example: 76
        sleep_duration:
          type: number
          nullable: true
          description: Sleep duration (hours)
          example: 7.2
        sleep_score:
          type: integer
          nullable: true
          description: Device sleep quality score (0–100)
          example: 78
        stress:
          type: integer
          nullable: true
          description: Stress index (0–100, device-reported)
          example: 32
        blood_glucose:
          type: number
          nullable: true
          description: Blood glucose (mmol/L)
          example: 5.4
        hemoglobin:
          type: number
          nullable: true
          description: Hemoglobin (g/dL)
          example: 13.8

    VitalsData:
      type: object
      properties:
        source:
          type: string
          enum: [polar, withings, oura, health_connect, manual]
          example: polar
        measured_at:
          type: string
          format: date-time
          example: "2026-05-04T07:15:00Z"
        metrics:
          $ref: '#/components/schemas/VitalsMetrics'

    PainEntry:
      type: object
      required: [id, pain_level, logged_at]
      properties:
        id:
          type: string
          description: Unique entry ID (timestamp-based)
          example: "1746349200000"
        pain_level:
          type: integer
          minimum: 0
          maximum: 10
          example: 6
        areas:
          type: array
          items:
            $ref: '#/components/schemas/BodyArea'
        qualities:
          type: array
          description: Pain quality descriptors selected by the patient
          items:
            type: string
          example: ["burning", "throbbing"]
        notes:
          type: string
          nullable: true
          example: "Worse after sitting for 2 hours"
        overall_wellbeing:
          type: number
          nullable: true
          minimum: 1
          maximum: 5
          description: Composite wellbeing score logged alongside this entry
          example: 3.2
        logged_at:
          type: string
          format: date-time
          example: "2026-05-04T09:00:00Z"

    WellbeingEntry:
      type: object
      required: [date, score]
      properties:
        date:
          type: string
          format: date
          example: "2026-05-04"
        score:
          type: number
          minimum: 1
          maximum: 5
          description: Composite score across sleep, mood, energy, anxiety, activity, social
          example: 3.6
        components:
          type: object
          description: Individual dimension scores (1–5 each)
          properties:
            sleep:
              type: integer
              example: 4
            mood:
              type: integer
              example: 3
            energy:
              type: integer
              example: 3
            anxiety:
              type: integer
              example: 2
            activity:
              type: integer
              example: 4
            social:
              type: integer
              example: 4

    TrendSummary:
      type: object
      properties:
        period_days:
          type: integer
          example: 30
        entry_count:
          type: integer
          example: 22
        avg_pain:
          type: number
          example: 5.8
        trend:
          type: string
          enum: [improving, stable, worsening]
          example: stable
        trend_diff:
          type: number
          description: Pain delta between first and second half of period (negative = improving)
          example: -0.4
        worst_day_of_week:
          type: string
          nullable: true
          example: Monday
        best_day_of_week:
          type: string
          nullable: true
          example: Wednesday
        wellbeing_pain_correlation:
          type: number
          nullable: true
          description: Pain difference on high vs low wellbeing days (negative = high wellbeing correlates with less pain)
          example: -1.8
        hrv_pain_correlation:
          type: number
          nullable: true
          description: Pain difference on high vs low HRV days
          example: -2.1
        sleep_pain_correlation:
          type: number
          nullable: true
          description: Pain difference on long vs short sleep days
          example: -1.3
        top_pain_areas:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              count:
                type: integer
          example:
            - id: left_knee
              count: 18
            - id: lower_back
              count: 11
        top_pain_qualities:
          type: array
          items:
            type: object
            properties:
              name:
                type: string
              count:
                type: integer
          example:
            - name: burning
              count: 14
            - name: throbbing
              count: 9

    IncidentReport:
      type: object
      required: [id, incident_date, type, documented_at]
      description: |
        A patient-documented incident report. Contains sensitive personal data.
        Accessible only with the `incidents:read` scope and explicit patient consent.

        **Privacy note:** Photo paths are never returned. `has_photos` indicates
        whether encrypted photos were attached. Raw descriptions and GPS
        coordinates are included only when the patient has explicitly consented.
      properties:
        id:
          type: string
          description: Unique incident ID (timestamp-based)
          example: "1746349200000"
        incident_date:
          type: string
          format: date
          description: Date the incident occurred (patient-selected)
          example: "2026-05-01"
        type:
          type: string
          description: Incident category as entered by the patient
          example: Physical
        location:
          type: string
          nullable: true
          description: Location text as entered by the patient
          example: "Workplace, 3rd floor corridor"
        location_gps:
          type: object
          nullable: true
          description: GPS coordinates captured at time of documentation (if patient granted location access)
          properties:
            lat:
              type: number
              example: 60.1699
            lng:
              type: number
              example: 24.9384
            accuracy:
              type: number
              description: Accuracy radius in metres
              example: 12.5
        witnesses:
          type: string
          nullable: true
          description: Witness information as entered by the patient
          example: "John Doe, colleague"
        description:
          type: string
          nullable: true
          description: Incident description as entered by the patient
          example: "Slipped on wet floor, fell and hit left shoulder."
        severity:
          type: string
          nullable: true
          enum: [mild, moderate, severe, null]
          description: Patient-assessed severity
          example: moderate
        pain_level:
          type: integer
          nullable: true
          minimum: 0
          maximum: 10
          description: Composite pain level from body map at time of documentation
          example: 7
        pain_areas:
          type: array
          nullable: true
          description: Body areas affected, drawn from the body map
          items:
            $ref: '#/components/schemas/BodyArea'
        pain_qualities:
          type: array
          nullable: true
          items:
            type: string
          example: ["sharp", "burning"]
        has_photos:
          type: boolean
          description: Whether the patient attached encrypted photos to this report
          example: true
        documented_at:
          type: string
          format: date-time
          description: Timestamp when the report was created in the app
          example: "2026-05-01T14:32:00Z"

    LinkRequest:
      type: object
      required: [id, patient_email, status, scopes, requested_at, expires_at]
      properties:
        id:
          type: string
          description: Unique request ID
          example: "lr_a1b2c3d4e5f6"
        patient_email:
          type: string
          format: email
          description: Email address the request was sent to
          example: "potilas@example.com"
        patient_id:
          type: string
          nullable: true
          description: |
            Anonymized patient identifier — populated once the patient accepts.
            Use this in all `/patients/{patient_id}/...` data endpoints.
          example: "p2c_anon_a3f9b2"
        status:
          type: string
          enum: [pending, accepted, rejected, expired, cancelled]
          example: pending
        scopes:
          type: array
          description: Data scopes requested from the patient
          items:
            type: string
          example: ["pain:read", "wellbeing:read"]
        message:
          type: string
          nullable: true
          description: Optional message shown to the patient in the consent dialog
          example: "Tampereen kipuklinikka haluaa seurata hoitosi edistymistä."
        requested_at:
          type: string
          format: date-time
          example: "2026-05-06T10:00:00Z"
        expires_at:
          type: string
          format: date-time
          description: Request expires 7 days after creation if not responded to
          example: "2026-05-13T10:00:00Z"
        responded_at:
          type: string
          format: date-time
          nullable: true
          description: When the patient accepted or rejected the request
          example: "2026-05-07T08:30:00Z"

    WebhookRegistration:
      type: object
      required: [url, events]
      properties:
        url:
          type: string
          format: uri
          example: https://ehr.yourclinic.fi/webhooks/pain2care
        events:
          type: array
          items:
            type: string
            enum:
              - pain_entry.created
              - wellbeing.logged
              - vitals.synced
              - incident.created
              - consent.revoked
          example: [pain_entry.created, consent.revoked]
        secret:
          type: string
          description: |
            Optional HMAC-SHA256 signing secret. If provided, Pain2Care signs
            every webhook payload and includes the signature in
            `X-P2C-Signature` header so you can verify authenticity.
          example: "whsec_xxxxxxxxxxxxxxxx"

    WebhookEvent:
      type: object
      properties:
        event:
          type: string
          example: pain_entry.created
        patient_id:
          type: string
          description: Anonymized patient identifier (stable per consent grant)
          example: "p2c_anon_a3f9b2"
        timestamp:
          type: string
          format: date-time
          example: "2026-05-04T09:00:12Z"
        data:
          type: object
          description: Event-specific payload — same schema as the corresponding GET endpoint

# ── Security (applied globally) ──────────────────────────────────────────────

security:
  - ApiKey: []
    PatientOAuth: []

# ── Paths ────────────────────────────────────────────────────────────────────

paths:

  # ── Patient linking / consent ────────────────────────────────────────────

  /link-requests:
    post:
      tags: [Consent]
      summary: Send a data access request to a patient
      description: |
        Sends a consent request directly to the patient's Pain2Care app.
        The patient sees a pending request in their profile and can accept
        or reject it at any time within 7 days.

        Once accepted you receive a `consent.granted` webhook containing
        the `patient_id` to use in all data endpoints.

        **One active request per patient per partner** — returns 409 if a
        pending request already exists for this email.
      security:
        - ApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [patient_email, scopes]
              properties:
                patient_email:
                  type: string
                  format: email
                  description: The patient's Pain2Care account email
                  example: "potilas@example.com"
                scopes:
                  type: array
                  description: Data scopes you are requesting access to
                  items:
                    type: string
                    enum: [pain:read, wellbeing:read, vitals:read, trends:read, reports:read, incidents:read]
                  example: ["pain:read", "wellbeing:read"]
                message:
                  type: string
                  description: |
                    Optional plain-text message shown to the patient in the
                    consent dialog (max 200 characters). Use the patient's
                    language where possible.
                  maxLength: 200
                  example: "Tampereen kipuklinikka pyytää lupaa seurata kipukirjauksiasi hoitosi tueksi."
      responses:
        "201":
          description: Request sent — patient will be notified in-app
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LinkRequest'
        "401":
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        "404":
          description: No Pain2Care account found for this email address
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        "409":
          description: A pending link request already exists for this patient
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

    get:
      tags: [Consent]
      summary: List your sent link requests
      description: Returns all link requests sent by your organisation, optionally filtered by status.
      security:
        - ApiKey: []
      parameters:
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum: [pending, accepted, rejected, expired, cancelled]
          description: Filter by request status
      responses:
        "200":
          description: Link requests
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/LinkRequest'

  /link-requests/{request_id}:
    delete:
      tags: [Consent]
      summary: Cancel a pending link request
      description: |
        Cancels a request that has not yet been responded to by the patient.
        Returns 409 if the request has already been accepted or rejected.
      security:
        - ApiKey: []
      parameters:
        - name: request_id
          in: path
          required: true
          schema:
            type: string
            example: "lr_a1b2c3d4e5f6"
      responses:
        "204":
          description: Request cancelled
        "404":
          description: Request not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        "409":
          description: Cannot cancel — patient has already responded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ── OAuth ────────────────────────────────────────────────────────────────

  /oauth/token:
    post:
      tags: [Auth]
      summary: Exchange authorization code or refresh token
      security: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [grant_type, client_id]
              properties:
                grant_type:
                  type: string
                  enum: [authorization_code, refresh_token]
                client_id:
                  type: string
                code:
                  type: string
                  description: Required for authorization_code grant
                code_verifier:
                  type: string
                  description: PKCE verifier — required for authorization_code grant
                redirect_uri:
                  type: string
                  format: uri
                refresh_token:
                  type: string
                  description: Required for refresh_token grant
      responses:
        "200":
          description: Token response
          content:
            application/json:
              schema:
                type: object
                properties:
                  access_token:
                    type: string
                  token_type:
                    type: string
                    example: Bearer
                  expires_in:
                    type: integer
                    example: 3600
                  refresh_token:
                    type: string
                  scope:
                    type: string
                    example: "pain:read wellbeing:read"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /oauth/revoke:
    post:
      tags: [Auth]
      summary: Revoke a token (patient or partner-initiated)
      security: [{ ApiKey: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token]
              properties:
                token:
                  type: string
      responses:
        "204":
          description: Token revoked

  # ── Pain entries ─────────────────────────────────────────────────────────

  /patients/{patient_id}/pain-entries:
    get:
      tags: [Pain]
      summary: List pain check-in entries
      security:
        - ApiKey: []
          PatientOAuth: [pain:read]
      parameters:
        - name: patient_id
          in: path
          required: true
          description: |
            The clinic token value assigned to this patient (e.g. `ABC12345`).
            Issued via the Clinic Admin panel. Becomes active once the patient
            has logged in at least once with the token.
          schema:
            type: string
            example: ABC12345
        - name: from
          in: query
          schema:
            type: string
            format: date
          example: "2026-04-01"
        - name: to
          in: query
          schema:
            type: string
            format: date
          example: "2026-05-04"
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: per_page
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
        - name: Accept
          in: header
          schema:
            type: string
            enum:
              - application/json
          description: Response format — always application/json
      responses:
        "200":
          description: Pain entries
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/PainEntry'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        "403":
          description: Missing scope or consent revoked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /patients/{patient_id}/pain-entries/{entry_id}:
    get:
      tags: [Pain]
      summary: Get a single pain entry
      security:
        - ApiKey: []
          PatientOAuth: [pain:read]
      parameters:
        - name: patient_id
          in: path
          required: true
          description: |
            The clinic token value assigned to this patient (e.g. `ABC12345`).
            Issued via the Clinic Admin panel. Becomes active once the patient
            has logged in at least once with the token.
          schema:
            type: string
            example: ABC12345
        - name: entry_id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Pain entry
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PainEntry'
        "404":
          description: Entry not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ── Wellbeing ─────────────────────────────────────────────────────────────

  /patients/{patient_id}/wellbeing:
    get:
      tags: [Wellbeing]
      summary: List daily wellbeing scores
      security:
        - ApiKey: []
          PatientOAuth: [wellbeing:read]
      parameters:
        - name: patient_id
          in: path
          required: true
          description: |
            The clinic token value assigned to this patient (e.g. `ABC12345`).
            Issued via the Clinic Admin panel. Becomes active once the patient
            has logged in at least once with the token.
          schema:
            type: string
            example: ABC12345
        - name: from
          in: query
          schema:
            type: string
            format: date
        - name: to
          in: query
          schema:
            type: string
            format: date
      responses:
        "200":
          description: Wellbeing entries
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/WellbeingEntry'

  # ── Vitals ────────────────────────────────────────────────────────────────

  /patients/{patient_id}/vitals:
    get:
      tags: [Vitals]
      summary: List vitals data from connected devices
      description: |
        Returns vitals synced from any connected device (Polar, Withings, Oura,
        Google Health Connect, or manual entry). Only metrics that were actually
        measured are populated — the rest are null.
      security:
        - ApiKey: []
          PatientOAuth: [vitals:read]
      parameters:
        - name: patient_id
          in: path
          required: true
          description: |
            The clinic token value assigned to this patient (e.g. `ABC12345`).
            Issued via the Clinic Admin panel. Becomes active once the patient
            has logged in at least once with the token.
          schema:
            type: string
            example: ABC12345
        - name: from
          in: query
          schema:
            type: string
            format: date
        - name: to
          in: query
          schema:
            type: string
            format: date
        - name: source
          in: query
          schema:
            type: string
            enum: [polar, withings, oura, health_connect, manual]
          description: Filter by device source
      responses:
        "200":
          description: Vitals records
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/VitalsData'

  # ── Trends ────────────────────────────────────────────────────────────────

  /patients/{patient_id}/trends:
    get:
      tags: [Trends]
      summary: Get computed trend summary
      description: |
        Pre-computed statistical summary for the requested period.
        Includes pain trend direction, wellbeing correlation, and
        device vitals correlations where data is available.
      security:
        - ApiKey: []
          PatientOAuth: [trends:read]
      parameters:
        - name: patient_id
          in: path
          required: true
          description: |
            The clinic token value assigned to this patient (e.g. `ABC12345`).
            Issued via the Clinic Admin panel. Becomes active once the patient
            has logged in at least once with the token.
          schema:
            type: string
            example: ABC12345
        - name: days
          in: query
          schema:
            type: integer
            enum: [7, 30, 90]
            default: 30
          description: Lookback period in days
      responses:
        "200":
          description: Trend summary
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TrendSummary'
        "422":
          description: Insufficient data (fewer than 5 entries in period)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ── Reports ───────────────────────────────────────────────────────────────

  /patients/{patient_id}/reports/pdf:
    get:
      tags: [Reports]
      summary: Download a PDF pain history report
      description: |
        Generates the same PDF report that patients can download in-app.
        Includes pain charts, wellbeing timeline, body map summary,
        and AI-generated pattern analysis (if available).
      security:
        - ApiKey: []
          PatientOAuth: [reports:read]
      parameters:
        - name: patient_id
          in: path
          required: true
          description: |
            The clinic token value assigned to this patient (e.g. `ABC12345`).
            Issued via the Clinic Admin panel. Becomes active once the patient
            has logged in at least once with the token.
          schema:
            type: string
            example: ABC12345
        - name: from
          in: query
          schema:
            type: string
            format: date
        - name: to
          in: query
          schema:
            type: string
            format: date
        - name: lang
          in: query
          schema:
            type: string
            enum: [en, fi, sv, de, es, fr, pt]
            default: en
      responses:
        "200":
          description: PDF file
          content:
            application/pdf:
              schema:
                type: string
                format: binary
          headers:
            Content-Disposition:
              schema:
                type: string
                example: attachment; filename="pain2care-report-2026-05-04.pdf"

  # ── Incident reports ─────────────────────────────────────────────────────

  /patients/{patient_id}/incidents:
    get:
      tags: [Incidents]
      summary: List incident reports
      description: |
        Returns incident reports documented by the patient.

        **Scope required:** `incidents:read`

        Incident reports contain highly sensitive personal data. This endpoint
        is intended for:
        - Occupational health / workplace safety teams (injury documentation)
        - Insurance claims assessment (objective timestamped records)
        - Social services case management (patient-consented sharing)
        - Law enforcement (requires legal order AND patient consent)

        GPS coordinates and full descriptions are included only when present.
        Photo files are never returned — use `has_photos` to know if evidence
        photos exist; request them separately under a court order or DPA clause.
      security:
        - ApiKey: []
          PatientOAuth: [incidents:read]
      parameters:
        - name: patient_id
          in: path
          required: true
          description: |
            The clinic token value assigned to this patient (e.g. `ABC12345`).
            Issued via the Clinic Admin panel.
          schema:
            type: string
            example: ABC12345
        - name: from
          in: query
          schema:
            type: string
            format: date
          description: Filter — incidents on or after this date (incident_date)
          example: "2026-01-01"
        - name: to
          in: query
          schema:
            type: string
            format: date
          description: Filter — incidents on or before this date (incident_date)
          example: "2026-05-04"
        - name: type
          in: query
          schema:
            type: string
          description: Filter by incident type (case-insensitive)
          example: Physical
      responses:
        "200":
          description: Incident reports
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/IncidentReport'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        "403":
          description: Missing incidents:read scope or consent not granted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /patients/{patient_id}/incidents/{incident_id}:
    get:
      tags: [Incidents]
      summary: Get a single incident report
      security:
        - ApiKey: []
          PatientOAuth: [incidents:read]
      parameters:
        - name: patient_id
          in: path
          required: true
          schema:
            type: string
            example: ABC12345
        - name: incident_id
          in: path
          required: true
          description: Incident ID (timestamp-based, from the list endpoint)
          schema:
            type: string
            example: "1746349200000"
      responses:
        "200":
          description: Incident report
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IncidentReport'
        "404":
          description: Incident not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ── Webhooks ─────────────────────────────────────────────────────────────

  /webhooks:
    post:
      tags: [Webhooks]
      summary: Register a webhook endpoint
      description: |
        Register a URL to receive real-time push notifications when patients
        log new data. Pain2Care sends an HTTP POST to your URL within seconds
        of the event occurring.

        **Signature verification**
        If you provide a `secret`, each webhook delivery includes an
        `X-P2C-Signature: sha256=<hmac>` header. Verify it server-side to
        confirm the payload is genuine.
      security:
        - ApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookRegistration'
      responses:
        "201":
          description: Webhook registered
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    example: "wh_a1b2c3"
                  status:
                    type: string
                    example: active
    get:
      tags: [Webhooks]
      summary: List registered webhooks
      security:
        - ApiKey: []
      responses:
        "200":
          description: Webhooks
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/WebhookRegistration'

  /webhooks/{webhook_id}:
    delete:
      tags: [Webhooks]
      summary: Delete a webhook
      security:
        - ApiKey: []
      parameters:
        - name: webhook_id
          in: path
          required: true
          schema:
            type: string
      responses:
        "204":
          description: Deleted

# ── Webhook payload examples (x-webhooks extension) ─────────────────────────

x-webhooks:
  pain_entry.created:
    post:
      summary: New pain check-in logged
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEvent'
            example:
              event: pain_entry.created
              patient_id: p2c_anon_a3f9b2
              timestamp: "2026-05-04T09:00:12Z"
              data:
                id: "1746349200000"
                pain_level: 6
                areas:
                  - id: left_knee
                    label: Left knee
                    max_intensity: 6
                logged_at: "2026-05-04T09:00:00Z"

  incident.created:
    post:
      summary: New incident report documented
      description: |
        Fired when a patient saves a new incident report.
        Only delivered to webhooks registered by partners who hold the
        `incidents:read` scope for this patient.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEvent'
            example:
              event: incident.created
              patient_id: p2c_anon_a3f9b2
              timestamp: "2026-05-01T14:32:05Z"
              data:
                id: "1746349200000"
                incident_date: "2026-05-01"
                type: Physical
                pain_level: 7
                has_photos: true
                documented_at: "2026-05-01T14:32:00Z"

  consent.granted:
    post:
      summary: Patient accepted a link request
      description: |
        Fired when a patient accepts your data access request in the Pain2Care app.
        The `patient_id` in the payload is the anonymized identifier to use in
        all subsequent `/patients/{patient_id}/...` data requests.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEvent'
            example:
              event: consent.granted
              patient_id: p2c_anon_a3f9b2
              timestamp: "2026-05-07T08:30:00Z"
              data:
                request_id: "lr_a1b2c3d4e5f6"
                scopes: ["pain:read", "wellbeing:read"]

  consent.revoked:
    post:
      summary: Patient revoked their consent grant
      description: |
        Received when a patient removes your integration from their Pain2Care
        account. You must delete all data you have stored for this patient
        within 30 days (GDPR compliance).
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEvent'
            example:
              event: consent.revoked
              patient_id: p2c_anon_a3f9b2
              timestamp: "2026-05-04T14:22:00Z"
              data:
                revoked_scopes: [pain:read, wellbeing:read]

tags:
  - name: Consent
    description: |
      Patient linking and consent management.
      Send link requests to patients, track status, and receive webhooks
      when patients accept or revoke access.
  - name: Auth
    description: OAuth 2.0 token management
  - name: Pain
    description: Pain check-in entries
  - name: Wellbeing
    description: Daily wellness scores
  - name: Vitals
    description: Device vitals (Polar, Withings, Oura, Health Connect)
  - name: Trends
    description: Computed statistics and correlations
  - name: Reports
    description: PDF report generation
  - name: Incidents
    description: |
      Incident reports (injury, violence, workplace accident, etc.).
      Requires `incidents:read` scope. Sensitive data — extra DPA clauses apply.
  - name: Webhooks
    description: Real-time push notifications
