openapi: 3.1.0
info:
  title: Healthspan Score API
  version: 1.0.0
  summary: PlanScout intake scoring and Genworth cost-of-care analysis
  description: Receives PlanScout intake events, produces versioned person and
    household Healthspan scores, and delivers results through an asynchronous
    callback. The API also exposes the existing Genworth 2024 care-analysis
    model as a standalone operation.
  contact:
    name: Black Flag Design
    url: https://blackflag.design
servers:
  - url: https://blessed-squirrel-650.convex.site
    description: Active QA deployment
tags:
  - name: Integration
    description: PlanScout intake and score status
  - name: Care analysis
    description: Genworth 2024 cost-of-care model
  - name: Operations
    description: Deployment readiness
paths:
  /oauth/token:
    post:
      tags:
        - Integration
      operationId: issuePlanScoutAccessToken
      summary: Exchange PlanScout machine credentials for a Clerk M2M token
      description: Implements the OAuth 2.0 client-credentials request shape requested
        by PlanScout. The opaque bearer token is issued and verified by Clerk
        M2M authentication and expires after one hour.
      security: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required:
                - grant_type
              properties:
                grant_type:
                  type: string
                  const: client_credentials
                scope:
                  type: string
                  const: healthspan.intakes.write
                client_id:
                  type: string
                  description: Optional when HTTP Basic authentication is used.
                client_secret:
                  type: string
                  format: password
                  description: Optional when HTTP Basic authentication is used.
      responses:
        "200":
          description: Clerk M2M access token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OAuthTokenResponse"
        "400":
          $ref: "#/components/responses/OAuthError"
        "401":
          $ref: "#/components/responses/OAuthError"
  /v1/intakes:
    post:
      tags:
        - Integration
      operationId: submitPlanScoutIntake
      summary: Accept a PlanScout intake for asynchronous scoring
      description: Production requests use a Clerk-backed OAuth 2.0 client-credentials
        bearer token. HMAC-SHA256 remains available as a compatibility fallback.
        Delivery is at least once; duplicate eventId values return the original
        status without creating a second score or callback.
      security:
        - planScoutOAuth:
            - healthspan.intakes.write
        - planScoutHmac: []
      parameters:
        - $ref: "#/components/parameters/PlanScoutTimestamp"
        - $ref: "#/components/parameters/PlanScoutSignature"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PlanScoutIntake"
            examples:
              complete:
                summary: Complete synthetic intake
                value:
                  eventType: intake.submitted
                  eventId: f1c2e0a4-9b6d-4f2a-8e21-2c7b9a0d4e55
                  eventTimestamp: 2026-07-10T16:30:00.000Z
                  schemaVersion: v1
                  advisorId: "10482"
                  clientGuid: b7e63d4a-1f88-4c0e-9a2f-3d51c6e9aa10
                  planscoutPlanRequestId: 665f0a1b2c3d4e5f60718293
                  advisor:
                    name: Jane Advisor
                    company: Summit Wealth Partners
                    email: jane@summitwealth.example
                  client:
                    hasPartner: true
                    primary:
                      firstName: Robert
                      lastName: Miyazaki
                      age: 64
                    partner:
                      firstName: Susan
                      lastName: Miyazaki
                      age: 61
                  intake:
                    employment:
                      primary:
                        isRetired: false
                        currentIncome: 145000
                        expectedRetirementAge: 67
                      partner:
                        isRetired: false
                        currentIncome: 60000
                        expectedRetirementAge: 65
                    savings:
                      totalAssets: 1170000
                    goals:
                      targetIncome: 164000
                      legacyGoal: 250000
                  healthSupport:
                    schemaVersion: v1
                    answers:
                      - key: diet_frequency
                        questionId: HQ1
                        type: single_choice_scale
                        subject: primary
                        selected:
                          value: often
                          label: Often
                          ordinal: 3
                      - key: community_connection
                        questionId: HQ2
                        type: single_choice_scale
                        subject: primary
                        selected:
                          value: somewhat_connected
                          label: Somewhat connected
                          ordinal: 2
                      - key: health_conditions
                        questionId: HQ3
                        type: multi_select
                        subject: primary
                        selected:
                          - value: hypertension
                            label: Hypertension
                        noneSelected: false
                      - key: care_reliance
                        questionId: HQ4
                        type: multi_select
                        subject: primary
                        selected:
                          - value: spouse_partner
                            label: Spouse / partner
                          - value: adult_child
                            label: Adult child
                      - key: care_dependents
                        questionId: HQ5
                        type: multi_select
                        subject: primary
                        selected:
                          - value: parent
                            label: Parent
                      - key: emergency_contact
                        questionId: HQ6
                        type: single_choice_with_name
                        subject: primary
                        selected:
                          value: adult_child
                          label: Adult child
                        contactName: Emily Miyazaki
                  healthspan:
                    state: Colorado
                    careAssessment:
                      conditionSummary: Hypertension managed with medication
                      cognitionStage: none
                      adlGrid:
                        bathing: independent
                        dressing: independent
                        toileting: independent
                        transferring: independent
                        continence: independent
                        feeding: independent
                      iadlGrid:
                        medication_management: cueing
                        meal_prep: independent
                        housekeeping: assist
                        shopping: independent
                        transportation: independent
                        finances: independent
                        communication: independent
                      riskFlags: []
                      dmeInUse: []
                      caregiverHoursPerWeek: 20
                      carePreferences: Remain at home as long as practical
                      state: Colorado
      responses:
        "200":
          description: Duplicate event already accepted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AcceptedResponse"
        "202":
          description: Accepted for scoring
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AcceptedResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
  /v1/intakes/{eventId}:
    get:
      tags:
        - Integration
      operationId: getIntakeStatus
      summary: Read score and callback status by PlanScout event ID
      security:
        - planScoutOAuth:
            - healthspan.intakes.write
        - planScoutHmac: []
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Current processing state with generated callback payload
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntakeStatus"
        "404":
          description: Event not found
  /v1/care-cost/estimate:
    post:
      tags:
        - Care analysis
      operationId: estimateCareCost
      summary: Run the Genworth 2024 care-needs and cost model
      description: Derives weekly care needs, matches appropriate settings to state
        median costs, and generates multi-stage care scenarios.
      security:
        - planScoutOAuth:
            - healthspan.intakes.write
        - planScoutHmac: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CareAssessment"
      responses:
        "200":
          description: Care analysis result
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
  /health:
    get:
      tags:
        - Operations
      operationId: getHealth
      summary: Check API readiness
      responses:
        "200":
          description: Healthy deployment
          content:
            application/json:
              schema:
                type: object
