Synchronous Text to Speech v3
Generate audio synchronously or create recoverable asynchronous jobs with the provider-neutral HTTP v3 contract.
HTTP TTS v3 is the recommended contract for new integrations. The platform owns provider routing; clients use only public voiceId, modelId, and generic controls.
The endpoint, authentication, and response structure stay stable when you change models. You must still confirm that the voice's modelIds contains the target modelId, because supported controls, text limits, and output formats vary by model.
Base URL
https://fishaudio.org/api/open/v3Every endpoint except capabilities and the OpenAPI document requires a server-side Bearer API key. Never embed the key in browser code, mobile binaries, or public repositories.
Choose synchronous or asynchronous delivery
| Use case | Endpoint | Why |
|---|---|---|
| Short text and the caller can keep the HTTP request open | POST /speech/tts | Returns audio bytes directly |
| Long text, batches, or durable result lookup | POST /speech/tts/jobs | Returns a job ID that can be polled and downloaded again |
| The output must be recoverable after a lost response | Asynchronous Jobs | The synchronous endpoint does not replay completed audio |
Step 1: discover model capabilities
Capabilities is public. Read it before constructing a generation request instead of hard-coding model behavior.
curl "https://fishaudio.org/api/open/v3/speech/tts/capabilities"{
"contractVersion": "v3",
"route": "/api/open/v3/speech/tts",
"models": [
{
"id": "fishaudio-s21pro-flash",
"available": true,
"maxTextLength": 10000,
"outputFormats": ["mp3", "wav", "ogg"],
"controls": {
"speed": true,
"volume": true,
"stability": true,
"similarity": true
},
"supportedOperations": {
"synchronous": true,
"asynchronous": true
}
}
]
}Choose only a model with available=true. Treat its maxTextLength, outputFormats, and controls as authoritative.
Step 2: find a compatible voice
curl "https://fishaudio.org/api/open/v3/voices?page=1&pageSize=20&includePersonal=false" \
-H "Authorization: Bearer $FISHAUDIO_API_KEY"{
"total": 1,
"page": 1,
"pageSize": 20,
"totalPages": 1,
"items": [
{
"voiceId": "00a1b221-6137-4b73-ad62-b0cbce134167",
"name": "System test voice",
"isPersonal": false,
"primaryLanguage": "en",
"languages": ["en"],
"modelIds": ["fishaudio-s21pro-flash"]
}
],
"requestId": "req_xxx"
}modelIds is the list of public models currently supported by that voice. Generation requires a combination where the model is available and the voice supports it. Use GET /voices/{voiceId} to inspect one voice.
Step 3: generate synchronously
POST /api/open/v3/speech/ttscurl "https://fishaudio.org/api/open/v3/speech/tts" \
-H "Authorization: Bearer $FISHAUDIO_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Request-Id: trace-123-segment-1" \
-H "Idempotency-Key: tts-order-123-segment-1" \
-d '{
"text": "Hello from HTTP TTS v3.",
"voiceId": "00a1b221-6137-4b73-ad62-b0cbce134167",
"modelId": "fishaudio-s21pro-flash",
"format": "mp3",
"speed": 1,
"volume": 0,
"stability": 1,
"similarity": 1,
"language": "en",
"textNormalization": true
}' \
--output speech.mp3Request fields
| Field | Type | Required | Constraints and defaults |
|---|---|---|---|
text | string | Yes | 1–10,000 characters and no more than the model's maxTextLength |
voiceId | string | Yes | Returned by /voices and must support the selected modelId |
modelId | string | Yes | Public model ID returned by capabilities |
format | string | No | mp3, wav, or ogg; default mp3; must be in the model's outputFormats |
speed | number | No | 0.5–2; default 1 |
volume | number | No | -20–20; default 0 |
pitch | number | No | -12–12; send only when capabilities declares support |
stability | number | No | 0.5–1.5; send only when capabilities declares support |
similarity | number | No | 0.5–1.5; send only when capabilities declares support |
language | string | No | Language hint, 1–64 characters |
emotion | string | No | Global emotion ID, 1–64 characters |
instruction | string | No | Style, dialect, role, or emotion instruction, up to 1,600 characters |
textNormalization | boolean | No | Enables structured-text pronunciation normalization |
The request contract is strict. provider, provider API keys, provider-native model fields, and other unknown properties return 400. Valid generic controls unsupported by the selected model are ignored and listed in X-OpenAPI-Ignored-Parameters.
Successful response
HTTP/1.1 200 OK
Content-Type: audio/mpeg
X-Request-Id: trace-123-segment-1
X-OpenAPI-Quota-Remaining: 99994
X-OpenAPI-Credits-Used: 6
X-OpenAPI-Ignored-Parameters: pitch
<binary audio data>Check the HTTP status first, then branch on Content-Type. A successful response contains audio bytes; an error contains JSON. X-OpenAPI-Ignored-Parameters is present only when a control was ignored.
Asynchronous jobs
Use Jobs for long text, batches, or workflows that require recoverable output. The create body is the same as synchronous v3.
curl "https://fishaudio.org/api/open/v3/speech/tts/jobs" \
-H "Authorization: Bearer $FISHAUDIO_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Request-Id: trace-job-001" \
-H "Idempotency-Key: tts-job-001" \
-d '{
"text": "Text that needs durable generation and download.",
"voiceId": "00a1b221-6137-4b73-ad62-b0cbce134167",
"modelId": "fishaudio-s21pro-flash",
"format": "mp3"
}'Creation returns 202 Accepted. Location points to the job status resource and Retry-After provides the suggested polling delay. Store task.taskId immediately:
{
"task": {
"taskId": "task_123",
"status": "pending",
"voiceId": "00a1b221-6137-4b73-ad62-b0cbce134167",
"modelId": "fishaudio-s21pro-flash"
},
"requestId": "trace-job-001",
"quotaRemaining": 99994,
"creditsUsed": 6,
"ignoredParameters": []
}GET /speech/tts/jobs/{jobId}
GET /speech/tts/jobs?page=1&limit=20&status=success
GET /speech/tts/jobs/{jobId}/audio?download=1
GET /speech/tts/jobs/{jobId}/segments/{segmentIndex}
Authorization: Bearer FISHAUDIO_API_KEYpending and processing are non-terminal. Only success, partial_fail, and fail are terminal. Keep Bearer authentication when downloading audio and allow the client to follow redirects.
Error format and handling
{
"code": "ERR_REQUEST_ID_CONFLICT",
"message": "Request id was reused with a different request",
"requestId": "tts-order-123-segment-1"
}| Status | Common cause | Client action |
|---|---|---|
400 | Invalid JSON, field, model, voice, or format | Fix the request; do not retry unchanged |
401 | Missing or invalid API key | Replace the server-side credential |
402 | Insufficient API quota | Stop the workflow and surface a billing state |
404 | Voice or asynchronous job not found | Check the ID and owning account |
409 | Request active, completed, refunded, or reused with different input | Branch on code; do not blindly use a new ID |
413 | HTTP request body exceeds the limit | Reduce the request; use Jobs for long text |
429 | Rate limit exceeded | Honor Retry-After and use exponential backoff |
500 | Platform or provider generation failed | Preserve requestId; confirm settlement before retrying |
503 | The selected model is currently unavailable | Refresh capabilities and select an available model |
Idempotency, billing, and retries
X-Request-Id is a trace id and is echoed in the response; the server generates it when omitted. Idempotency-Key is the business idempotency key and is at most 128 characters. Create one stable idempotency key per logical generation and reuse it only for an identical retry. Never send concurrent requests with the same key.
Existing clients may temporarily continue using X-Request-Id as a compatibility idempotency key. New clients must send the two headers separately. When Idempotency-Key is omitted, the request still runs, but the client cannot rely on an idempotent retry for that call.
- Synchronous generation has at-most-once semantics. Reusing a completed idempotency key returns
409 ERR_REQUEST_ALREADY_COMPLETED; it does not replay audio and does not charge again. - Asynchronous creation replays the accepted task for an identical request, so it remains queryable and is not billed twice.
- Reusing an idempotency key with different parameters returns
409 ERR_REQUEST_ID_CONFLICT. - A client timeout does not mean the server stopped. Use Jobs whenever output must be recovered after a lost response.
- Validation, authentication, and quota failures are not charged. Generation or settlement failures enter the refund path.
Machine-readable contract and migration
The complete OpenAPI 3.1 document is public:
GET /api/open/v3/openapi.jsonOpenAPI defines stable fields and response shapes; capabilities describes live model availability. Read both together. Existing v1 and v2 clients remain compatible, while new integrations should use v3. See the migration guide for field and path changes.