> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pyai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Stream transcription (WebSocket)

> Upgrade to a WebSocket for streaming speech-to-text with eager partials (PyAI Hear first partial measured at about 200 ms in-region. It is revisable and not an SLA). Requires the `hear:stream` scope. The only frame protocol is `pyai-hear-v1`, the published frame catalog below. Cue grounding configuration is reserved but is not active on the serving route.

**Auth:** browsers can't set `Authorization` on a WebSocket, so send the key as a subprotocol, together with the `pyai.v1` marker: `Sec-WebSocket-Protocol: pyai.v1, pyai-key.<API_KEY>` (server clients may use `?api_key=` instead). Offer both values: PyAI echoes only the marker and never reflects your key, and a 101 can only select a value the client offered. The key is validated and swapped for the internal upstream credential on the upgrade.

Set `vocabulary` only when the session has known names, brands, products, or other distinctive terms. This biases known vocabulary only when supplied for the session or enabled through the stored `hear_stream` profile. Request terms take priority and the effective list is fixed when the session opens.

**Client -> server:** stream mono little-endian PCM16 binary audio frames continuously at `sample_rate=8000` or `sample_rate=16000` (default). The service converts 8 kHz input before recognition and endpointing. Other formats are rejected with `400 unsupported_audio_format` before the WebSocket opens. Send a JSON `{"type":"commit"}` text frame to force-finalize the current utterance (e.g. when your VAD detects end-of-turn). Closing the socket also flushes a final for any buffered audio.

Set `endpointing_ms` on the connect URL or send `{"type":"config","endpointing_ms":800}` mid-session. It is the minimum trailing-pause length before an utterance may end. Turn detection may wait longer; an utterance always completes within `max(endpointing_ms, 1500 ms)` of streamed silence. Keep sending audio frames, including silence, while the caller pauses. Pausing the stream pauses the frame-driven clock.

**Server -> client (JSON text frames):**

| `type` | When | Payload |
| --- | --- | --- |
| `config_ack` | after connect-time or mid-session endpointing config | `{endpointing_ms, effective_floor_ms, effective_ceiling_ms, score_interval_ms, warnings}`, the applied values and any validation warnings |
| `partial` | every eager tick | `{text, stable_text, active_text, utterance_id, t_ms}`, the live hypothesis for that `utterance_id` |
| `partial_stable` | when a prefix locks in | `{text, utterance_id, t_ms}`, the portion the recognizer no longer expects to revise |
| `speech_final` | on endpoint/commit | `{text, utterance_id, t_ms, audio_ms, endpoint_reason}`, stable, end of an utterance |
| `final` | follows `speech_final` | `{text, utterance_id, t_ms, audio_ms, endpoint_reason}`, corrected full-context transcript |
| `usage` | just before a graceful close | `{product, meter, audio_seconds, minutes}`, the session's billed active-audio, so you can reconcile realtime spend in-band (a realtime WS carries no `x-pyai-units` response header). Best-effort; absent if the session had no billable audio or closed abnormally. |
| `error` | on fault | `{code, message}` |

`t_ms` is the audio-timeline position of the hypothesis; `audio_ms` is the utterance's active-speech length (the billed signal); `utterance_id` groups partials/finals for one utterance. Assert that `config_ack.warnings` is empty instead of inferring that a setting applied. Warning reasons include `clamped_to_range`, `not_a_number`, and `unknown_config_field`; invalid config leaves the session open. `endpoint_reason` reports why automatic endpointing fired, including `peak_te_early` for the high-confidence path and `silence_backstop` for the bounded fallback. Log it when tuning conversational turns. The Cue grounding config frame and grounding result fields are reserved but currently have no effect on the serving stream.

**Close codes:** `1000` normal · `1008` auth/policy (bad key, scope, revoked token) · `1011` engine error · `4429` over concurrency cap.

**Billing:** metered active audio at the Hear rate ($0.001/min), speech time derived from transcript timing rather than connection wall-clock.



