openapi: 3.1.0
info:
  title: Silicon Waveform API
  version: 0.1.0
  summary: Provider-independent text-to-speech and speech-to-text
  description: |
    Synchronous speech operations for Carbons, Silicons, and IAM applications.
    Provider selection and fallback are internal. TTS output is normalized to
    MP3 and stored in the represented actor's Briefcase. OBO requests are
    accepted only when the configured IAM and Briefcase request-bound contracts
    are available. Test planes use deterministic speech with paired IAM and
    Briefcase SDK uploads, delegated reads, and current replay access checks.
    Inbound OBO speech is unavailable until IAM supports downstream delegation
    from the initiating request. Bearer speech is implemented.
servers:
  - url: https://waveform.teamofsilicons.com/api/v1
security:
  - bearerAuth: []
tags:
  - name: Speech
  - name: Capabilities
  - name: Authentication
  - name: Jobs
  - name: Testing environments
  - name: Account
  - name: Webhooks

paths:
  /api/v1/telemetry:
    post:
      operationId: recordBrowserEvent
      summary: Record an authenticated browser diagnostic
      description: Test events and opted-out account events are discarded. No credentials or user content are accepted.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [event]
              properties:
                event: { type: string, enum: [speech_completed, speech_failed, settings_saved, page_view] }
                elapsed_ms: { type: integer, minimum: 0, maximum: 86400000, default: 0 }
      responses:
        '204': { description: Event handled }
  /api/v1/reports:
    post:
      operationId: submitBugReport
      summary: Submit a bug report with optional pull request
      description: Authenticated, idempotent, ten reports per actor per hour. Testing simulates delivery.
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/TestingEnvironmentKey'
        - name: Idempotency-Key
          in: header
          required: true
          schema: { type: string, minLength: 1, maxLength: 128 }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [message]
              additionalProperties: false
              properties:
                message: { type: string, minLength: 1, maxLength: 60000 }
                pr: { type: [string, 'null'], format: uri }
                client_version: { type: string, maxLength: 100 }
      responses:
        '202':
          description: Persisted report with queued, sent, or simulated notification status
        '400': { description: Invalid report or credential content }
        '401': { description: Missing or invalid authentication }
        '409': { description: Idempotency key reused with different content }
        '429': { description: Hourly report limit exceeded }
        '503': { description: Postmark not configured }

  /tts:
    post:
      security:
        - bearerAuth: []
        - oboAccess: []
      tags: [Speech]
      operationId: synthesizeSpeech
      summary: Convert text to speech and store the MP3 in Briefcase
      description: |
        Attempts Gemini 3.1 Flash TTS Preview, ElevenLabs Multilingual v2,
        then OpenAI tts-1. Returns only after the normalized MP3 is stored and
        both its permanent and temporary Briefcase URLs are available. A
        completed idempotent retry re-authorizes the stored file and returns a
        freshly issued temporary URL without rerunning synthesis.
      parameters:
        - $ref: '#/components/parameters/OrgId'
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/AppId'
        - $ref: '#/components/parameters/RequestId'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TTSRequest' }
      responses:
        '200':
          description: Stored audio result, with a currently authorized temporary URL
          headers:
            X-Request-ID: { $ref: '#/components/headers/RequestId' }
            Idempotency-Replayed: { $ref: '#/components/headers/IdempotencyReplayed' }
            Cache-Control: { $ref: '#/components/headers/CacheControl' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TTSResult' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/SourceNotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '413': { $ref: '#/components/responses/PayloadTooLarge' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '502': { $ref: '#/components/responses/BadGateway' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }
        '504': { $ref: '#/components/responses/GatewayTimeout' }
        default: { $ref: '#/components/responses/Error' }

  /stt:
    post:
      security:
        - bearerAuth: []
        - oboAccess: []
      tags: [Speech]
      operationId: transcribeSpeech
      summary: Transcribe an authorized Briefcase file
      description: |
        Re-authorizes the permanent Briefcase reference as the represented
        actor, then attempts Gemini 3.5 Transcribe, OpenAI gpt-transcribe, and
        Deepgram Nova-3 Multilingual. Waveform never fetches the supplied URL
        directly. A completed idempotent retry verifies current access to the
        exact source before returning its cached transcript.
      parameters:
        - $ref: '#/components/parameters/OrgId'
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/AppId'
        - $ref: '#/components/parameters/RequestId'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/STTRequest' }
      responses:
        '200':
          description: Normalized transcript released after current source authorization
          headers:
            X-Request-ID: { $ref: '#/components/headers/RequestId' }
            Idempotency-Replayed: { $ref: '#/components/headers/IdempotencyReplayed' }
            Cache-Control: { $ref: '#/components/headers/CacheControl' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/STTResult' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/SourceNotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '413': { $ref: '#/components/responses/PayloadTooLarge' }
        '415': { $ref: '#/components/responses/UnsupportedMediaType' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '502': { $ref: '#/components/responses/BadGateway' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }
        '504': { $ref: '#/components/responses/GatewayTimeout' }
        default: { $ref: '#/components/responses/Error' }

  /capabilities:
    get:
      tags: [Capabilities]
      operationId: getCapabilities
      summary: Get the stable Waveform-level speech capabilities
      security: []
      parameters:
        - $ref: '#/components/parameters/RequestId'
      responses:
        '200':
          description: Normalized service capabilities, independent of live provider health
          headers:
            X-Request-ID: { $ref: '#/components/headers/RequestId' }
            Cache-Control: { $ref: '#/components/headers/CacheControl' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Capabilities' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        default: { $ref: '#/components/responses/Error' }

  /iam:
    get:
      tags: [Authentication]
      operationId: getIamInfo
      summary: Discover public IAM application metadata
      security: []
      parameters:
        - $ref: '#/components/parameters/TestingEnvironmentKey'
      responses:
        '200':
          description: Public configuration for the selected environment, without secrets
          headers:
            Cache-Control: { $ref: '#/components/headers/CacheControl' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IamInfo' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }

  /auth/login:
    post:
      tags: [Authentication]
      operationId: login
      summary: Exchange an IAM SLT for Waveform session tokens
      security: []
      parameters:
        - $ref: '#/components/parameters/TestingEnvironmentKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/LoginRequest' }
      responses:
        '200':
          description: IAM access and refresh tokens
          headers:
            Cache-Control: { $ref: '#/components/headers/CacheControl' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/OAuthTokenResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }

  /auth/refresh:
    post:
      tags: [Authentication]
      operationId: refresh
      summary: Exchange an IAM refresh token for a new token pair
      security: []
      parameters:
        - $ref: '#/components/parameters/TestingEnvironmentKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RefreshRequest' }
      responses:
        '200':
          description: Refreshed IAM access and refresh tokens
          headers:
            Cache-Control: { $ref: '#/components/headers/CacheControl' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/OAuthTokenResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }

  /auth/logout:
    post:
      tags: [Authentication]
      operationId: logout
      summary: Revoke an IAM access or refresh token
      security: []
      parameters:
        - $ref: '#/components/parameters/TestingEnvironmentKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/LogoutRequest' }
      responses:
        '204': { description: Token revoked successfully }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }

  /auth/me:
    get:
      tags: [Authentication]
      operationId: getCurrentActor
      summary: Return the current actor's verified IAM authority
      parameters:
        - in: header
          name: X-Org-ID
          required: false
          description: Optional organization assertion; otherwise derived from IAM.
          schema: { type: string }
        - $ref: '#/components/parameters/TestingEnvironmentKey'
      responses:
        '200':
          description: Verified actor and organization authority
          headers:
            Cache-Control: { $ref: '#/components/headers/CacheControl' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApplicationAuthorization' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /jobs:
    get:
      tags: [Jobs]
      operationId: listJobs
      summary: List actor-scoped speech job history
      parameters:
        - $ref: '#/components/parameters/OrgId'
        - $ref: '#/components/parameters/TestingEnvironmentKey'
        - in: query
          name: operation
          schema: { type: string, enum: [tts, stt] }
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
        - in: query
          name: cursor
          description: Opaque keyset cursor returned as next_cursor.
          schema: { type: string }
      responses:
        '200':
          description: Newest jobs first
          content:
            application/json:
              schema: { $ref: '#/components/schemas/JobPage' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /jobs/{job_id}:
    get:
      tags: [Jobs]
      operationId: getJob
      summary: Get one actor-scoped speech job
      parameters:
        - $ref: '#/components/parameters/OrgId'
        - $ref: '#/components/parameters/TestingEnvironmentKey'
        - $ref: '#/components/parameters/JobId'
      responses:
        '200':
          description: Speech job
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Job' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/Error' }

  /preferences:
    get:
      tags: [Account]
      operationId: getPreferences
      summary: Get effective provider order and account voice profile
      parameters:
        - $ref: '#/components/parameters/OrgId'
        - $ref: '#/components/parameters/TestingEnvironmentKey'
      responses:
        '200':
          description: Effective account provider preferences
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Preferences' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
    patch:
      tags: [Account]
      operationId: updatePreferences
      summary: Set account provider order and/or default voice profile
      parameters:
        - $ref: '#/components/parameters/OrgId'
        - $ref: '#/components/parameters/TestingEnvironmentKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PreferencesUpdate' }
      responses:
        '200':
          description: Updated provider order preferences
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PreferencesUpdate' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /voice-profiles:
    get:
      tags: [Account]
      operationId: listVoiceProfiles
      summary: List profiles and provider mappings in the selected environment
      parameters:
        - $ref: '#/components/parameters/OrgId'
        - $ref: '#/components/parameters/TestingEnvironmentKey'
      responses:
        '200':
          description: Voice catalog; provisional mappings have auditioned=false
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: '#/components/schemas/VoiceProfile' } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /provider-keys:
    get:
      tags: [Account]
      operationId: listProviderKeys
      summary: List configured provider keys without revealing secrets
      parameters:
        - $ref: '#/components/parameters/OrgId'
        - $ref: '#/components/parameters/TestingEnvironmentKey'
      responses:
        '200':
          description: Configured provider metadata
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items: { $ref: '#/components/schemas/ProviderKeyInfo' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /provider-keys/{provider}:
    put:
      tags: [Account]
      operationId: setProviderKey
      summary: Replace a personal provider API key
      parameters:
        - $ref: '#/components/parameters/OrgId'
        - $ref: '#/components/parameters/TestingEnvironmentKey'
        - $ref: '#/components/parameters/Provider'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ProviderKeyRequest' }
      responses:
        '204': { description: Provider key stored }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
    delete:
      tags: [Account]
      operationId: deleteProviderKey
      summary: Delete a personal provider API key
      parameters:
        - $ref: '#/components/parameters/OrgId'
        - $ref: '#/components/parameters/TestingEnvironmentKey'
        - $ref: '#/components/parameters/Provider'
      responses:
        '204': { description: Provider key deleted }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /testing-environment:
    get:
      tags: [Testing environments]
      operationId: getCurrentTestingEnvironment
      summary: Describe the selected isolated test plane
      security: []
      parameters:
        - $ref: '#/components/parameters/TestingEnvironmentKey'
      responses:
        '200':
          description: Selected testing environment metadata
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TestingEnvironment' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /testing-environment/clean:
    post:
      tags: [Testing environments]
      operationId: cleanTestingEnvironment
      summary: Delete all disposable data in the selected test plane
      security: []
      parameters:
        - $ref: '#/components/parameters/TestingEnvironmentKey'
      responses:
        '200':
          description: Disposable test-plane data removed
          content:
            application/json:
              schema:
                type: object
                required: [cleaned]
                properties: { cleaned: { type: boolean, const: true } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /testing-environments:
    post:
      tags: [Testing environments]
      operationId: createTestingEnvironment
      summary: Create an isolated test plane
      parameters:
        - $ref: '#/components/parameters/OrgId'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateTestingEnvironment' }
      responses:
        '200':
          description: Created plane and one-time root key
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CreatedTestingEnvironment' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    get:
      tags: [Testing environments]
      operationId: listTestingEnvironments
      summary: List test planes in the current organization
      parameters:
        - $ref: '#/components/parameters/OrgId'
      responses:
        '200':
          description: Plane metadata; root keys are never listed
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: '#/components/schemas/TestingEnvironment' } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /testing-environments/{environment_id}:
    get:
      tags: [Testing environments]
      operationId: getTestingEnvironment
      summary: Get test-plane metadata
      parameters:
        - $ref: '#/components/parameters/OrgId'
        - $ref: '#/components/parameters/EnvironmentId'
      responses:
        '200':
          description: Test-plane metadata
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TestingEnvironment' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/Error' }

  /testing-environments/{environment_id}/key:
    get:
      tags: [Testing environments]
      operationId: getTestingEnvironmentKey
      summary: Retrieve the current test-plane root key
      parameters:
        - $ref: '#/components/parameters/OrgId'
        - $ref: '#/components/parameters/EnvironmentId'
      responses:
        '200':
          description: Current root key
          content:
            application/json:
              schema:
                type: object
                required: [key]
                properties: { key: { type: string, minLength: 32, maxLength: 32 } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/Error' }

  /testing-environments/{environment_id}/rotate-key:
    post:
      tags: [Testing environments]
      operationId: rotateTestingEnvironmentKey
      summary: Rotate a test-plane root key
      parameters:
        - $ref: '#/components/parameters/OrgId'
        - $ref: '#/components/parameters/EnvironmentId'
      responses:
        '200':
          description: New root key; previous key is immediately invalid
          content:
            application/json:
              schema:
                type: object
                required: [key]
                properties: { key: { type: string, minLength: 32, maxLength: 32 } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/Error' }

  /testing-environments/{environment_id}/delete:
    post:
      tags: [Testing environments]
      operationId: deleteTestingEnvironment
      summary: Soft-delete a test plane
      parameters:
        - $ref: '#/components/parameters/OrgId'
        - $ref: '#/components/parameters/EnvironmentId'
      responses:
        '200':
          description: Plane marked deleted and recoverable for 30 days
          content:
            application/json:
              schema:
                type: object
                required: [deleted, recoverable_for_days]
                properties:
                  deleted: { type: boolean, const: true }
                  recoverable_for_days: { type: integer, const: 30 }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/Error' }

  /testing-environments/{environment_id}/restore:
    post:
      tags: [Testing environments]
      operationId: restoreTestingEnvironment
      summary: Restore a soft-deleted test plane during its recovery window
      parameters:
        - $ref: '#/components/parameters/OrgId'
        - $ref: '#/components/parameters/EnvironmentId'
      responses:
        '200':
          description: Plane restored
          content:
            application/json:
              schema:
                type: object
                required: [restored]
                properties: { restored: { type: boolean, const: true } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/Error' }

  /webhooks/:
    post:
      tags: [Authentication]
      operationId: receiveIamWebhook
      summary: Receive a signed IAM event
      description: Verifies exact request bytes and deduplicates by event ID. Payload content is never persisted.
      servers:
        - url: https://waveform.teamofsilicons.com
      security: []
      requestBody:
        required: true
        content:
          application/json: { schema: { type: object } }
      responses:
        '204': { description: Event accepted or already processed }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: IAM access token
      description: Opaque IAM access token for a Carbon or Silicon.
    oboAccess:
      type: apiKey
      in: header
      name: X-IAM-OBO-Access-Proof
      description: |
        Short-lived IAM proof bound to Waveform, the represented actor,
        organization, and requested action. X-App-ID is also required.

  parameters:
    OrgId:
      name: X-Org-ID
      in: header
      required: true
      description: Organization selected for this operation; it must match verified IAM state.
      schema:
        type: string
        minLength: 1
        maxLength: 64
        pattern: '^[A-Za-z0-9_-]+$'
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: |
        Scoped to actor, organization, and operation for at least 24 hours
        (the default is 24 hours). Reusing a key with a different validated
        request during retention returns 409.
      schema:
        type: string
        minLength: 8
        maxLength: 255
        pattern: '^[!-~]+$'
    AppId:
      name: X-App-ID
      in: header
      required: false
      description: Required with X-IAM-OBO-Access-Proof and omitted for Bearer requests.
      schema:
        type: string
        minLength: 1
        maxLength: 80
        pattern: '^[A-Za-z0-9_-]+$'
    RequestId:
      name: X-Request-ID
      in: header
      required: false
      description: |
        Optional attempt correlation UUID. Invalid values are replaced by
        Waveform. A successful idempotent replay returns the stored canonical
        operation UUID instead.
      schema: { type: string, format: uuid }
    TestingEnvironmentKey:
      name: X-Testing-Environment-Key
      in: header
      required: false
      description: IAM test app_secret selecting the Waveform sandbox. Validated live; no paired Briefcase key needed. Legacy root selectors remain accepted.
      schema: { type: string, pattern: '^(ask_[A-Za-z0-9_-]{43}|[A-Za-z0-9]{32})$' }
    JobId:
      name: job_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    EnvironmentId:
      name: environment_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    Provider:
      name: provider
      in: path
      required: true
      schema: { type: string, enum: [gemini, elevenlabs, openai, deepgram] }

  headers:
    RequestId:
      description: |
        Waveform request correlation ID. A successful speech replay uses the
        stored canonical operation UUID; replay-time errors use the current
        attempt UUID.
      schema: { type: string, format: uuid }
    IdempotencyReplayed:
      description: |
        True when stable completed-operation state was reused. Any TTS
        temporary delivery URL in the response was freshly authorized and is
        not loaded from the idempotency record.
      schema: { type: boolean }
    CacheControl:
      description: Prevents intermediaries from retaining private speech data or correlation IDs.
      schema: { type: string, const: no-store }

  responses:
    BadRequest:
      description: Malformed headers, credentials, URL, language hint, or JSON input
      headers:
        X-Request-ID: { $ref: '#/components/headers/RequestId' }
        Cache-Control: { $ref: '#/components/headers/CacheControl' }
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Unauthorized:
      description: IAM credentials are missing, invalid, expired, or inactive
      headers:
        X-Request-ID: { $ref: '#/components/headers/RequestId' }
        Cache-Control: { $ref: '#/components/headers/CacheControl' }
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Forbidden:
      description: The represented actor cannot use this organization or resource
      headers:
        X-Request-ID: { $ref: '#/components/headers/RequestId' }
        Cache-Control: { $ref: '#/components/headers/CacheControl' }
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    SourceNotFound:
      description: Briefcase reports the exact STT source or completed TTS replay file as missing
      headers:
        X-Request-ID: { $ref: '#/components/headers/RequestId' }
        Cache-Control: { $ref: '#/components/headers/CacheControl' }
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Conflict:
      description: Idempotency key mismatch or another request currently owns the key
      headers:
        X-Request-ID: { $ref: '#/components/headers/RequestId' }
        Retry-After: { schema: { type: integer, minimum: 1 } }
        Cache-Control: { $ref: '#/components/headers/CacheControl' }
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    PayloadTooLarge:
      description: Text, request body, source media, or generated audio exceeds a synchronous limit
      headers:
        X-Request-ID: { $ref: '#/components/headers/RequestId' }
        Cache-Control: { $ref: '#/components/headers/CacheControl' }
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    UnsupportedMediaType:
      description: Briefcase source media is not accepted by the normalized STT contract
      headers:
        X-Request-ID: { $ref: '#/components/headers/RequestId' }
        Cache-Control: { $ref: '#/components/headers/CacheControl' }
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    TooManyRequests:
      description: Waveform admission control rejected the request
      headers:
        X-Request-ID: { $ref: '#/components/headers/RequestId' }
        Retry-After: { schema: { type: integer, minimum: 1 } }
        Cache-Control: { $ref: '#/components/headers/CacheControl' }
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    BadGateway:
      description: Every provider failed or returned an invalid response
      headers:
        X-Request-ID: { $ref: '#/components/headers/RequestId' }
        Cache-Control: { $ref: '#/components/headers/CacheControl' }
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    ServiceUnavailable:
      description: IAM, Briefcase, PostgreSQL, or a required contract capability is unavailable
      headers:
        X-Request-ID: { $ref: '#/components/headers/RequestId' }
        Retry-After: { schema: { type: integer, minimum: 1 } }
        Cache-Control: { $ref: '#/components/headers/CacheControl' }
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    GatewayTimeout:
      description: The bounded synchronous operation exceeded its deadline
      headers:
        X-Request-ID: { $ref: '#/components/headers/RequestId' }
        Cache-Control: { $ref: '#/components/headers/CacheControl' }
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Error:
      description: Normalized Waveform error
      headers:
        X-Request-ID: { $ref: '#/components/headers/RequestId' }
        Cache-Control: { $ref: '#/components/headers/CacheControl' }
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }

  schemas:
    VoiceProfileRef:
      type: object
      required: [id, revision]
      properties:
        id: { type: string }
        revision: { type: integer, minimum: 1 }
    VoiceProfile:
      type: object
      required: [id, name, description, revision, auditioned, gemini_voice, openai_voice, elevenlabs]
      properties:
        id: { type: string, pattern: '^[a-z0-9-]{1,64}$' }
        name: { type: string }
        description: { type: string }
        revision: { type: integer, minimum: 1 }
        auditioned: { type: boolean, description: Whether a listening review has confirmed the cross-provider mapping }
        gemini_voice: { type: string }
        openai_voice: { type: string, enum: [alloy, ash, coral, echo, fable, onyx, nova, sage, shimmer] }
        elevenlabs:
          type: object
          required: [voice_id, model_id, voice_settings]
          properties:
            voice_id: { type: string }
            model_id: { type: string, const: eleven_multilingual_v2 }
            voice_settings:
              type: object
              required: [stability, similarity_boost, style, use_speaker_boost, speed]
              properties:
                stability: { type: number, minimum: 0, maximum: 1 }
                similarity_boost: { type: number, minimum: 0, maximum: 1 }
                style: { type: number, minimum: 0, maximum: 1 }
                use_speaker_boost: { type: boolean }
                speed: { type: number, minimum: 0.7, maximum: 1.2 }

    TTSRequest:
      type: object
      additionalProperties: false
      required: [text]
      properties:
        voice_profile:
          type: [string, 'null']
          pattern: '^[a-z0-9-]{1,64}$'
          description: Optional profile ID from voice-profiles. Omitted/null uses the account default. Applies to all fallback providers and never changes the saved default.
        text:
          type: string
          minLength: 1
          maxLength: 4096
          description: Non-whitespace text. The limit counts Unicode scalar values.
        lang:
          type: string
          minLength: 2
          maxLength: 64
          description: Optional canonicalizable BCP 47 hint; passed only to Gemini TTS.
        provider_order:
          type: array
          minItems: 1
          maxItems: 3
          uniqueItems: true
          description: Preferred provider prefix; omitted providers follow the account order.
          items:
            type: string
            enum: [gemini, elevenlabs, openai]

    STTRequest:
      type: object
      additionalProperties: false
      required: [file_url]
      properties:
        file_url:
          type: string
          format: uri
          pattern: '^https://'
          description: Permanent authenticated URL at the configured Silicon Briefcase origin.
        language:
          type: string
          minLength: 2
          maxLength: 64
          description: Optional canonicalizable BCP 47 transcription hint.
        provider_order:
          type: array
          minItems: 1
          maxItems: 3
          uniqueItems: true
          description: Preferred provider prefix; omitted providers follow the account order.
          items:
            type: string
            enum: [gemini, openai, deepgram]

    TTSResult:
      type: object
      additionalProperties: false
      required: [request_id, file_url, temporary_url, media_type, provider, duration_ms]
      properties:
        voice_profile:
          oneOf:
            - $ref: '#/components/schemas/VoiceProfileRef'
            - { type: 'null' }
        request_id:
          type: string
          format: uuid
          description: Canonical operation UUID, preserved on completed replay.
        file_url: { type: string, format: uri, description: Permanent authenticated Briefcase URL }
        temporary_url:
          type: [string, 'null']
          format: uri
          description: Optional expiring delivery URL; null for published Briefcase one-shot uploads.
        media_type: { type: string, const: audio/mpeg }
        provider:
          type: string
          enum: [gemini, elevenlabs, openai]
          description: Provider that completed synthesis
        duration_ms: { type: integer, format: int64, minimum: 0 }

    STTResult:
      type: object
      additionalProperties: false
      required: [request_id, transcript, detected_language, provider, duration_ms]
      properties:
        request_id:
          type: string
          format: uuid
          description: Canonical operation UUID, preserved on completed replay.
        transcript: { type: string }
        detected_language: { type: [string, 'null'] }
        provider:
          type: string
          enum: [gemini, openai, deepgram]
          description: Provider that completed transcription
        duration_ms: { type: [integer, 'null'], format: int64, minimum: 0 }

    LoginRequest:
      type: object
      additionalProperties: false
      required: [slt]
      properties:
        slt: { type: string, minLength: 1, maxLength: 16384, description: 'Production requires an IAM SLT. A selected sandbox also accepts an existing active identity public ID.' }
    RefreshRequest:
      type: object
      additionalProperties: false
      required: [refresh_token]
      properties:
        refresh_token: { type: string, minLength: 5, maxLength: 16384, pattern: '^ort_[!-~]+$' }
    LogoutRequest:
      type: object
      additionalProperties: false
      required: [token]
      properties:
        token: { type: string, minLength: 5, maxLength: 16384, pattern: '^o(at|rt)_[!-~]+$' }
    OAuthTokenResponse:
      type: object
      required: [access_token, refresh_token, token_type, expires_in, refresh_expires_at, actor, session_id]
      properties:
        access_token: { type: string }
        refresh_token: { type: string }
        token_type: {}
        expires_in: { type: integer, format: int64 }
        refresh_expires_at: { type: string, format: date-time }
        actor: { $ref: '#/components/schemas/ActorRef' }
        session_id: { type: string, format: uuid }
    ActorRef:
      type: object
      required: [principal_id, type, public_id]
      properties:
        principal_id: { type: string, format: uuid }
        type: { type: string }
        public_id: { type: string }
    IamInfo:
      type: object
      additionalProperties: false
      required: [app_id, iam_base_url, testing_environment_id]
      properties:
        app_id: { type: string }
        iam_base_url: { type: string, format: uri }
        testing_environment_id:
          type: [string, 'null']
          format: uuid
          description: Upstream IAM test environment UUID, or null for production.
    ApplicationAuthorization:
      type: object
      required: [principal_id, actor_type, public_id, organization_id, org_id, membership_id, membership_version, authorization_epoch, audience, scopes]
      properties:
        principal_id: { type: string, format: uuid }
        actor_type: { type: string }
        public_id: { type: string }
        organization_id: { type: string, format: uuid }
        org_id: { type: string }
        membership_id: { type: string, format: uuid }
        membership_version: { type: integer, format: int64 }
        authorization_epoch: { type: integer, format: int64 }
        audience: { type: string }
        testing_environment_id: { type: [string, 'null'], format: uuid }
        scopes: { type: array, items: { type: string } }
        org_role: { type: [string, 'null'] }
        tags: { type: [array, 'null'], items: { type: object } }
    Job:
      type: object
      required: [id, operation, status, first_line, created_at]
      properties:
        voice_profile:
          oneOf:
            - $ref: '#/components/schemas/VoiceProfileRef'
            - { type: 'null' }
        id: { type: string, format: uuid }
        operation: { type: string, enum: [tts, stt] }
        status: { type: string, enum: [running, failed, completed] }
        first_line: { type: string }
        duration_ms: { type: [integer, 'null'], format: int64, minimum: 0 }
        provider: { type: [string, 'null'] }
        error_code: { type: [string, 'null'] }
        created_at: { type: string, format: date-time }
        finished_at: { type: [string, 'null'], format: date-time }
    JobPage:
      type: object
      required: [items, next_cursor]
      properties:
        items: { type: array, items: { $ref: '#/components/schemas/Job' } }
        next_cursor: { type: [string, 'null'] }
    Preferences:
      type: object
      required: [tts_order, stt_order, voice_profile, defaults]
      properties:
        telemetry_enabled: { type: boolean, default: true }
        voice_profile: { type: [string, 'null'], pattern: '^[a-z0-9-]{1,64}$' }
        tts_order: { type: array, items: { type: string, enum: [gemini, elevenlabs, openai] } }
        stt_order: { type: array, items: { type: string, enum: [gemini, openai, deepgram] } }
        defaults:
          type: object
          required: [tts_order, stt_order, voice_profile]
          properties:
            voice_profile: { type: [string, 'null'], pattern: '^[a-z0-9-]{1,64}$' }
            tts_order: { type: array, items: { type: string } }
            stt_order: { type: array, items: { type: string } }
    PreferencesUpdate:
      type: object
      additionalProperties: false
      properties:
        telemetry_enabled: { type: boolean, default: true }
        voice_profile: { type: [string, 'null'], pattern: '^[a-z0-9-]{1,64}$' }
        tts_order: { type: [array, 'null'], items: { type: string, enum: [gemini, elevenlabs, openai] } }
        stt_order: { type: [array, 'null'], items: { type: string, enum: [gemini, openai, deepgram] } }
    ProviderKeyRequest:
      type: object
      additionalProperties: false
      required: [api_key]
      properties:
        api_key: { type: string, minLength: 1, maxLength: 16384 }
    ProviderKeyInfo:
      type: object
      required: [provider, configured, updated_at]
      properties:
        provider: { type: string }
        configured: { type: boolean, const: true }
        updated_at: { type: string, format: date-time }
    CreateTestingEnvironment:
      type: object
      additionalProperties: false
      required: [name, iam_environment_id, iam_environment_key, app_secret, briefcase_environment_key]
      properties:
        name: { type: string, minLength: 1, maxLength: 128 }
        description: { type: [string, 'null'], maxLength: 4096 }
        iam_environment_id: { type: string, format: uuid }
        iam_environment_key: { type: string, minLength: 32, maxLength: 32, pattern: '^[A-Za-z0-9]+$' }
        app_secret: { type: string, minLength: 1, maxLength: 512 }
        briefcase_environment_key: { type: string, minLength: 47, maxLength: 47, pattern: '^ask_[A-Za-z0-9_-]{43}$' }
    CreatedTestingEnvironment:
      allOf:
        - $ref: '#/components/schemas/TestingEnvironment'
        - type: object
          required: [key]
          properties:
            key: { type: string, minLength: 32, maxLength: 32, pattern: '^[A-Za-z0-9]+$' }
    TestingEnvironment:
      type: object
      required: [id, name, created_at, last_activity_at]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: [string, 'null'] }
        created_at: { type: string, format: date-time }
        last_activity_at: { type: string, format: date-time }

    Capabilities:
      type: object
      additionalProperties: false
      required: [tts, stt]
      properties:
        tts:
          type: object
          additionalProperties: false
          required: [output_format, languages]
          properties:
            output_format: { type: string, const: mp3 }
            languages:
              type: array
              minItems: 78
              maxItems: 78
              uniqueItems: true
              items:
                type: string
                enum:
                  - af
                  - am
                  - ar
                  - az
                  - be
                  - bg
                  - bn
                  - ca
                  - ceb
                  - cmn
                  - cs
                  - da
                  - de
                  - el
                  - en
                  - es
                  - et
                  - eu
                  - fa
                  - fi
                  - fil
                  - fr
                  - gl
                  - gu
                  - he
                  - hi
                  - hr
                  - ht
                  - hu
                  - hy
                  - id
                  - is
                  - it
                  - ja
                  - jv
                  - ka
                  - kn
                  - ko
                  - kok
                  - la
                  - lb
                  - lo
                  - lt
                  - lv
                  - mai
                  - mg
                  - mk
                  - ml
                  - mn
                  - mr
                  - ms
                  - my
                  - nb
                  - ne
                  - nl
                  - nn
                  - or
                  - pa
                  - pl
                  - ps
                  - pt
                  - ro
                  - ru
                  - sd
                  - si
                  - sk
                  - sl
                  - sq
                  - sr
                  - sv
                  - sw
                  - ta
                  - te
                  - th
                  - tr
                  - uk
                  - ur
                  - vi
              description: Best-effort BCP 47 language hints accepted by Waveform.
        stt:
          type: object
          additionalProperties: false
          required: [languages, accepted_media_types]
          properties:
            languages:
              type: array
              minItems: 10
              maxItems: 10
              uniqueItems: true
              items:
                type: string
                enum: [de, en, es, fr, hi, it, ja, nl, pt, ru]
              description: Best-effort BCP 47 language hints accepted by Waveform.
            accepted_media_types:
              type: array
              minItems: 11
              maxItems: 11
              uniqueItems: true
              items:
                type: string
                enum:
                  - audio/mpeg
                  - audio/wav
                  - audio/flac
                  - audio/ogg
                  - audio/webm
                  - audio/mp4
                  - audio/aac
                  - audio/m4a
                  - video/mp4
                  - video/mpeg
                  - video/webm

    Error:
      type: object
      additionalProperties: false
      required: [error]
      properties:
        error:
          type: object
          additionalProperties: false
          required: [code, message, request_id]
          properties:
            code:
              type: string
              enum:
                - invalid_request
                - unauthenticated
                - forbidden
                - payload_too_large
                - unsupported_media_type
                - source_not_found
                - idempotency_key_reused
                - request_in_progress
                - providers_exhausted
                - dependency_contract_unavailable
                - dependency_unavailable
                - dependency_timeout
                - too_many_requests
                - request_timeout
                - not_ready
                - not_found
                - method_not_allowed
                - internal_error
            message: { type: string, minLength: 1 }
            request_id: { type: string, format: uuid }
