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

  1. Have an active paid Basic, Pro or Scale subscription. Monthly and annual plans are supported; trials do not include API access.
  2. Create a key in API settings and copy it immediately. The complete key is shown only once.
  3. 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.

statusWhat to do
queued, running, merging, settlingStill in progress. Wait and check again.
successDownload the audio from result.url.
failed, cancelledStop 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.wav

You 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.mjs

Python 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.py

To 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.mjs

For 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:

HeaderValue
AuthorizationBearer YOUR_API_KEY. Required on every API request.
Content-Typeapplication/json. Required when creating a job.
Idempotency-KeyA UUID that you generate for this recording. Required when creating a job. Reuse it for retries of that same request.
FieldWhen requiredMeaning
input.modeAlwayssingle for one voice, or multi for a sequence of speaker turns.
input.textSingle voiceThe words to speak. Must contain spoken text.
input.voiceNameSingle voiceA supported voice name, such as Kore. Case-sensitive.
input.segmentsMultiple speakersA nonempty list of turns, in speaking order.
input.segments[].speakerEvery turnYour character label, up to 100 characters.
input.segments[].textEvery turnThe words spoken in that turn.
input.segments[].voiceNameEvery turnThe 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 fieldMeaning
quota.remainingUnspent credits from currently valid credit grants, including credits set aside for unfinished jobs.
quota.reservedCredits set aside for unfinished jobs from those grants.
quota.availableCredits you can use now: remaining - reserved.
usage.billableCharactersThe billable character count for one job.
usage.reservedCreditsCredits currently set aside for that job.
usage.chargedCreditsThe 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:

LimitDefault
Create requests6 per minute
Read requests, including status, voices and quota60 per minute
Cancellation requests60 per minute
Unfinished API jobs2 at a time
Active, unexpired API keys5
Input text100,000 UTF-16 code units across all text or turns
Speaker turns1,000 per request
JSON body1 MiB
PausesUp 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"
}
HTTPWhat it meansWhat to do
400Invalid input, voice or request IDCheck the parameter table, exact voice name and UUID format. Remove extra fields.
401Missing, invalid, expired or revoked keyCheck the Authorization header and key in API settings. Replace the key if it expired or was revoked.
402Not enough available creditsCheck quota.available. Wait for unfinished jobs to settle or add credits to your account.
403Subscription or key does not allow API generationUse a developer API key on an account with an active paid Basic, Pro or Scale subscription.
404Job not found or not accessibleUse an API job ID from the same account. Website job IDs cannot be accessed here.
409Conflicting request ID, or a completed job cannot be cancelledCheck 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.
413Request body is too largeShorten the text or split it into separate jobs within the limits.
415Unsupported content typeSend JSON with Content-Type: application/json.
429A request, job or generation-attempt limit was reachedWait for Retry-After. Allow existing jobs to finish before creating more.
503Service temporarily unavailable, paused or queue fullWhen 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.