FlowSpeech Text-to-Speech API
Turn text into speech with a step-by-step guide and ready-to-run examples.
Use the FlowSpeech API to turn text into speech in your own application, with one voice or multiple speakers.
Generation happens in the background: send your text → get a job ID → check until it is ready → download the audio. You do not need to specify a model.
Before you start
- Have an active paid Basic, Pro or Scale subscription. Monthly and annual plans are supported; trials do not include API access.
- Create a key in API settings and copy it immediately. The complete key is shown only once.
- Call the API from your server or a local terminal. Keep the key out of browser code and public repositories. Browser cross-origin requests are not supported.
The API and website share your account's credits. Creating a job sets aside credits; a successful recording is charged once. Failed or cancelled jobs release those credits.
Generate your first audio
This walkthrough uses Bash or Zsh with curl and Python 3, on macOS, Linux or WSL. To automate all the steps, use the complete Node.js or Python example.
1. Set your key and request ID
Replace YOUR_API_KEY with the key you just created:
export FLOWSPEECH_API_KEY='YOUR_API_KEY'
export FLOWSPEECH_REQUEST_ID="$(python3 -c 'import uuid; print(uuid.uuid4())')"The request ID is a unique label that prevents a retry from creating a duplicate recording. Generate a new ID for each new recording. Keep the same ID and request body when retrying an interrupted request. Save this ID if you may need to resume later.
2. Send your text
curl --include --silent --show-error https://flowspeech.io/api/v1/tts/jobs \
-H "Authorization: Bearer $FLOWSPEECH_API_KEY" \
-H 'Content-Type: application/json' \
-H "Idempotency-Key: $FLOWSPEECH_REQUEST_ID" \
-d '{"input":{"mode":"single","text":"Hello world.","voiceName":"Kore"}}'The API commands in this guide use --include to show the HTTP status and response headers above the JSON body. For example, HTTP/2 202 means the job was accepted, and Retry-After: 5 means wait at least 5 seconds before the next request. Header names may appear in lowercase. When automating JSON parsing, save headers separately with --dump-header headers.txt instead of --include.
A new job normally returns 202 Accepted. Here are the key fields from the JSON body of an example response; your IDs will be different:
{
"id": "b698824b-de05-486d-8833-05d5c4522a39",
"requestId": "51382b81-9d36-49a5-994e-9b67686c6bd1",
"status": "queued",
"usage": {
"billableCharacters": 12,
"reservedCredits": 12,
"chargedCredits": 0
}
}Copy id, the job ID returned by FlowSpeech, for the next step. requestId is the label you supplied in step 1. If you receive an error instead, follow Troubleshooting before continuing.
3. Check whether it is ready
Replace PASTE_JOB_ID_HERE with the id from your response:
FLOWSPEECH_JOB_ID='PASTE_JOB_ID_HERE'
curl --include --silent --show-error "https://flowspeech.io/api/v1/tts/jobs/$FLOWSPEECH_JOB_ID" \
-H "Authorization: Bearer $FLOWSPEECH_API_KEY"Run the query again after 5 seconds while the job is in progress. If a response includes Retry-After, wait at least that many seconds.
status | What to do |
|---|---|
queued, running, merging, settling | Still in progress. Wait and check again. |
success | Download the audio from result.url. |
failed, cancelled | Stop polling. Check error.code; the reserved credits are released. |
When the job succeeds, the response includes these fields. The URL below is a placeholder; use the URL in your actual response:
{
"id": "b698824b-de05-486d-8833-05d5c4522a39",
"status": "success",
"result": {
"url": "https://audio.example.com/your-recording.wav",
"mimeType": "audio/wav",
"sampleRate": 24000,
"channels": 1,
"bitsPerSample": 16,
"access": "link"
},
"usage": {
"billableCharacters": 12,
"reservedCredits": 0,
"chargedCredits": 12
}
}An HTTP 200 only means the status query worked. Always check the job's status to know whether generation succeeded.
4. Download your recording
Replace PASTE_RESULT_URL_HERE with result.url:
FLOWSPEECH_AUDIO_URL='PASTE_RESULT_URL_HERE'
curl --fail --location --show-error "$FLOWSPEECH_AUDIO_URL" --output speech.wavYou now have speech.wav: WAV audio at 24 kHz, mono, 16 bit. Do not send your API key when downloading audio. Anyone with the result link can download it, so only share it with people who should have access.
If the download fails, retry the download using the same URL. You do not need to create another recording. A previously deleted recording can have status: "success" with result: null and resultDeleted: true; see Audio access and retention.
Complete examples
These scripts submit the text, wait for the result and save speech.wav. They use only built-in libraries and retry temporary request failures using the same request ID. Choose one example.
Set FLOWSPEECH_API_KEY as shown above. With FLOWSPEECH_REQUEST_ID still set, the unchanged example resumes the same “Hello world.” job. Without it, each script run creates a new request ID and prints it for recovery. Save that ID before closing the terminal. Changing the text or voice requires a new ID.
Node.js 20 or newer
Download the Node.js example, or run:
curl --fail --location --show-error https://flowspeech.io/examples/public-api/generate.mjs --output generate.mjs
node generate.mjsPython 3.9 or newer
Download the Python example, or run:
curl --fail --location --show-error https://flowspeech.io/examples/public-api/generate.py --output generate.py
python3 generate.pyTo resume after an interruption, set FLOWSPEECH_REQUEST_ID to the ID the script printed, then rerun the same script with unchanged text and voice. For example:
export FLOWSPEECH_REQUEST_ID='PASTE_PREVIOUS_REQUEST_ID_HERE'
node generate.mjsFor Python, use python3 generate.py instead. For a new recording, run unset FLOWSPEECH_REQUEST_ID before starting the script so it generates a new ID. Stopping either script stops polling; it does not cancel the job on the server.
Choose a voice or use multiple speakers
Choose a voice
Kore is a firm, focused voice; Puck is upbeat and playful. To see all supported voices:
curl --include --silent --show-error https://flowspeech.io/api/v1/voices \
-H "Authorization: Bearer $FLOWSPEECH_API_KEY"The response contains a voices list with name, description and gender for each voice. Copy a name exactly, including capitalization, into voiceName.
Use multiple speakers
Replace the single-speaker request body with the following JSON, and use a new request ID. The submit, check and download steps stay the same:
{
"input": {
"mode": "multi",
"segments": [
{ "speaker": "Alice", "text": "Welcome!", "voiceName": "Kore" },
{ "speaker": "Bob", "text": "Thanks for having me.", "voiceName": "Puck" }
]
}
}speaker is a label you choose for a character, such as Alice. voiceName selects the actual voice that speaks. Segments are read in the order you provide them; use the same label and voice for each turn by the same character.
Add pauses
Use [pause] for a one-second pause or [pause:2s] for a two-second pause inside text. For example: Welcome.[pause:2s]Let's begin. Pause tags are not spoken or charged.
Request parameters
Send JSON to POST https://flowspeech.io/api/v1/tts/jobs with these headers:
| Header | Value |
|---|---|
Authorization | Bearer YOUR_API_KEY. Required on every API request. |
Content-Type | application/json. Required when creating a job. |
Idempotency-Key | A UUID that you generate for this recording. Required when creating a job. Reuse it for retries of that same request. |
| Field | When required | Meaning |
|---|---|---|
input.mode | Always | single for one voice, or multi for a sequence of speaker turns. |
input.text | Single voice | The words to speak. Must contain spoken text. |
input.voiceName | Single voice | A supported voice name, such as Kore. Case-sensitive. |
input.segments | Multiple speakers | A nonempty list of turns, in speaking order. |
input.segments[].speaker | Every turn | Your character label, up to 100 characters. |
input.segments[].text | Every turn | The words spoken in that turn. |
input.segments[].voiceName | Every turn | The supported voice name for that turn. |
Only send fields for the selected mode. Unknown fields and unsupported model values are rejected. Browser cookies and Figma login tokens do not authenticate this API.
Responses also include progress, timestamps and deadlineAt. The deadline is two hours after the job is accepted, including queue time; it is not an audio download expiry. Status queries do not return your raw script or a full credit balance.
Credits and limits
Check your available credits
curl --include --silent --show-error https://flowspeech.io/api/v1/quota \
-H "Authorization: Bearer $FLOWSPEECH_API_KEY"The response includes quota, your current API subscription entitlement (or null) and configured limits. Use quota.available to decide whether you can start new work.
| Credit field | Meaning |
|---|---|
quota.remaining | Unspent credits from currently valid credit grants, including credits set aside for unfinished jobs. |
quota.reserved | Credits set aside for unfinished jobs from those grants. |
quota.available | Credits you can use now: remaining - reserved. |
usage.billableCharacters | The billable character count for one job. |
usage.reservedCredits | Credits currently set aside for that job. |
usage.chargedCredits | The final charge for a successful job. |
One billable character costs one credit. Automatic generation retries do not add customer charges. For example, Hello world. uses 12 credits, including the space and period.
How characters, subscriptions and credit reservations are counted
Characters are counted from the parsed speech transcript using UTF-16 code units: Hi costs 2, 你好 costs 2 and 😀 costs 2. Parsed style instructions, speaker labels and pauses are not spoken or charged. Spaces and punctuation retained in the transcript count. For example, [happy]Hi[pause]again has 7 billable characters. The server's usage.billableCharacters is authoritative.
API and website generation share your account's valid credits. A job reserves credits when accepted and is charged once a complete recording has been stored. Failed or cancelled jobs release the reservation. Credits reserved before their grant expires remain valid for that job, even though the expired grant disappears from the balance snapshot.
Trials and expired, paused or refunded subscriptions cannot create jobs. Cancelling renewal retains access until the paid subscription ends. Accepted jobs can finish after ordinary subscription expiry; refunds cancel unfinished jobs using refunded credits. An unexpired, unrevoked key can still read or cancel existing jobs after subscription expiry. Revoking a key prevents access with that key but does not cancel accepted jobs.
Request and generation limits
Limits apply to the account, across all its keys. The initial defaults are:
| Limit | Default |
|---|---|
| Create requests | 6 per minute |
| Read requests, including status, voices and quota | 60 per minute |
| Cancellation requests | 60 per minute |
| Unfinished API jobs | 2 at a time |
| Active, unexpired API keys | 5 |
| Input text | 100,000 UTF-16 code units across all text or turns |
| Speaker turns | 1,000 per request |
| JSON body | 1 MiB |
| Pauses | Up to 30 seconds each and 10 minutes total |
Polling one or two jobs every 5 seconds fits within the default read limit. If you receive 429, wait for the duration in Retry-After before retrying.
Additional limits for frequent or large requests
Five cancellations within one hour trigger a cooldown on new jobs. A request may produce at most 2,000 planned speech/pause segments. The shared queue, generation concurrency and provider request rate also have limits; throughput and completion times are not guaranteed.
Generation-attempt budgets initially allow 400,000 input characters per account and 4,000,000 across the API in a rolling 24-hour period. Attempts include automatic retries, failures and cancelled work. These service-capacity budgets are separate from billable credits: releasing a credit reservation does not restore an attempt budget. A job that exhausts its attempt budget fails with TTS_API_BUDGET_EXCEEDED and releases its reserved credits.
Troubleshooting
An API error looks like this:
{
"error": {
"code": "subscription_required",
"message": "An active paid subscription is required",
"retryable": false
},
"traceId": "9dcbf565-01b4-475c-9690-90b24145cd88"
}| HTTP | What it means | What to do |
|---|---|---|
| 400 | Invalid input, voice or request ID | Check the parameter table, exact voice name and UUID format. Remove extra fields. |
| 401 | Missing, invalid, expired or revoked key | Check the Authorization header and key in API settings. Replace the key if it expired or was revoked. |
| 402 | Not enough available credits | Check quota.available. Wait for unfinished jobs to settle or add credits to your account. |
| 403 | Subscription or key does not allow API generation | Use a developer API key on an account with an active paid Basic, Pro or Scale subscription. |
| 404 | Job not found or not accessible | Use an API job ID from the same account. Website job IDs cannot be accessed here. |
| 409 | Conflicting request ID, or a completed job cannot be cancelled | Check error.code. For idempotency_conflict, retry with the original body; use a new UUID only if you intend a new recording. For job_already_completed, retrieve the finished job. |
| 413 | Request body is too large | Shorten the text or split it into separate jobs within the limits. |
| 415 | Unsupported content type | Send JSON with Content-Type: application/json. |
| 429 | A request, job or generation-attempt limit was reached | Wait for Retry-After. Allow existing jobs to finish before creating more. |
| 503 | Service temporarily unavailable, paused or queue full | When error.retryable is true, wait for Retry-After and retry with the same request ID and body. |
Check both the HTTP response and the job's status. A successful status query can still report a failed job. Keep error.code and traceId when asking for help. traceId (also sent as X-Request-Id) identifies one HTTP request; it is different from your recording's requestId.
A request timed out: did my job start?
Do not generate a new request ID just because a response was lost. Retry the original create request with the same ID and body, or find the existing job:
curl --include --silent --show-error "https://flowspeech.io/api/v1/tts/jobs?requestId=$FLOWSPEECH_REQUEST_ID" \
-H "Authorization: Bearer $FLOWSPEECH_API_KEY"If this lookup returns 404, retry the original create request with the same ID and body. Once you have a job ID, continue querying that job. A replay returns 202 while unfinished or 200 after it has finished.
A failed or cancelled job stays in that state when the same request ID is reused. To intentionally try generation again, fix any input or account issue first, then use a new UUID.
Cancel a job
curl --include --silent --show-error --request POST \
"https://flowspeech.io/api/v1/tts/jobs/$FLOWSPEECH_JOB_ID/cancel" \
-H "Authorization: Bearer $FLOWSPEECH_API_KEY"Cancelling an unfinished job releases its reserved credits. If the job finished before cancellation took effect, the API returns 409 job_already_completed; the completed recording remains charged. Repeated cancellation returns the existing terminal state, except that successful jobs still return this conflict.
Request ID behavior across retries and key rotation
Request IDs belong to the account and API version, so rotating an API key does not create duplicate jobs. Reusing a UUID with different normalized generation parameters returns 409 idempotency_conflict.
Replaying an existing request works after subscription expiry, exhausted credits or a pause on new API jobs. It still requires a valid key and obeys request rate limits. IDs are remembered for at least 30 days and for as long as the task metadata is retained; task metadata is currently not automatically deleted. Always use a new UUID for new work.
Audio access and retention
Anyone holding a result link can download the recording. These are public, link-accessible URLs, not private signed URLs. Successful recordings follow the same storage policy as website creations and currently have no automatic expiry. Temporary audio chunks expire after two days.
Deleting a creation hides its result from API responses (result: null, resultDeleted: true) and the creations list. It does not immediately delete the underlying audio or invalidate a previously copied URL. Do not rely on deletion to revoke download access. Raw scripts and task metadata remain stored for recovery and accounting.
OpenAPI and developer tools
Download the API definition (OpenAPI JSON).
This file describes the endpoints, authentication, request fields and responses in a format developer tools can read. Import it into tools such as Postman to explore requests, or use an OpenAPI-compatible generator to create a client. You do not need to download it to follow this guide.
Migrating an existing integration
If you are connecting for the first time, use the /api/v1 examples above. This section is only for integrations that used the earlier endpoints.
Bearer requests to /api/ai/text-to-speech, its /quota endpoint and /jobs endpoints return 410 with migration guidance. Replace synchronous generation and its base64 response with the create → check → download flow in this guide. Website Cookie flows and plugin-specific authentication remain separate.
Legacy developer keys were hashed during migration, hidden from the UI and given a 30-day rotation window. Check the expiry in API settings, create a replacement key and update your server before it expires. Figma login tokens have a separate plugin-only scope and cannot call this public API.