openapi: 3.1.0
info:
  title: Health Bank One Partner API
  version: "2026-09-16"
  description: >
    Partner-facing APIs for Health Bank One: fetch full event data, patient
    demographics and insurance, and download health records. This spec covers
    the request/response endpoints you call. Event notifications we send to your
    endpoint are described under `webhooks`.


    Authoritative prose, including authentication details, mTLS/source-IP
    requirements, the notification flow, and error semantics, lives in the
    README alongside this file.
  contact:
    name: Health Bank One Integration Team

servers:
  - url: https://partner.healthbankone.com
    description: Production and sandbox share this host; the client ID (prod-* vs sandbox-*) selects the environment.

security:
  - clientAccessToken: []

tags:
  - name: Events
    description: Fetch full event data after receiving a notification.
  - name: Patient
    description: Patient demographics and insurance.
  - name: Records
    description: List and download patient health records.
  - name: Discovery
    description: Public key discovery.

paths:
  /events/{id}:
    get:
      tags: [Events]
      summary: Get full event data
      description: >
        Fetch the full payload of an event by its ID, after receiving a
        notification. Requires a Client Access Token whose `htu`/`htm` match
        this exact URL and method.
      operationId: getEvent
      parameters:
        - $ref: '#/components/parameters/EventId'
      responses:
        '200':
          description: The full event payload.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Event'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'

  /patients/{id}:
    get:
      tags: [Patient]
      summary: Get patient demographics and insurance
      operationId: getPatient
      parameters:
        - $ref: '#/components/parameters/PatientId'
      responses:
        '200':
          description: Patient demographics and (optionally) insurance.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Patient'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'

  /patients/{id}/records:
    get:
      tags: [Records]
      summary: List all records for a patient
      description: >
        Returns all of the patient's records as a flat array in a single
        response. There is no pagination and there are no query parameters.
        A patient with no records returns 200 with an empty array.
      operationId: listPatientRecords
      parameters:
        - $ref: '#/components/parameters/PatientId'
      responses:
        '200':
          description: All records for the patient (may be empty).
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/RecordSummary'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'

  /patients/{id}/records/{recordId}:
    get:
      tags: [Records]
      summary: Get a specific record with a download URL
      description: >
        Returns record metadata plus a pre-signed download URL. The URL already
        includes authorization and is not subject to mTLS or source-IP
        restrictions; it expires after 5 minutes.
      operationId: getPatientRecord
      parameters:
        - $ref: '#/components/parameters/PatientId'
        - $ref: '#/components/parameters/RecordId'
      responses:
        '200':
          description: Record metadata with a pre-signed download URL.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Record'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'

  /.well-known/jwks.json:
    get:
      tags: [Discovery]
      summary: Health Bank One notification signing keys (JWKS)
      description: >
        Public JWKS used to verify notification JWTs we send you. This endpoint
        is public: no Client Access Token, mTLS, or source-IP restriction.
      operationId: getJwks
      security: []
      responses:
        '200':
          description: JSON Web Key Set.
          content:
            application/json:
              schema:
                type: object
                properties:
                  keys:
                    type: array
                    items:
                      type: object
                      additionalProperties: true

webhooks:
  eventNotification:
    post:
      tags: [Events]
      operationId: receiveEventNotification
      summary: Event notification (we call your endpoint)
      description: >
        We POST a signed JWT to the endpoint you registered whenever an event
        occurs. The JWT carries only the event type and event ID; verify it
        against our JWKS, then fetch the full event with GET /events/{id}.


        The order is: verify the JWT, write the notification to durable storage
        (a queue), then return 200, then fetch and process asynchronously off
        that store. Return 200 only after the durable write, since the 200 stops
        our retries. Any non-200, or a response after 5 seconds, is treated as a
        delivery failure and retried (up to 5 attempts: 30s, 1m, 2m, 4m, 8m).
        Notifications may be redelivered, so de-duplicate by event ID.
      requestBody:
        required: true
        content:
          application/jwt:
            schema:
              type: string
              description: >
                The raw request body is a compact JWS (RS256), not JSON; the
                Content-Type is application/jwt. The header carries `kid`. The
                verified payload is `{ type: "event_notification", event: {
                type, id }, iss: "partner.healthbankone.com", aud: <your client
                id>, iat, nbf, exp, jti }`, where `exp` is 5 minutes after
                `iat`.
      responses:
        '200':
          description: Receipt acknowledged. Return within 5 seconds.

