openapi: 3.0.3
info:
  title: HumMatch Partner API
  description: >
    A partner-facing API over HumMatch's real vocal-range and song-matching
    logic: normalize a singer's vocal profile, score how well a specific
    catalog song fits that profile, and find shared-range song candidates for
    a small group. Every endpoint here is stateless computation over
    caller-submitted data plus read-only catalog lookups -- no HumMatch user
    accounts, hum sessions, or SquadMatch groups are created or touched.
  version: "1.0.0"
  contact:
    name: HumMatch
    url: https://hummatch.me/contact
servers:
  - url: https://hummatch.me/api
    description: Production
  - url: http://localhost:3000/api
    description: Local development

tags:
  - name: Onboarding
    description: Self-serve sandbox API key issuance.
  - name: Vocal Profile
    description: Normalize a singer's vocal range/hum-feature input.
  - name: Song Fit
    description: Score a single catalog song against a vocal profile.
  - name: Group Fit
    description: Find shared-range song candidates for multiple singers.

paths:
  /v1/sandbox-key:
    post:
      operationId: createSandboxKey
      tags: [Onboarding]
      summary: Instantly issue a sandbox API key
      description: >
        Public endpoint, no API key required -- this IS the onboarding step.
        Generates a new `hm_sandbox_...` key immediately, with no human
        approval or email verification. Rate-limited per IP (5 keys/hour) to
        prevent abuse, since it is the one endpoint that doesn't itself
        require an API key.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
                  example: partner@example.com
                name:
                  type: string
                  example: Acme Karaoke Co.
      responses:
        "200":
          description: Sandbox key created. The `apiKey` value is shown exactly once.
          content:
            application/json:
              schema:
                type: object
                properties:
                  apiKey:
                    type: string
                    description: >
                      The full plaintext API key. Store it now -- HumMatch
                      never stores or displays the plaintext value again.
                    example: hm_sandbox_3f9a1c2b7d8e4f5061a2b3c4d5e6f708
                  keyPrefix:
                    type: string
                    example: hm_sandbox_3
                  mode:
                    type: string
                    enum: [sandbox]
                  rateLimitPerMinute:
                    type: integer
                    example: 30
        "400":
          description: Missing or invalid email.
        "429":
          description: Too many sandbox keys requested from this IP; see `Retry-After` header.

  /v1/vocal-profile:
    post:
      operationId: buildVocalProfile
      tags: [Vocal Profile]
      summary: Normalize a vocal range or hum-feature profile
      security:
        - ApiKeyAuth: []
      description: >
        Accepts either `{ rangeLow, rangeHigh }` (note names like "C3"/"A4"
        or raw MIDI numbers) or `{ humProfile: {...} }` (audio-feature-style
        input). Returns a normalized profile object that can be passed
        directly into `/v1/songs/{slug}/fit` and `/v1/group-fit`. Pure
        computation, no persistence.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - type: object
                  required: [rangeLow, rangeHigh]
                  properties:
                    rangeLow:
                      type: string
                      example: "C3"
                    rangeHigh:
                      type: string
                      example: "A4"
                - type: object
                  required: [humProfile]
                  properties:
                    humProfile:
                      $ref: "#/components/schemas/HumProfileInput"
            examples:
              byRange:
                summary: Note-name range
                value:
                  rangeLow: "C3"
                  rangeHigh: "A4"
              byHumProfile:
                summary: Audio-feature hum profile
                value:
                  humProfile:
                    low: "C3"
                    high: "A4"
                    confidence: 0.82
                    pitchStability: 0.74
                    brightnessScore: 58
      responses:
        "200":
          description: Normalized vocal profile.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VocalProfile"
              example:
                vocalId: "Baritone · Warm tone"
                rangeLow: 48
                rangeHigh: 69
                voiceType: Baritone
                toneLabel: Warm
                humProfile:
                  low: 48
                  high: 69
                  center: 58.5
                  confidence: 0.82
                  pitchStability: 0.74
                  brightnessScore: 58
                  warmthScore: 42
                  timbreLabel: Balanced
                  timbreConfidence: 0.615
        "400":
          description: Missing/invalid rangeLow, rangeHigh, or humProfile.
        "401":
          description: Missing, invalid, or revoked API key.
        "429":
          description: Rate limit exceeded for this API key; see `Retry-After` header.

  /v1/songs/{slug}/fit:
    get:
      operationId: getSongFit
      tags: [Song Fit]
      summary: Score one catalog song against a vocal profile
      security:
        - ApiKeyAuth: []
      description: >
        Looks up a single song by its catalog slug and scores it against the
        supplied profile using HumMatch's real per-song scoring function.
        Read-only: one row lookup plus scoring, no catalog scan and no writes.
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
          example: love-is-a-game-adele
          description: The catalog song's slug (its real identifier, not a numeric id).
        - name: profile
          in: query
          required: true
          schema:
            type: string
          description: >
            URL-encoded JSON matching the response shape of
            `POST /v1/vocal-profile`, or the same `{ rangeLow, rangeHigh }` /
            `{ humProfile }` shape directly -- both are accepted.
          example: "%7B%22rangeLow%22%3A%22G3%22%2C%22rangeHigh%22%3A%22D5%22%7D"
      responses:
        "200":
          description: Fit score and explanation for this song against this profile.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SongFit"
              example:
                slug: love-is-a-game-adele
                title: Love Is a Game
                artist: Adele
                matchScore: 78
                vocalFitScore: 81
                tessituraScore: 74
                voiceTypeFitScore: 88
                whyItFits: "Strong likely fit: the melody sits close to your comfortable range and the song is familiar."
                reason: "Strong likely fit: the melody sits close to your comfortable range and the song is familiar."
                riskWarning: ""
                suggestedKey: ""
                fitDetails:
                  rangeFitLabel: Strong fit
                  voiceTypeFitLabel: Strong voice lane fit
                  priorityTier: 2
                  riskLabel: No major range risk
        "400":
          description: Missing/invalid `profile` query parameter.
        "401":
          description: Missing, invalid, or revoked API key.
        "404":
          description: No song found for the given slug.
        "429":
          description: Rate limit exceeded for this API key; see `Retry-After` header.

  /v1/group-fit:
    post:
      operationId: getGroupFit
      tags: [Group Fit]
      summary: Find shared-range song candidates for multiple singers
      security:
        - ApiKeyAuth: []
      description: >
        Accepts 2+ caller-submitted vocal profiles and computes their shared
        vocal range plus ranked candidate songs, using the same logic
        HumMatch's SquadMatch feature uses -- entirely in-memory over the
        submitted profiles. No SquadMatch group, member, or match rows are
        ever created. `sharedRange` is `null` when the profiles genuinely
        have no overlap; it is never fabricated.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [profiles]
              properties:
                profiles:
                  type: array
                  minItems: 2
                  items:
                    type: object
                    required: [rangeLow, rangeHigh]
                    properties:
                      id:
                        type: string
                        example: singer-1
                      rangeLow:
                        type: string
                        example: "C3"
                      rangeHigh:
                        type: string
                        example: "E4"
            example:
              profiles:
                - id: singer-1
                  rangeLow: "C3"
                  rangeHigh: "E4"
                - id: singer-2
                  rangeLow: "A3"
                  rangeHigh: "G5"
      responses:
        "200":
          description: Shared range and ranked candidate songs.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GroupFit"
              example:
                sharedRange:
                  low: 57
                  high: 64
                songs:
                  - slug: friends-in-low-places-garth-brooks
                    title: Friends in Low Places
                    artist: Garth Brooks
                    genre: Country
                    year: 1990
                    difficulty: Easy win
                    fitLabel: Group-ready
                    reason: Tagged as group-friendly and close to the shared comfort lane.
                    fitsMemberIds: [singer-1, singer-2]
        "400":
          description: Fewer than 2 profiles, or an invalid rangeLow/rangeHigh in one of them.
        "401":
          description: Missing, invalid, or revoked API key.
        "429":
          description: Rate limit exceeded for this API key; see `Retry-After` header.

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
  schemas:
    HumProfileInput:
      type: object
      properties:
        low:
          description: Note name or MIDI number.
          example: "C3"
        high:
          description: Note name or MIDI number.
          example: "A4"
        confidence:
          type: number
          minimum: 0
          maximum: 1
        pitchStability:
          type: number
          minimum: 0
          maximum: 1
        brightnessScore:
          type: number
          minimum: 0
          maximum: 100
        timbreLabel:
          type: string
    VocalProfile:
      type: object
      properties:
        vocalId:
          type: string
        rangeLow:
          type: integer
        rangeHigh:
          type: integer
        voiceType:
          type: string
        toneLabel:
          type: string
        humProfile:
          type: object
          properties:
            low:
              type: integer
            high:
              type: integer
            center:
              type: number
            confidence:
              type: number
            pitchStability:
              type: number
            brightnessScore:
              type: number
              nullable: true
            warmthScore:
              type: number
              nullable: true
            timbreLabel:
              type: string
            timbreConfidence:
              type: number
    SongFit:
      type: object
      properties:
        slug:
          type: string
        title:
          type: string
        artist:
          type: string
        matchScore:
          type: integer
        vocalFitScore:
          type: integer
        tessituraScore:
          type: integer
        voiceTypeFitScore:
          type: integer
        whyItFits:
          type: string
        reason:
          type: string
        riskWarning:
          type: string
        suggestedKey:
          type: string
        fitDetails:
          type: object
    GroupFit:
      type: object
      properties:
        sharedRange:
          type: object
          nullable: true
          properties:
            low:
              type: integer
            high:
              type: integer
        songs:
          type: array
          items:
            type: object
            properties:
              slug:
                type: string
              title:
                type: string
              artist:
                type: string
              genre:
                type: string
              year:
                type: integer
              difficulty:
                type: string
              fitLabel:
                type: string
              reason:
                type: string
              fitsMemberIds:
                type: array
                items:
                  type: string