## OpenAPI

````yaml https://api.pyai.com/openapi.json get /v1/audio/transcriptions/stream
openapi: 3.1.0
info:
  title: PyAI API
  version: 2.4.0
  description: >-
    Telephony-native Voice AI behind one bearer key:


    - **Hear**, speech-to-text · `POST /v1/audio/transcriptions` (streaming +
    batch)

    - **Speak**, text-to-speech · `POST /v1/audio/speech`, `GET /v1/voices`

    - **Clone**, custom voices from a short clip · `/v1/voice/clones`

    - **Cast**, auto-directed expressive voiceovers · `/v1/cast`

    - **Dub**, asynchronous English-to-Hindi audio dubbing · `POST /v1/dub`
    ([guide](https://docs.pyai.com/guides/dub-overview))

    - **Cue (not active on the serving stream)**, reserved grounding
    configuration for `GET /v1/audio/transcriptions/stream`

    - **Omni**, full-duplex agentic voice (speech-to-speech, grounded in your
    knowledge bases + tools) · `/v1/omni`

    - **Knowledge Bases**, hosted grounding for Omni: create bases, add
    documents (file, URL, or text), crawl a public website, bind to agents or
    org defaults · `/v1/knowledgebases`

    - **AMD API**, answering-machine detection: know *who or what* answered a
    call (human, voicemail, IVR, iPhone/Google screening, dead number) with the
    reason it decided · `wss …/v1/amd/stream` (Twilio Media Streams drop-in),
    `POST /v1/amd/config`, `GET /v1/amd/calls/{id}`

    - **Agents Beta**, the live console feature to create, configure, test, and
    connect Omni voice agents without code. Beta features and limits may change.


    ## Authentication


    Create a key in the [console](https://console.pyai.com) (it is shown once)
    and send it as a bearer token:


    ```

    Authorization: Bearer pyai_live_...

    ```


    Keys are environment-scoped: `pyai_live_...` (production) and
    `pyai_test_...` (sandbox). `POST /v1/sandbox/keys` creates an instant,
    short-lived test key without login or billing. Account signup also creates a
    sandbox key. Live keys consume prepaid credit; phone verification may unlock
    promotional credit under graduated-signup rules, but credit is not
    guaranteed at signup.


    Keys are self-validating signed tokens: they work on every PyAI surface the
    instant they are created, no activation or propagation delay. Treat them as
    opaque strings (up to 512 chars) and never parse their contents.


    WebSocket endpoints can't use request headers from a browser, so pass the
    key as a **subprotocol** instead:


    ```

    Sec-WebSocket-Protocol: pyai.v1, pyai-key.pyai_live_...

    ```


    (server-side clients may instead append `?api_key=...` to the URL). Your key
    is authenticated on the upgrade and never reaches the model.


    ## Quickstart, Hear (speech-to-text)


    ```

    curl https://api.pyai.com/v1/audio/transcriptions \
      -H "Authorization: Bearer $PYAI_API_KEY" \
      -F file=@audio.wav -F model=pyai-hear
    # -> { "text": "..." }

    ```


    ## Quickstart, Speak (text-to-speech)


    ```

    curl https://api.pyai.com/v1/audio/speech \
      -H "Authorization: Bearer $PYAI_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"model":"pyai-speak","input":"Hello from PyAI.","voice":"voice_abc"}' \
      --output speech.wav
    ```


    `voice` is a stock voice id from `GET /v1/voices` (the curated prebuilt
    catalog with personas and avatars) or a cloned voice id from
    `/v1/voice/clones`. Omit it to use the platform default voice
    (`stock_dorit_en_us`).


    ## Quickstart, Omni (realtime voice agent)


    Omni is **zero-state, there is nothing to create first.** Open a WebSocket,
    pass your key as a subprotocol, and send the agent's behavior (voice,
    persona, knowledge endpoint) in the first `configure` frame:


    ```

    wss://api.pyai.com/v1/omni?session_label=support&format=pcm16&rate=24000
      Sec-WebSocket-Protocol: pyai.v1, pyai-key.$PYAI_API_KEY
    ```


    The session is authorized by your key's **organization**; `session_label` is
    an **optional, opaque** tag (echoed to your own knowledge endpoint for
    correlation), omit it or use any value. When `session_label` equals a
    **`/v1/agents` profile id**, the engine loads persona, voice, and **greeting
    message** from that profile (turn-0 playback). `format` and `rate` are
    load-bearing on the connect URL (the SDK sets them). Prefix each PCM16 frame
    with byte `0x01`; prefix control JSON with byte `0x03`. **Optional
    convenience:** pre-store config via `POST /v1/agents` (including `greeting`,
    `consent_line`, `recordings_enabled`) and pass its id as `session_label`, or
    send everything inline in the post-handshake `configure` frame. Not required
    to connect.


    For reproducible eval runs, determinism controls (`seed`/`temperature`) ride
    the Omni session's `configure` frame, which the gateway passes through
    unchanged, they are honored once the engine supports them; no platform
    change is required.


    ## Scopes


    | Scope | Grants |

    | --- | --- |

    | `hear:transcribe` | `POST /v1/audio/transcriptions` |

    | `hear:stream` | `GET /v1/audio/transcriptions/stream` (WebSocket) |

    | `hear:configure` | `GET`/`PUT /v1/hear/vocabulary` |

    | `speak:synthesize` | `POST /v1/audio/speech` (Speak) |

    | `speak:clone` | `/v1/voice/clones` (Clone) |

    | `speak:design` | `/v1/voice/design` (Speak) |

    | `omni:session` | `/v1/omni` and `POST /v1/omni/sessions` (mint a browser
    session token) |

    | `omni:read` | `/v1/omni/calls` (Omni post-call records) |

    | `kb:manage` | `/v1/knowledgebases/*` (hosted knowledge bases for Omni
    grounding) |

    | `transcribe:jobs` | `/v1/transcription/jobs` |

    | `trace:configure` | `/v1/trace/config`, `/v1/trace/rule-packs` (Trace
    management) |

    | `trace:read` | `/v1/trace/interactions`, `/violations`, `/findings`,
    `/exposure` (Trace reads) |

    | `recap:configure` | `/v1/recap/config` (Recap management) |

    | `recap:configure` | `/v1/recap/crm-config` (Salesforce field mapping) |

    | `recap:read` | `/v1/recap/calls` (Recap reads and speaker-role
    corrections) |

    | `amd:detect` | `wss …/v1/amd/stream` (AMD realtime detection, Twilio
    drop-in) |

    | `amd:configure` | `/v1/amd/config` (AMD operating-point dial + webhook) |

    | `amd:read` | `/v1/amd/calls` (AMD decision records) |

    | `telephony:manage` | `/v1/telephony/*` (managed numbers) |


    `GET /v1/models`, `GET /v1/voices`, and `GET /v1/me` need no specific scope,
    any active key may call them. Wildcards (`hear:*`, `speak:*`, …, and the
    global `*`) grant every scope in their family.


    ## Canonical endpoints


    One row per product surface, endpoint, auth, required scope, and lifecycle
    status. **live** = generally available; **beta** = available now with
    features or limits that may change; **unavailable** = reserved in the
    contract but not active on the serving route.


    | Product | Endpoint | Auth | Scope | Status |

    | --- | --- | --- | --- | --- |

    | Identity | `GET /v1/me` | Bearer | _any active key_ | live |

    | Models | `GET /v1/models` | Bearer | _any active key_ | live |

    | Voices | `GET /v1/voices`, `GET /v1/voices/{id}` | Bearer | _any active
    key_ | live |

    | Hear (batch) | `POST /v1/audio/transcriptions` | Bearer |
    `hear:transcribe` | live |

    | Hear (vocabulary settings) | `GET`/`PUT /v1/hear/vocabulary` | Bearer |
    `hear:configure` | live |

    | Hear (streaming) | `GET /v1/audio/transcriptions/stream` (WS) |
    Subprotocol | `hear:stream` | live |

    | Cue | `GET /v1/audio/transcriptions/stream` + grounding (WS) | Subprotocol
    | `hear:stream` | unavailable |

    | Hear (async batch) | `POST`/`GET /v1/transcription/jobs` | Bearer |
    `transcribe:jobs` | live |

    | Speak (TTS) | `POST /v1/audio/speech` | Bearer | `speak:synthesize` | live
    |

    | Clone | `GET`/`POST /v1/voice/clones` | Bearer | `speak:clone` | live |

    | Speak (design) | `/v1/voice/design` | Bearer | `speak:design` | live |

    | Omni | `wss …/v1/omni?session_label=` | Subprotocol | `omni:session` |
    live |

    | Agent profiles (optional config) | `/v1/agents`, `/v1/agents/{id}` |
    Bearer | `omni:session` | live |

    | Knowledge Bases (hosted grounding) | `/v1/knowledgebases/*`, `PUT
    /v1/agents/{id}/knowledgebases` | Bearer | `kb:manage` (`omni:session` for
    the binding) | live |

    | Trace (config) | `/v1/trace/config`, `/v1/trace/rule-packs` | Bearer |
    `trace:configure` | beta |

    | Trace (reads) | `/v1/trace/interactions`, `/violations`, `/findings`,
    `/exposure` | Bearer | `trace:read` | beta |

    | Recap (config) | `/v1/recap/config` | Bearer | `recap:configure` | live |

    | Recap (CRM) | `/v1/recap/crm-config` | Bearer | `recap:configure` | live |

    | Integrations (Zapier) | `/v1/integrations/events`,
    `/v1/integrations/zapier/hooks` | Bearer | _any active key_ | live |

    | Recap (reads) | `/v1/recap/calls` | Bearer | `recap:read` | live |

    | Omni call records | `/v1/omni/calls`, `/v1/omni/calls/{id}` | Bearer |
    `omni:read` | live |

    | AMD (stream) | `wss …/v1/amd/stream` (Twilio Media Streams drop-in) |
    TwiML `<Parameter name="api_key">` (from Twilio) or subprotocol
    (server-side) | `amd:detect` | live |

    | AMD (config) | `GET`/`POST /v1/amd/config` | Bearer | `amd:configure` |
    live |

    | AMD (reads) | `GET /v1/amd/calls`, `/v1/amd/calls/{id}` | Bearer |
    `amd:read` | live |

    | Telephony | `/v1/telephony/*` | Bearer | `telephony:manage` | live |

    | Agents (console builder) | `https://console.pyai.com/agents` | Console
    session |, | beta |


    WebSocket surfaces authenticate with the `Sec-WebSocket-Protocol: pyai.v1,
    pyai-key.<API_KEY>` subprotocol pair (or `?api_key=` server-side);
    everything else takes the `Authorization: Bearer` key. Managed-number calls
    return 404 until the PyAI network is enabled for the account.


    ## Rate limits & billing


    Every key has a per-second rate limit (with burst) and a cap on concurrent
    realtime sessions. Exceeding either returns `429` with a `Retry-After`
    header. Usage is metered per minute of audio, transcription minutes (Hear),
    synthesized audio minutes (Speak), and realtime session minutes (Omni), and
    billed against your plan and credits. List prices: Hear $0.001/min (async
    Transcribe $0.0005/min), Speak $0.04/min, Omni $0.05/min including speech
    plus brain, and Agents Live Beta $0.08/min. Managed telephony is separate at
    $0.01/min. English Natural (`en1`) is available on Speak (streaming and
    buffered) and Omni; Omni acknowledges the canonical id and `voice_tier:
    natural`. Hindi uses the Standard-tier voices `hi1`–`hi4` on Omni; the
    former Hindi Natural aliases (`hi5`–`hi8`) are retired and no longer in the
    catalog. Standard and Natural voices are included in their product's base
    rate with no voice-tier add-on. The AMD API bills per **answered** call, the
    first 5,000 answered calls each month are free, then $0.004/answered call
    (no-answers, busies, and failed calls are free; AMD bundled with PyAI
    telephony/Omni is included at no charge). AI products (Hear, Speak, Omni)
    bill **per second by default**, the pulse is applied once to each meter's
    invoice-period total, so many short sessions are summed and rounded a single
    time (never minute-rounded per call), and an empty/failed call bills
    nothing. Coarser pulses are available as an optional enterprise override.
    Managed telephony minutes keep a 1-minute pulse. Per-character Speak billing
    is available on enterprise contracts.
  contact:
    name: PyAI
    url: https://pyai.com
servers:
  - url: https://api.pyai.com
    description: Production
security:
  - apiKey: []
  - xApiKey: []
tags:
  - name: Dub
    description: >-
      Asynchronous audio dubbing into Hindi: submit a recording, poll the job
      and download the output.
  - name: Omni
    description: >-
      The flagship: build an AI voice agent with one WebSocket (`GET /v1/omni`)
      and one `configure` frame, nothing to pre-create. This group also holds
      the optional browser-token mint and the post-call records.
  - name: Knowledge Bases
    description: >-
      Hosted knowledge bases for Omni grounding: create a base, add documents
      (file upload, URL fetch, or pasted text), then bind it to agent profiles
      or set org-wide defaults. Bound bases are retrieved per turn, no
      `kb_endpoint` of your own required.
  - name: Identity
    description: >-
      Introspect the calling key: org/project, env, granted scopes, and
      limits/credit posture. Use it to self-diagnose a 401/403/402.
  - name: Speech To Text (Hear)
    description: Speech-to-text (streaming + batch)
  - name: Text To Speech (Speak)
    description: Text-to-speech, stock voices, and prompt-to-voice design
  - name: Clone
    description: Enroll, list, and delete custom voices from a short reference clip
  - name: Models
    description: Model catalog
  - name: Sandbox
    description: >-
      Zero-friction onboarding for coding agents: mint a free, instant, no-card
      sandbox key with no human steps.
  - name: Startup Program
    description: >-
      PyAI for Startups: $20k to $100k in PyAI credit for early-stage voice
      teams. Public application endpoint; review and activation happen out of
      band.
  - name: Cast
    description: >-
      Auto-directed, expressive multi-line voiceover projects and asynchronous
      renders.
  - name: Transcription Jobs
    description: Async batch transcription
  - name: Agents
    description: >-
      Agent profiles used by the live Agents Beta console and available directly
      through the API. Store Omni session config (persona, greeting, voice,
      conversation knobs) and reference it by id instead of sending a full
      `configure` frame each call. Profiles remain optional for direct
      `/v1/omni` integrations.
  - name: Trace
    description: >-
      Compliance & guardrails: per-agent config, rule packs, and the exposure /
      violations / interaction-evidence read views
  - name: AMD
    description: >-
      Answering-machine detection: know who or what answered a call (human,
      voicemail, IVR, iPhone/Google screening, dead number), with the reason it
      decided. Twilio Media Streams drop-in over `wss …/v1/amd/stream`; one
      operating-point dial; billed per answered call.
  - name: Telephony
    description: >-
      Managed phone numbers: search, provision, route to an agent, and release.
      Call minutes bill on telephony.minutes ($0.01/min).
  - name: WhatsApp
    description: >-
      WhatsApp Business Calling: register a WhatsApp Business number, enable
      calling, and let an Omni agent answer (and, with the user's permission,
      place) WhatsApp voice calls. Requires the `telephony:manage` scope.
  - name: Call Integrations
    description: >-
      Signed provider webhooks that import completed calls into Hear, Recap, and
      offline Trace.
paths:
  /v1/audio/transcriptions/stream:
    get:
      tags:
        - Speech To Text (Hear)
      summary: Stream transcription (WebSocket)
      description: >-
        Upgrade to a WebSocket for streaming speech-to-text with eager partials
        (PyAI Hear first partial measured at about 200 ms in-region. It is
        revisable and not an SLA). Requires the `hear:stream` scope. The only
        frame protocol is `pyai-hear-v1`, the published frame catalog below. Cue
        grounding configuration is reserved but is not active on the serving
        route.


        **Auth:** browsers can't set `Authorization` on a WebSocket, so send the
        key as a subprotocol, together with the `pyai.v1` marker:
        `Sec-WebSocket-Protocol: pyai.v1, pyai-key.<API_KEY>` (server clients
        may use `?api_key=` instead). Offer both values: PyAI echoes only the
        marker and never reflects your key, and a 101 can only select a value
        the client offered. The key is validated and swapped for the internal
        upstream credential on the upgrade.


        Set `vocabulary` only when the session has known names, brands,
        products, or other distinctive terms. This biases known vocabulary only
        when supplied for the session or enabled through the stored
        `hear_stream` profile. Request terms take priority and the effective
        list is fixed when the session opens.


        **Client -> server:** stream mono little-endian PCM16 binary audio
        frames continuously at `sample_rate=8000` or `sample_rate=16000`
        (default). The service converts 8 kHz input before recognition and
        endpointing. Other formats are rejected with `400
        unsupported_audio_format` before the WebSocket opens. Send a JSON
        `{"type":"commit"}` text frame to force-finalize the current utterance
        (e.g. when your VAD detects end-of-turn). Closing the socket also
        flushes a final for any buffered audio.


        Set `endpointing_ms` on the connect URL or send
        `{"type":"config","endpointing_ms":800}` mid-session. It is the minimum
        trailing-pause length before an utterance may end. Turn detection may
        wait longer; an utterance always completes within `max(endpointing_ms,
        1500 ms)` of streamed silence. Keep sending audio frames, including
        silence, while the caller pauses. Pausing the stream pauses the
        frame-driven clock.


        **Server -> client (JSON text frames):**


        | `type` | When | Payload |

        | --- | --- | --- |

        | `config_ack` | after connect-time or mid-session endpointing config |
        `{endpointing_ms, effective_floor_ms, effective_ceiling_ms,
        score_interval_ms, warnings}`, the applied values and any validation
        warnings |

        | `partial` | every eager tick | `{text, stable_text, active_text,
        utterance_id, t_ms}`, the live hypothesis for that `utterance_id` |

        | `partial_stable` | when a prefix locks in | `{text, utterance_id,
        t_ms}`, the portion the recognizer no longer expects to revise |

        | `speech_final` | on endpoint/commit | `{text, utterance_id, t_ms,
        audio_ms, endpoint_reason}`, stable, end of an utterance |

        | `final` | follows `speech_final` | `{text, utterance_id, t_ms,
        audio_ms, endpoint_reason}`, corrected full-context transcript |

        | `usage` | just before a graceful close | `{product, meter,
        audio_seconds, minutes}`, the session's billed active-audio, so you can
        reconcile realtime spend in-band (a realtime WS carries no
        `x-pyai-units` response header). Best-effort; absent if the session had
        no billable audio or closed abnormally. |

        | `error` | on fault | `{code, message}` |


        `t_ms` is the audio-timeline position of the hypothesis; `audio_ms` is
        the utterance's active-speech length (the billed signal); `utterance_id`
        groups partials/finals for one utterance. Assert that
        `config_ack.warnings` is empty instead of inferring that a setting
        applied. Warning reasons include `clamped_to_range`, `not_a_number`, and
        `unknown_config_field`; invalid config leaves the session open.
        `endpoint_reason` reports why automatic endpointing fired, including
        `peak_te_early` for the high-confidence path and `silence_backstop` for
        the bounded fallback. Log it when tuning conversational turns. The Cue
        grounding config frame and grounding result fields are reserved but
        currently have no effect on the serving stream.


        **Close codes:** `1000` normal · `1008` auth/policy (bad key, scope,
        revoked token) · `1011` engine error · `4429` over concurrency cap.


        **Billing:** metered active audio at the Hear rate ($0.001/min), speech
        time derived from transcript timing rather than connection wall-clock.
      operationId: openTranscriptionStream
      parameters:
        - name: protocol
          in: query
          required: false
          schema:
            type: string
            const: pyai-hear-v1
            default: pyai-hear-v1
          description: >-
            Canonical frame protocol. Omit to use the same `pyai-hear-v1`
            default.
        - name: model
          in: query
          required: false
          schema:
            type: string
            default: pyai-hear
          description: Streaming STT model.
        - name: language
          in: query
          required: false
          schema:
            type: string
            enum:
              - auto
              - en
              - es
              - fr
              - de
              - hi
              - it
              - pt
              - nl
            default: auto
          description: >-
            Omit or use auto to detect the spoken language automatically. An
            explicit ISO-639-1 code pins recognition for the session.
            Unsupported explicit values are rejected at the upgrade with `400
            unsupported_language`.
        - name: sample_rate
          in: query
          required: false
          schema:
            type: integer
            enum:
              - 8000
              - 16000
            default: 16000
          description: >-
            Input PCM sample rate in Hz. Send 8000 for 8 kHz telephony PCM; the
            service converts it before recognition and endpointing.
        - name: encoding
          in: query
          required: false
          schema:
            type: string
            enum:
              - pcm16
            default: pcm16
          description: >-
            Mono little-endian 16-bit PCM. Compressed audio is not supported on
            this stream.
        - name: interim_results
          in: query
          required: false
          schema:
            type: boolean
            default: true
          description: Emit eager partial hypotheses.
        - name: numerals
          in: query
          required: false
          schema:
            type: boolean
          description: >-
            Tri-state inverse-text normalization for English **final**
            transcripts (never interim partials). `true` renders spoken numbers
            as digits (phones, currency, dates, ordinals). `false` keeps those
            spans in spoken form. Omitted keeps the live engine default: number
            formatting is ON for finals. Independent of `smart_format`.
        - name: smart_format
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: >-
            Opt-in English punctuation and sentence capitalization on **final**
            transcripts only. Interim partials are never formatted. May change
            only case and punctuation; any failure returns the unformatted
            final. Default `false`. Independent of `numerals`. Non-English
            requests are unchanged.
        - name: dictation
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: >-
            Opt-in spoken punctuation commands on English **final** transcripts
            only: `period`, `comma`, `new paragraph`, and `question mark`.
            Separate from `smart_format` and off by default. Interim partials
            are never rewritten.
        - name: drop_fillers
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: >-
            Opt-in stripping of filled pauses (`um`, `uh`, `umm`, `uhh`, `er`)
            on English **final** transcripts. Off by default. Do not enable on
            legal or compliance audio by default. Interim partials are never
            rewritten.
        - name: vocabulary
          in: query
          required: false
          schema:
            type: string
            maxLength: 2048
          description: >-
            Optional per-session terms for known names, brands, products, and
            other distinctive phrases. PyAI trims entries, removes
            case-insensitive duplicates, and keeps the first spelling and order.
            Entries shorter than 4 characters, longer than 64 characters, longer
            than 5 words, or made only of common words are ignored. At most 5
            effective terms are used. When stored vocabulary is enabled for
            `hear_stream`, request terms come first and stored suggestions fill
            any remaining slots. The effective list is fixed for the session and
            does not select transcription language. Send a comma-separated list
            or a JSON array string.
        - name: seed
          in: query
          required: false
          schema:
            type: integer
          description: >-
            Optional determinism seed for reproducible eval runs. Forwarded to
            the engine and honored once the engine supports it; no effect when
            omitted.
        - name: temperature
          in: query
          required: false
          schema:
            type: number
          description: >-
            Optional sampling temperature for reproducible eval runs. Forwarded
            to the engine and honored once the engine supports it; no effect
            when omitted.
        - name: endpointing_ms
          in: query
          required: false
          schema:
            type: integer
            minimum: 50
            maximum: 5000
          description: >-
            Minimum trailing-pause length in milliseconds (50-5000, clamped)
            before an utterance may end. Turn detection may wait longer; an
            utterance always completes within `max(endpointing_ms, 1500 ms)` of
            streamed silence. The same setting can be changed mid-session with
            `{"type":"config","endpointing_ms":800}`. Both paths emit
            `config_ack`; assert that `warnings` is empty. A non-empty
            `warnings` means a value was not applied verbatim, but the session
            remains open. Timing counts only audio you stream, including
            silence, so pausing the stream pauses the clock. `{"type":"commit"}`
            still forces immediate end-of-turn on the STT socket.
        - name: call_id
          in: query
          required: false
          schema:
            type: string
          description: >-
            Stable call identifier used for the post-call Recap when Recap is
            enabled. Omit to use the stream session id.
        - name: pack_id
          in: query
          required: false
          schema:
            type: string
            pattern: ^[a-z0-9_]+$
          description: Optional Recap pack for the post-call record.
        - name: call_direction
          in: query
          required: false
          schema:
            type: string
            enum:
              - inbound
              - outbound
          description: Optional call direction attached to the post-call Recap.
      responses:
        '101':
          description: Switching Protocols, the streaming transcription WebSocket is open.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
components:
  responses:
    Unauthorized:
      description: 'Missing or invalid API key (`code: unauthorized`)'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    PaymentRequired:
      description: >-
        Billing gate: org out of prepaid credit, per-key budget hit, or plan
        quota exhausted (`code: credit_exhausted | key_budget_exceeded |
        insufficient_quota`). Do not retry; add credit or raise the limit. A
        brand-new key may see this on its first call until the account is
        funded.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: >-
        Key lacks the required scope or the origin is not allow-listed (`code:
        forbidden | origin_not_allowed`)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    RateLimited:
      description: >-
        Too many requests or too many concurrent realtime sessions; see
        Retry-After header (`code: rate_limit_exceeded |
        concurrency_limit_exceeded | daily_cap_exceeded`)
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds to wait.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  schemas:
    Error:
      type: object
      description: >-
        OpenAI-compatible error envelope returned by the gateway data plane
        (401/402/403/429). Control-plane request/resource errors use Problem
        (application/problem+json) instead.
      required:
        - error
      properties:
        error:
          type: object
          required:
            - message
          properties:
            message:
              type: string
              description: Human-readable explanation.
            type:
              type: string
              description: Error category, e.g. rate_limit_error.
            code:
              $ref: '#/components/schemas/ErrorCode'
            param:
              type: string
              nullable: true
              description: Offending parameter when applicable, else null.
    ErrorCode:
      type: string
      description: >-
        Stable, machine-readable error code. Branch on this rather than the
        human `message`.
      enum:
        - invalid_request_error
        - invalid_session_label
        - unauthorized
        - forbidden
        - origin_not_allowed
        - credit_exhausted
        - key_budget_exceeded
        - insufficient_quota
        - rate_limit_exceeded
        - concurrency_limit_exceeded
        - daily_cap_exceeded
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: 'Use `Authorization: Bearer pyai_live_...` (or `pyai_test_...`).'
    xApiKey:
      type: apiKey
      in: header
      name: x-api-key
      description: >-
        Header alias for bearer auth on HTTP endpoints. WebSocket auth uses the
        subprotocol pair `pyai.v1, pyai-key.<API_KEY>`.

````