openapi: 3.1.0
info:
  title: Open Health Tourism API
  version: 1.0.0
  description: |
    Independent, non-governmental, source-attributed access to official
    health-tourism authorization records. A positive result reports presence
    in a cited official source snapshot. It is not a clinical-quality assessment.
  contact:
    name: Open Health Tourism
    url: https://openhealthtourism.com/contact
  license:
    name: API Terms
    url: https://openhealthtourism.com/legal/api-terms
servers:
  - url: https://api.openhealthtourism.com/v1
    description: Production
  - url: https://api.sandbox.openhealthtourism.com/v1
    description: Sandbox
security:
  - bearerAuth: []
tags:
  - name: Jurisdictions
  - name: Sources
  - name: Search
  - name: Verification
  - name: Establishments
  - name: Datasets
  - name: Changes
  - name: Corrections
  - name: Account
paths:
  /jurisdictions:
    get:
      tags: [Jurisdictions]
      summary: List supported jurisdictions
      operationId: listJurisdictions
      security: []
      responses:
        "200":
          description: Supported jurisdictions
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JurisdictionListResponse"
  /jurisdictions/{code}:
    get:
      tags: [Jurisdictions]
      summary: Get jurisdiction coverage
      operationId: getJurisdiction
      security: []
      parameters:
        - $ref: "#/components/parameters/JurisdictionCode"
      responses:
        "200":
          description: Jurisdiction
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JurisdictionResponse"
        "404":
          $ref: "#/components/responses/NotFound"
  /sources:
    get:
      tags: [Sources]
      summary: List official sources
      operationId: listSources
      parameters:
        - in: query
          name: country
          schema:
            type: string
            minLength: 2
            maxLength: 2
      responses:
        "200":
          description: Source list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SourceListResponse"
  /sources/{sourceKey}:
    get:
      tags: [Sources]
      summary: Get a source
      operationId: getSource
      parameters:
        - in: path
          name: sourceKey
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Source
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SourceResponse"
        "404":
          $ref: "#/components/responses/NotFound"
  /sources/{sourceKey}/snapshots:
    get:
      tags: [Sources]
      summary: List source snapshots
      operationId: listSourceSnapshots
      parameters:
        - in: path
          name: sourceKey
          required: true
          schema:
            type: string
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Snapshots
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SnapshotListResponse"
  /search:
    get:
      tags: [Search]
      summary: Search establishments and official names
      operationId: searchEstablishments
      parameters:
        - in: query
          name: q
          required: true
          schema:
            type: string
            minLength: 2
            maxLength: 200
        - in: query
          name: country
          required: true
          schema:
            type: string
            minLength: 2
            maxLength: 2
        - in: query
          name: region
          schema:
            type: string
        - in: query
          name: facility_type
          schema:
            type: string
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Search results
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SearchResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "429":
          $ref: "#/components/responses/RateLimited"
  /verify:
    post:
      tags: [Verification]
      summary: Verify a single provider or facilitator
      operationId: verifySingle
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VerifyRequest"
      responses:
        "200":
          description: Verification result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VerifyResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/SourceUnavailable"
  /verify/batch:
    post:
      tags: [Verification]
      summary: Verify multiple providers or facilitators
      description: Requires the verify_batch entitlement.
      operationId: verifyBatch
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [items]
              properties:
                items:
                  type: array
                  minItems: 1
                  maxItems: 1000
                  items:
                    $ref: "#/components/schemas/VerifyRequest"
      responses:
        "200":
          description: Batch verification result
          content:
            application/json:
              schema:
                type: object
                required: [api_version, request_id, data, usage]
                properties:
                  api_version:
                    type: string
                    const: v1
                  request_id:
                    type: string
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/VerifyResultEnvelope"
                  usage:
                    $ref: "#/components/schemas/Usage"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
  /establishments:
    get:
      tags: [Establishments]
      summary: List establishments
      operationId: listEstablishments
      parameters:
        - in: query
          name: country
          required: true
          schema:
            type: string
        - in: query
          name: region
          schema:
            type: string
        - in: query
          name: status
          schema:
            $ref: "#/components/schemas/VerificationStatus"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Establishments
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EstablishmentListResponse"
  /establishments/{establishmentId}:
    get:
      tags: [Establishments]
      summary: Get an establishment
      operationId: getEstablishment
      parameters:
        - $ref: "#/components/parameters/EstablishmentId"
      responses:
        "200":
          description: Establishment
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EstablishmentResponse"
        "404":
          $ref: "#/components/responses/NotFound"
  /establishments/{establishmentId}/history:
    get:
      tags: [Establishments]
      summary: Get establishment history
      operationId: getEstablishmentHistory
      parameters:
        - $ref: "#/components/parameters/EstablishmentId"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: History
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChangeListResponse"
  /datasets:
    get:
      tags: [Datasets]
      summary: List published datasets
      operationId: listDatasets
      parameters:
        - in: query
          name: country
          schema:
            type: string
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Datasets
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SnapshotListResponse"
  /datasets/current:
    get:
      tags: [Datasets]
      summary: Get current dataset metadata
      operationId: getCurrentDataset
      parameters:
        - in: query
          name: country
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Current dataset
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SnapshotResponse"
        "404":
          $ref: "#/components/responses/NotFound"
  /datasets/{snapshotId}:
    get:
      tags: [Datasets]
      summary: Get dataset metadata
      operationId: getDataset
      parameters:
        - in: path
          name: snapshotId
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Dataset
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SnapshotResponse"
        "404":
          $ref: "#/components/responses/NotFound"
  /datasets/{snapshotId}/download:
    get:
      tags: [Datasets]
      summary: Download a dataset
      description: Requires dataset_download entitlement and source reuse clearance.
      operationId: downloadDataset
      parameters:
        - in: path
          name: snapshotId
          required: true
          schema:
            type: string
        - in: query
          name: format
          schema:
            type: string
            enum: [json, ndjson, csv]
            default: json
      responses:
        "200":
          description: Dataset file
          content:
            application/json:
              schema:
                type: string
                format: binary
            application/x-ndjson:
              schema:
                type: string
                format: binary
            text/csv:
              schema:
                type: string
                format: binary
        "403":
          $ref: "#/components/responses/Forbidden"
  /changes:
    get:
      tags: [Changes]
      summary: List registry changes
      operationId: listChanges
      parameters:
        - in: query
          name: country
          schema:
            type: string
        - in: query
          name: since
          schema:
            type: string
            format: date-time
        - in: query
          name: event_type
          schema:
            type: string
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Changes
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChangeListResponse"
  /corrections:
    post:
      tags: [Corrections]
      summary: Submit a correction or identity-evidence request
      operationId: createCorrection
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CorrectionRequest"
      responses:
        "202":
          description: Accepted for review
          content:
            application/json:
              schema:
                type: object
                required: [public_reference, status]
                properties:
                  public_reference:
                    type: string
                  status:
                    type: string
                    const: RECEIVED
        "400":
          $ref: "#/components/responses/BadRequest"
  /account:
    get:
      tags: [Account]
      summary: Get API account
      operationId: getAccount
      responses:
        "200":
          description: Account
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AccountResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /account/usage:
    get:
      tags: [Account]
      summary: Get current API usage
      operationId: getUsage
      responses:
        "200":
          description: Usage
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UsageResponse"
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: OHT_API_KEY
  parameters:
    JurisdictionCode:
      in: path
      name: code
      required: true
      schema:
        type: string
        minLength: 2
        maxLength: 12
    EstablishmentId:
      in: path
      name: establishmentId
      required: true
      schema:
        type: string
    Limit:
      in: query
      name: limit
      schema:
        type: integer
        minimum: 1
        maximum: 200
        default: 50
    Cursor:
      in: query
      name: cursor
      schema:
        type: string
  responses:
    BadRequest:
      description: Invalid request
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/Problem"
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/Problem"
    Forbidden:
      description: The account does not have the required entitlement
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/Problem"
    NotFound:
      description: Resource not found
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/Problem"
    RateLimited:
      description: Rate or quota limit exceeded
      headers:
        Retry-After:
          schema:
            type: integer
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/Problem"
    SourceUnavailable:
      description: Official source is unavailable or the current source cannot be validated
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/Problem"
  schemas:
    VerificationStatus:
      type: string
      enum:
        - LISTED_CURRENT
        - POSSIBLE_MATCH
        - AMBIGUOUS_MATCH
        - NOT_FOUND_CURRENT
        - HISTORICALLY_LISTED
        - REMOVED_FROM_CURRENT_SOURCE
        - SOURCE_UNAVAILABLE
        - SOURCE_CHANGED_UNPARSED
        - COVERAGE_UNAVAILABLE
        - MANUAL_REVIEW
    MatchConfidence:
      type: string
      enum: [HIGH, MEDIUM, LOW, NONE]
    Pagination:
      type: object
      required: [has_more, limit]
      properties:
        next_cursor:
          type: [string, "null"]
        has_more:
          type: boolean
        limit:
          type: integer
    Jurisdiction:
      type: object
      required: [code, name, coverage_status]
      properties:
        code:
          type: string
        name:
          type: string
        coverage_status:
          type: string
          enum: [AVAILABLE, PARTIAL, PERMISSION_PENDING, UNAVAILABLE]
        supported_record_types:
          type: array
          items:
            type: string
        current_snapshot_id:
          type: [string, "null"]
        last_successful_check:
          type: [string, "null"]
          format: date-time
    JurisdictionListResponse:
      type: object
      required: [api_version, data]
      properties:
        api_version:
          type: string
          const: v1
        data:
          type: array
          items:
            $ref: "#/components/schemas/Jurisdiction"
    JurisdictionResponse:
      type: object
      required: [api_version, data]
      properties:
        api_version:
          type: string
          const: v1
        data:
          $ref: "#/components/schemas/Jurisdiction"
    Source:
      type: object
      required:
        - source_key
        - jurisdiction_code
        - authority_name
        - source_name
        - source_url
        - access_mode
        - reuse_status
      properties:
        source_key:
          type: string
        jurisdiction_code:
          type: string
        authority_name:
          type: string
        source_name:
          type: string
        source_url:
          type: string
          format: uri
        access_mode:
          type: string
          enum: [API, CSV, JSON, PDF, HTML, SEARCH_ONLY]
        reuse_status:
          type: string
          enum: [CLEARED, ATTRIBUTION_REQUIRED, PERMISSION_PENDING, RESTRICTED, UNKNOWN]
        last_successful_snapshot_id:
          type: [string, "null"]
        last_checked_at:
          type: [string, "null"]
          format: date-time
    SourceListResponse:
      type: object
      required: [api_version, data]
      properties:
        api_version:
          type: string
          const: v1
        data:
          type: array
          items:
            $ref: "#/components/schemas/Source"
    SourceResponse:
      type: object
      required: [api_version, data]
      properties:
        api_version:
          type: string
          const: v1
        data:
          $ref: "#/components/schemas/Source"
    Snapshot:
      type: object
      required:
        - snapshot_id
        - source_key
        - retrieved_at
        - sha256
        - record_count
        - validation_status
      properties:
        snapshot_id:
          type: string
        source_key:
          type: string
        source_date:
          type: [string, "null"]
          format: date
        retrieved_at:
          type: string
          format: date-time
        sha256:
          type: string
        record_count:
          type: integer
        parser_version:
          type: string
        validation_status:
          type: string
          enum: [PASSED, WARNING, FAILED, MANUAL_APPROVAL]
        superseded_by:
          type: [string, "null"]
    SnapshotListResponse:
      type: object
      required: [api_version, data, pagination]
      properties:
        api_version:
          type: string
          const: v1
        data:
          type: array
          items:
            $ref: "#/components/schemas/Snapshot"
        pagination:
          $ref: "#/components/schemas/Pagination"
    SnapshotResponse:
      type: object
      required: [api_version, data]
      properties:
        api_version:
          type: string
          const: v1
        data:
          $ref: "#/components/schemas/Snapshot"
    SearchCandidate:
      type: object
      required:
        - establishment_id
        - official_name
        - country
        - match_confidence
        - match_score
      properties:
        establishment_id:
          type: string
        official_name:
          type: string
        aliases:
          type: array
          items:
            type: string
        country:
          type: string
        region:
          type: [string, "null"]
        facility_type_raw:
          type: [string, "null"]
        verification_status:
          $ref: "#/components/schemas/VerificationStatus"
        match_confidence:
          $ref: "#/components/schemas/MatchConfidence"
        match_score:
          type: integer
          minimum: 0
          maximum: 100
    SearchResponse:
      type: object
      required: [api_version, request_id, data, pagination]
      properties:
        api_version:
          type: string
          const: v1
        request_id:
          type: string
        data:
          type: array
          items:
            $ref: "#/components/schemas/SearchCandidate"
        pagination:
          $ref: "#/components/schemas/Pagination"
    VerifyRequest:
      type: object
      required: [country, name]
      additionalProperties: false
      properties:
        country:
          type: string
          minLength: 2
          maxLength: 12
        name:
          type: string
          minLength: 2
          maxLength: 300
        region:
          type: [string, "null"]
        phone:
          type: [string, "null"]
        address:
          type: [string, "null"]
        facility_type:
          type: [string, "null"]
    VerificationEvidence:
      type: object
      required:
        - authority_name
        - source_key
        - source_snapshot_id
        - source_url
        - retrieved_at
      properties:
        authority_name:
          type: string
        source_key:
          type: string
        source_snapshot_id:
          type: string
        source_url:
          type: string
          format: uri
        source_sequence_number:
          type: [integer, "null"]
        source_page:
          type: [integer, "null"]
        source_date:
          type: [string, "null"]
          format: date
        retrieved_at:
          type: string
          format: date-time
        source_sha256:
          type: string
    VerifyResult:
      type: object
      required: [status, match_confidence, statement]
      properties:
        status:
          $ref: "#/components/schemas/VerificationStatus"
        match_confidence:
          $ref: "#/components/schemas/MatchConfidence"
        match_score:
          type: integer
          minimum: 0
          maximum: 100
        establishment_id:
          type: [string, "null"]
        official_name:
          type: [string, "null"]
        facility_type_raw:
          type: [string, "null"]
        region:
          type: [string, "null"]
        statement:
          type: string
    Usage:
      type: object
      required: [billed_units]
      properties:
        billed_units:
          type: integer
        monthly_units_remaining:
          type: [integer, "null"]
    VerifyResponse:
      type: object
      required:
        - api_version
        - request_id
        - query
        - result
        - limitations
        - usage
      properties:
        api_version:
          type: string
          const: v1
        request_id:
          type: string
        query:
          $ref: "#/components/schemas/VerifyRequest"
        result:
          $ref: "#/components/schemas/VerifyResult"
        evidence:
          anyOf:
            - $ref: "#/components/schemas/VerificationEvidence"
            - type: "null"
        limitations:
          type: array
          items:
            type: string
        usage:
          $ref: "#/components/schemas/Usage"
    VerifyResultEnvelope:
      type: object
      required: [query, result, limitations]
      properties:
        query:
          $ref: "#/components/schemas/VerifyRequest"
        result:
          $ref: "#/components/schemas/VerifyResult"
        evidence:
          anyOf:
            - $ref: "#/components/schemas/VerificationEvidence"
            - type: "null"
        limitations:
          type: array
          items:
            type: string
    Establishment:
      type: object
      required:
        - establishment_id
        - official_name
        - jurisdiction_code
        - current_status
      properties:
        establishment_id:
          type: string
        entity_id:
          type: [string, "null"]
        official_name:
          type: string
        aliases:
          type: array
          items:
            type: string
        jurisdiction_code:
          type: string
        region:
          type: [string, "null"]
        district:
          type: [string, "null"]
        address_raw:
          type: [string, "null"]
        phones:
          type: array
          items:
            type: string
        facility_type_raw:
          type: [string, "null"]
        current_status:
          $ref: "#/components/schemas/VerificationStatus"
        current_snapshot_id:
          type: [string, "null"]
        first_observed_at:
          type: [string, "null"]
          format: date-time
        last_observed_at:
          type: [string, "null"]
          format: date-time
    EstablishmentListResponse:
      type: object
      required: [api_version, data, pagination]
      properties:
        api_version:
          type: string
          const: v1
        data:
          type: array
          items:
            $ref: "#/components/schemas/Establishment"
        pagination:
          $ref: "#/components/schemas/Pagination"
    EstablishmentResponse:
      type: object
      required: [api_version, data]
      properties:
        api_version:
          type: string
          const: v1
        data:
          $ref: "#/components/schemas/Establishment"
    ChangeEvent:
      type: object
      required:
        - event_id
        - event_type
        - occurred_at
      properties:
        event_id:
          type: string
        event_type:
          type: string
        establishment_id:
          type: [string, "null"]
        source_key:
          type: [string, "null"]
        previous_snapshot_id:
          type: [string, "null"]
        current_snapshot_id:
          type: [string, "null"]
        occurred_at:
          type: string
          format: date-time
        public_summary:
          type: [string, "null"]
        changes:
          type: object
          additionalProperties: true
    ChangeListResponse:
      type: object
      required: [api_version, data, pagination]
      properties:
        api_version:
          type: string
          const: v1
        data:
          type: array
          items:
            $ref: "#/components/schemas/ChangeEvent"
        pagination:
          $ref: "#/components/schemas/Pagination"
    CorrectionRequest:
      type: object
      required: [category, description, contact_email]
      properties:
        establishment_id:
          type: [string, "null"]
        category:
          type: string
          enum:
            - WRONG_MATCH
            - NAME_CHANGE
            - ADDRESS_CHANGE
            - PHONE_CHANGE
            - ALIAS_EVIDENCE
            - SOURCE_ERROR
            - PRIVACY_REQUEST
            - OTHER
        description:
          type: string
          minLength: 20
          maxLength: 5000
        evidence_urls:
          type: array
          maxItems: 10
          items:
            type: string
            format: uri
        contact_name:
          type: [string, "null"]
        contact_email:
          type: string
          format: email
        organization_name:
          type: [string, "null"]
    AccountResponse:
      type: object
      required: [api_version, data]
      properties:
        api_version:
          type: string
          const: v1
        data:
          type: object
          required: [account_id, plan, entitlements]
          properties:
            account_id:
              type: string
            plan:
              type: string
            entitlements:
              type: array
              items:
                type: string
    UsageResponse:
      type: object
      required: [api_version, data]
      properties:
        api_version:
          type: string
          const: v1
        data:
          type: object
          required: [period_start, period_end, used_units]
          properties:
            period_start:
              type: string
              format: date-time
            period_end:
              type: string
              format: date-time
            used_units:
              type: integer
            included_units:
              type: [integer, "null"]
            remaining_units:
              type: [integer, "null"]
    Problem:
      type: object
      required: [type, title, status, request_id]
      properties:
        type:
          type: string
          format: uri
        title:
          type: string
        status:
          type: integer
        detail:
          type: string
        instance:
          type: string
        request_id:
          type: string