components:
  securitySchemes:
    planScoutOAuth:
      type: oauth2
      description: PlanScout machine authentication. Exchange client credentials at
        the token endpoint, cache the returned Clerk M2M bearer token until
        expiry, and send it in the Authorization header.
      flows:
        clientCredentials:
          tokenUrl: https://blessed-squirrel-650.convex.site/oauth/token
          scopes:
            healthspan.intakes.write: Submit intakes and read scoring status.
    planScoutHmac:
      type: apiKey
      in: header
      name: X-PlanScout-Signature
      description: sha256=<hex HMAC-SHA256(secret, timestamp + '.' + rawBody)>. Pair
        with X-PlanScout-Timestamp; requests more than five minutes old are
        rejected.
  parameters:
    PlanScoutTimestamp:
      name: X-PlanScout-Timestamp
      in: header
      required: false
      schema:
        type: integer
        format: int64
      description: Current Unix time in seconds.
    PlanScoutSignature:
      name: X-PlanScout-Signature
      in: header
      required: false
      schema:
        type: string
        pattern: ^sha256=[0-9a-f]{64}$
  responses:
    BadRequest:
      description: Malformed JSON or request headers
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: Missing, expired, or invalid bearer token or HMAC signature
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    OAuthError:
      description: OAuth client authentication or token request failed
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/OAuthError"
    UnprocessableEntity:
      description: Payload does not satisfy schema v1
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
  schemas:
    OAuthTokenResponse:
      type: object
      required:
        - access_token
        - token_type
        - expires_in
        - scope
      properties:
        access_token:
          type: string
          description: Opaque Clerk M2M token.
        token_type:
          type: string
          const: Bearer
        expires_in:
          type: integer
          minimum: 1
          maximum: 3600
        scope:
          type: string
          const: healthspan.intakes.write
    OAuthError:
      type: object
      required:
        - error
        - error_description
      properties:
        error:
          type: string
        error_description:
          type: string
    PlanScoutIntake:
      type: object
      required:
        - eventType
        - eventId
        - eventTimestamp
        - schemaVersion
        - advisorId
        - clientGuid
        - planscoutPlanRequestId
        - advisor
        - client
        - intake
        - healthSupport
      properties:
        eventType:
          const: intake.submitted
        eventId:
          type: string
          format: uuid
        eventTimestamp:
          type: string
          format: date-time
        schemaVersion:
          const: v1
        advisorId:
          type: string
        clientGuid:
          type: string
          format: uuid
        planscoutPlanRequestId:
          type: string
        advisor:
          type: object
          additionalProperties: true
        client:
          type: object
          additionalProperties: true
        intake:
          type: object
          additionalProperties: true
        estimatesApplied:
          type: object
          additionalProperties:
            type: boolean
        estimatedValues:
          type: object
          additionalProperties: true
        healthSupport:
          type: object
          required:
            - schemaVersion
            - answers
          properties:
            schemaVersion:
              const: v1
            answers:
              type: array
              minItems: 1
              items:
                $ref: "#/components/schemas/HealthAnswer"
        healthspan:
          type: object
          description: Optional extension for state-specific Genworth care analysis.
          properties:
            state:
              type: string
            careAssessment:
              $ref: "#/components/schemas/CareAssessment"
      additionalProperties: true
    HealthAnswer:
      type: object
      required:
        - key
        - questionId
        - subject
      properties:
        key:
          type: string
        questionId:
          type: string
          examples:
            - HQ1
        label:
          type: string
        type:
          type: string
        subject:
          type: string
          enum:
            - primary
            - partner
        selected: {}
        noneSelected:
          type: boolean
        otherText:
          type:
            - string
            - "null"
        contactName:
          type: string
      additionalProperties: true
    CareAssessment:
      type: object
      required:
        - conditionSummary
        - cognitionStage
        - adlGrid
        - iadlGrid
        - riskFlags
        - dmeInUse
        - caregiverHoursPerWeek
        - carePreferences
        - state
      properties:
        conditionSummary:
          type: string
        cognitionStage:
          type: string
          enum:
            - none
            - mci
            - mild_dementia
            - moderate_dementia
            - severe_dementia
        adlGrid:
          type: object
          additionalProperties:
            $ref: "#/components/schemas/AbilityLevel"
        iadlGrid:
          type: object
          additionalProperties:
            $ref: "#/components/schemas/AbilityLevel"
        riskFlags:
          type: array
          items:
            type: string
        dmeInUse:
          type: array
          items:
            type: string
        caregiverHoursPerWeek:
          type: number
          minimum: 0
          maximum: 168
        carePreferences:
          type: string
        state:
          type: string
    AbilityLevel:
      type: string
      enum:
        - independent
        - cueing
        - assist
        - dependent
    AcceptedResponse:
      type: object
      required:
        - accepted
        - eventId
        - duplicate
        - status
        - statusUrl
      properties:
        accepted:
          const: true
        eventId:
          type: string
          format: uuid
        duplicate:
          type: boolean
        status:
          type: string
          enum:
            - accepted
            - completed
        statusUrl:
          type: string
          format: uri
    IntakeStatus:
      type: object
      properties:
        eventId:
          type: string
          format: uuid
        scoreStatus:
          type: string
        callbackStatus:
          type: string
        score:
          type: object
          additionalProperties: true
        callback:
          type: object
          additionalProperties: true
    Error:
      type: object
      required:
        - error
        - message
      properties:
        error:
          type: string
        message:
          type: string
        details:
          type: array
          items:
            type: object
