https://api.fish.audio/compat follows the contract on this
page.
How parameters are handled
Unsupported options return an explicit error — never a silent no-op. Every parameter is either mapped to the native equivalent (stability →
temperature, speed → speech rate, bitrates snap to a tier), ignored when
it doesn’t affect synthesis (similarity_boost, style, seed), or
refused with a 4xx in the protocol’s own error format when honoring it would
change the response content or structure —
full list. Unknown JSON fields are ignored,
matching OpenAI behavior.
What each protocol supports
Two protocols: OpenAI at/v1 (equivalently /api/v1 and /openai/v1 — the
OpenAI, OpenRouter, and Groq SDKs all speak it), and ElevenLabs at
/elevenlabs.
Voices are always Fish voice IDs — empty or
default selects the model’s
default voice, and anything that isn’t a real voice ID, preset names (nova,
echo, Rachel, …) included, is a 400. One exception: lowercase alloy
matches a real Fish voice and synthesizes — an unrelated voice, not OpenAI’s
Alloy (Alloy is refused). Where to find voice IDs:
Voices.
Audio formats
Requesting a sample rate resamples the audio; a rate the codec doesn’t take is
a 400, including all of 22.05 kHz (
mp3_22050_32, pcm_22050, …). On REST,
OpenAI pcm16 defaults to 24 kHz and pcm to 44.1 kHz; an explicit
sample_rate overrides that.
μ-law and A-law (ulaw_*, alaw_*, audio/pcmu, audio/pcma) are refused
everywhere. How formats are written on the Realtime socket, and when rates
reset, is in the
Realtime protocol reference.
Bitrate snapping
MP3 is encoded at 64, 128, or 192 kbps. An out-of-tier request snaps up to the nearest tier (mp3_32000_32 → 64 kbps at 32 kHz); anything above 192
kbps is a 400. To pick the tier exactly, set it as a
Fish-native parameter — a non-tier value
there is refused too. Opus bitrate is automatic: requested values are accepted
and ignored.
What is explicitly refused
Each returns a 4xx in the protocol’s own error format — or, on WebSockets, an in-band error event:
On OpenAI transcription,
languages, keywords, prompt, and temperature
are in the ignored tier.
Booleans count as requested only when true: false, "False", 0, none,
and off are treated as unset, so SDK defaults never trigger a refusal.
Placeholder values
Protocol fields with no Fish counterpart are filled with neutral constants — don’t build logic on them:- OpenAI
verbose_json: the Whisper-engine internals (avg_logprob,no_speech_prob,compression_ratio,temperature,seek,tokens) are neutral constants. - ElevenLabs STT
language_probabilityis always1.0, andwords[].logprobis always0. Neither is a confidence score. speaker_idis alwaysnull; requesting diarization returns an explicit error instead.languagein responses echoes your request — it is never a detection result. Send none and you getlanguage: ""alongsidelanguage_probability: 1.0.
Reaching Fish-native parameters
The full native TTS parameter set —temperature, top_p, chunk_length,
latency, normalize, prosody, reference audio — is reachable on
synthesis endpoints, in two ways. One exception: reference audio is not
available on the timestamped ElevenLabs endpoints (…/with-timestamps,
…/stream/with-timestamps, the stream-input WebSocket) — a 400 on the REST
variants, an in-band error frame at flush time on the WebSocket.
provider.options in the request body
On OpenAI-protocol speech and chat-synthesis requests, a
provider.options.fish-audio block sets the corresponding native TTS fields:
Code examples (Python · Node · curl)
Code examples (Python · Node · curl)
references — base64 audio plus its transcript — enable zero-shot
voice cloning through any OpenAI SDK.
X-Fish-* request headers
On every synthesis endpoint — OpenAI POST /v1/audio/speech and chat
synthesis, the ElevenLabs endpoints, and OpenAI Realtime synthesis sessions,
including their WebSocket handshakes — an X-Fish-<Param> header sets the
corresponding native TTS field:
Code examples (Python · Node · curl)
Code examples (Python · Node · curl)
provider.options.fish-audio or X-Fish-*. That includes a
Realtime socket whose handshake carried X-Fish-* headers — the
input_audio_buffer.commit there is refused with
unsupported_native_options, so open a second socket without the headers to
transcribe. On the native /v1/tts/live WebSocket, put native fields in
each start.request payload rather than in handshake headers.
Synthesis on the Compatible APIs defaults to
latency: "low"
(streaming-optimized). For maximum quality on non-interactive workloads, set
X-Fish-Latency: normal — your explicit value always wins.Limits and operational contract
The 32 MiB frame limit bounds one WebSocket message, not a session. On a
Realtime socket, item-borne audio and
input_audio_buffer bytes share the
same 100 MiB budget.
- Billing — like native usage, against your key. Billable requests are never retried automatically; retrying is your call.
- Rate limits — the Compatible APIs add none of their own today; a 429
means you reached your account’s concurrency limit, the same one native
calls count against. Every 429 carries a
Retry-Afterheader. - Truncation is an error — a stream that fails mid-response returns an error frame or 502, never a truncated 200.
- Every synthesis or transcription response carries an
X-Generation-Idheader — include it in support requests. - During service incidents, requests may return 503; back off exponentially.
Known client-side limitations
- OpenAI Realtime: the endpoint speaks the GA event names
(
response.output_audio.*). Currentopenaipackages emit these; very old beta-era clients expectingresponse.audio.*will not match. - ElevenLabs Python
convert_realtime: passmodel_idandvoice_settingsexplicitly — left to the SDK’sOMITsentinel,model_idreaches the handshake URL as the literalmodel_id=Ellipsis(a 400) andvoice_settingsraisesAttributeErroron the first frame. The SDK also force-upgradeshttp://base URLs towss://.