components:
  securitySchemes:
    clientAccessToken:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >
        Client Access Token: an RS256-signed JWT you mint per request. Claims:
        `iss` = your client ID, `aud` = partner.healthbankone.com,
        `htu`/`htm` = base64url SHA-256 of the request URL and uppercased method,
        `iat`/`nbf`/`exp` within a 5-minute window, and a unique `jti`. The JWT
        header carries `kid`. Production also requires mTLS with public-key
        pinning and a source-IP allowlist (enforced at the transport layer, not
        expressible in this spec).

  parameters:
    EventId:
      name: id
      in: path
      required: true
      description: Event ID from the notification.
      schema:
        type: string
        format: uuid
    PatientId:
      name: id
      in: path
      required: true
      description: Patient ID (the `patient_id` from an event).
      schema:
        type: string
        format: uuid
    RecordId:
      name: recordId
      in: path
      required: true
      description: Record ID from the records list or a record.added event.
      schema:
        type: string
        format: uuid

  responses:
    Unauthenticated:
      description: Authentication failed.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/AuthError'
          example:
            status: UNAUTHENTICATED
            message: JWT expired
    Forbidden:
      description: Authenticated but not permitted for this resource.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            error:
              code: FORBIDDEN
              message: Access Denied
              requestId: b1f2c3d4-5678-90ab-cdef-1234567890ab
    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            error:
              code: NOT_FOUND
              message: Record not found
              requestId: b1f2c3d4-5678-90ab-cdef-1234567890ab
    InternalError:
      description: Unexpected server error.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'

  schemas:
    AuthError:
      type: object
      required: [status, message]
      properties:
        status:
          type: string
          example: UNAUTHENTICATED
        message:
          type: string
          description: Specific reason for the authentication failure.

    ErrorEnvelope:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, requestId]
          properties:
            code:
              type: string
              description: Machine-readable code, e.g. FORBIDDEN, NOT_FOUND.
            message:
              type: string
            requestId:
              type: string
            correlationId:
              type: string
              description: Present when the request crossed internal services.
            details:
              type: array
              items:
                type: object
                additionalProperties: true

    Address:
      type: object
      properties:
        street1:
          type: string
          maxLength: 200
        street2:
          type: string
          maxLength: 100
        city:
          type: string
          maxLength: 100
        state:
          type: string
          pattern: '^[A-Z]{2}$'
        zip:
          type: string
          pattern: '^\d{5}$'

    Insurance:
      type: object
      properties:
        health_insurance_provider:
          type: string
          maxLength: 200
        member_id_number:
          type: string
          maxLength: 100
        group_number:
          type: string
          maxLength: 100
        member_name:
          type: string
          maxLength: 200
        insurance_plans:
          type: array
          items:
            type: string

    PatientDemographics:
      type: object
      properties:
        id:
          type: string
          format: uuid
        first_name:
          type: string
          maxLength: 100
        last_name:
          type: string
          maxLength: 100
        gender:
          type: string
          enum: [Male, Female, Other]
        date_of_birth:
          type: string
          format: date
          pattern: '^\d{4}-\d{2}-\d{2}$'
        email:
          type: string
          format: email
          maxLength: 200
        mobile_phone:
          type: string
          pattern: '^\+?[1-9]\d{1,14}$'
          maxLength: 16
        address:
          $ref: '#/components/schemas/Address'

    Patient:
      allOf:
        - $ref: '#/components/schemas/PatientDemographics'
        - type: object
          properties:
            primary_insurance:
              $ref: '#/components/schemas/Insurance'
            secondary_insurance:
              $ref: '#/components/schemas/Insurance'

    RecordSummary:
      type: object
      required: [id, file_name, file_type]
      properties:
        id:
          type: string
          format: uuid
        file_name:
          type: string
          maxLength: 100
        file_type:
          type: string
          enum: [ccda, image, pdf, dicom, zip, text, bin]

    Record:
      allOf:
        - $ref: '#/components/schemas/RecordSummary'
        - type: object
          properties:
            download:
              type: object
              properties:
                url:
                  type: string
                  format: uri
                  maxLength: 2048
                  description: Pre-signed download URL, valid until expires_at.
                expires_at:
                  type: string
                  format: date-time

    Permissions:
      type: array
      items:
        type: string
        enum: [records:read, records:add]

    Event:
      description: >
        Full event payload. Discriminated by `type`. connection.connected and
        connection.updated share a shape; patient/demographics are present only
        when records:read is granted.
      oneOf:
        - $ref: '#/components/schemas/ConnectionEvent'
        - $ref: '#/components/schemas/ConnectionDisconnectedEvent'
        - $ref: '#/components/schemas/RecordAddedEvent'
      discriminator:
        propertyName: type
        mapping:
          connection.connected: '#/components/schemas/ConnectionEvent'
          connection.updated: '#/components/schemas/ConnectionEvent'
          connection.disconnected: '#/components/schemas/ConnectionDisconnectedEvent'
          record.added: '#/components/schemas/RecordAddedEvent'

    EventBase:
      type: object
      required: [type, timestamp, id, patient_id]
      properties:
        type:
          type: string
          maxLength: 255
        timestamp:
          type: string
          format: date-time
        id:
          type: string
          format: uuid
        patient_id:
          type: string
          format: uuid
        portal_link:
          type: string
          format: uri
          maxLength: 2048
        promo_code:
          type: string
          maxLength: 32
          description: Optional; present only if a promo code was used.

    ConnectionEvent:
      description: connection.connected or connection.updated.
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          properties:
            type:
              type: string
              enum: [connection.connected, connection.updated]
            patient:
              $ref: '#/components/schemas/PatientDemographics'
            permissions:
              $ref: '#/components/schemas/Permissions'

    ConnectionDisconnectedEvent:
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          properties:
            type:
              type: string
              enum: [connection.disconnected]

    RecordAddedEvent:
      allOf:
        - $ref: '#/components/schemas/EventBase'
        - type: object
          properties:
            type:
              type: string
              enum: [record.added]
            records:
              type: array
              items:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  portal_link:
                    type: string
                    format: uri
                    maxLength: 2048
