Cover image for How to Implement Fish Audio: API, Voice Cloning, Streaming, and What It Actually Costs

At a glance

Reading time

~200 words/min

Published

15 hours ago

Aug 9, 2026

Views

13

All-time total

How to Implement Fish Audio: API, Voice Cloning, Streaming, and What It Actually Costs

In the previous post I looked at what Fish Audio is as a company and why three of its models are open while the flagship sits behind a paid API. This one is the other half: how you actually wire it into something. Authentication, the endpoint, the Python SDK, both routes to voice cloning, streaming that starts speaking before your language model has finished thinking, and the pricing arithmetic that decides whether any of it is affordable at your volume. Everything below comes from the current documentation rather than from memory.

The short version

  • One POST to https://api.fish.audio/v1/tts with a bearer token gets you audio; the model is chosen with a header, not a body field
  • The s2.1-pro-free model is the same model as s2.1-pro at zero cost, intended for testing and prototyping, which makes evaluation genuinely free
  • Billing is 15 dollars per million UTF-8 bytes, which is roughly 180,000 English words or about 12 hours of speech
  • Voice cloning comes in two forms: a persistent reusable voice model, or zero shot references passed inline with a single request
  • The websocket mode accepts a generator of text tokens, so you can speak an LLM response as it is produced instead of waiting for it
  • Your rate limit is concurrency based and rises with lifetime spend, starting at 5 simultaneous requests

The smallest thing that works

Before installing anything, confirm your key works with a single request. The endpoint is POST https://api.fish.audio/v1/tts, authentication is a standard bearer token, and the response is a chunked audio stream rather than a JSON envelope containing a URL.

# The model is selected with a header, which is unusual enough to trip people up.
# s2.1-pro-free costs nothing, so use it while you are still experimenting.

curl --request POST \
  --url https://api.fish.audio/v1/tts \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --header 'model: s2.1-pro-free' \
  --data '{"text": "Hello! Welcome to Fish Audio."}' \
  --output hello.mp3

Two details in that request matter more than they look. The model header is how you pick the engine, so a request that seems to ignore your model choice is usually one where the value was put in the JSON body instead. And the response streams as audio bytes directly, so if you are debugging by printing the body you will get binary noise rather than an error message.

The Python SDK

For anything beyond a smoke test the SDK is less work. One thing to note before you start: the package you install and the module you import are not spelled the same way, which costs people a few confused minutes.

pip install fish-audio-sdk      # note the hyphens
export FISH_API_KEY=your_api_key_here
from fishaudio import FishAudio          # but the module has no hyphens
from fishaudio.utils import save

client = FishAudio()                     # reads FISH_API_KEY from the environment
# or be explicit: FishAudio(api_key="...")
# AsyncFishAudio is the asyncio variant

audio = client.tts.convert(text="Hello from Fish Audio!")
save(audio, "out.mp3")

Output format, sample rate and speaking rate are all adjustable. The default format is mp3, which is right for delivery to a browser and wrong for anything you intend to process further, since you want uncompressed frames for that.

from fishaudio.types import TTSConfig

# wav or pcm when the audio feeds another system, mp3 or opus when it feeds a person
audio = client.tts.convert(
    text="High quality narration for the archive.",
    config=TTSConfig(format="wav", sample_rate=44100),
)

# Speed accepts 0.5 to 2.0. Small adjustments read as natural, large ones do not.
brisk = client.tts.convert(text="Speaking a little faster.", speed=1.2)

# Pick a specific voice by id
branded = client.tts.convert(
    text="This uses a specific voice.",
    reference_id="9a9cf47702da476aa4629e2506d4a857",
)

For long documents, use stream rather than convert. It yields chunks instead of building the entire clip in memory, which matters once you are narrating something book length.

with open("chapter.mp3", "wb") as f:
    for chunk in client.tts.stream(text=very_long_passage):
        f.write(chunk)

Which model to use, and the free tier that is not a downgrade

This is the part worth reading carefully, because the naming hides something genuinely useful. The documentation describes s2.1-pro as the recommended production model, with improved quality, latency and throughput over the previous S2-Pro. It describes s2.1-pro-free as the same model at zero cost, intended for testing, prototyping and development.

Model options and current pricing
Model Price Use it for
s2.1-pro 15.00 dollars per million UTF-8 bytes Production, the recommended default
s2.1-pro-free 0.00 dollars Evaluation, prototyping, development
s2-pro 15.00 dollars per million UTF-8 bytes Previous generation, superseded
s1 15.00 dollars per million UTF-8 bytes Legacy, only for existing integrations
transcribe-1 0.36 dollars per audio hour Speech to text, billed to the second
voice-design-1 0.01 dollars per successful request Generating a voice from a description

The practical consequence is that you can evaluate the actual production model against your real content, in your real languages, at no cost, before committing a single rupee or dollar. That removes the usual excuse for benchmarking on a weaker tier and then being surprised in production. Do your quality testing on the free model, and change one header value when you go live.

💡

Tip

Evaluate on the model you will ship

Since s2.1-pro-free is documented as the same model at zero cost, there is no reason to assess quality on anything else. Run your worst text through it, the product names, the numbers, the abbreviations, and only then decide whether the paid tier earns its place.

Voice cloning, both ways

There are two approaches and they suit different products. Create a persistent voice model when the same voice will be reused, since you clone once and then reference an id forever. Use zero shot references when the voice is supplied per request and there is nothing to keep.

# Route 1: a reusable voice model. Clone once, reference the id afterwards.
with open("sample.wav", "rb") as f:
    voice = client.voices.create(
        title="Narrator",
        voices=[f.read()],
        description="Cloned from a studio sample",
        visibility="private",          # private is the default; unlist and public also exist
    )

print(voice.id, voice.state)

audio = client.tts.convert(
    text="Now I speak in the cloned voice.",
    reference_id=voice.id,
)
# Route 2: zero shot. Nothing is stored, the reference travels with the request.
from fishaudio.types import ReferenceAudio

with open("reference.wav", "rb") as f:
    audio = client.tts.convert(
        text="This will sound like the reference voice.",
        references=[ReferenceAudio(
            audio=f.read(),
            text="The exact words spoken in the reference clip.",
        )],
    )

Note the transcript in the zero shot path. You pass both the audio and the words actually spoken in it, and accuracy there measurably affects the result. A transcript that does not match the audio produces a worse clone than no attempt at all.

On source material, the documentation is specific: WAV, MP3, M4A or Opus, a minimum of ten seconds per clip, and one to two minutes of clear single speaker speech as the optimum. That last figure is worth respecting. Ten seconds works, but the difference between the minimum and a proper minute of clean audio is audible, and it is the cheapest quality improvement available. Audio enhancement for noisy recordings is on by default.

Warning

The consent path is your responsibility

Ten seconds of audio is enough to impersonate someone. If users can upload reference audio, you have shipped a voice cloning tool. Verify the uploader has rights to the voice, log which account generated which clip, and disclose synthetic audio to listeners. None of this is provided by the API.

Streaming, and speaking while the model is still thinking

For voice agents and assistants, the HTTP streaming path is still too slow, because it cannot begin until your language model has produced the complete text. The websocket mode removes that wait entirely. It accepts a generator of text tokens and starts emitting audio as the words arrive.

from fishaudio import FishAudio
from fishaudio.utils import play

client = FishAudio()

def llm_tokens():
    # In production this yields tokens from your LLM stream, not a fixed list
    for token in ["The ", "first ", "move ", "sets ", "everything ", "in ", "motion."]:
        yield token

for chunk in client.tts.stream_websocket(llm_tokens(), reference_id="YOUR_VOICE_ID"):
    play(chunk)

This is the single most important pattern in the whole integration for anything conversational. Without it, perceived latency is the language model generation time plus the synthesis time. With it, the two overlap, and the user hears the first words while the model is still composing the rest. That difference is usually the gap between an assistant that feels responsive and one that feels broken.

There is a latency parameter with two settings, and the documentation attaches actual numbers to them. normal is described as best quality at roughly 500 milliseconds, and balanced as good quality at roughly 300 milliseconds. For anything conversational that 200 millisecond difference is worth more than the quality increment, and the documentation itself recommends balanced mode when audio is taking too long to start. Reserve normal for pre rendered audio where nobody is waiting. An async client, AsyncFishAudio, takes async generators in the same shape.

Latency modes and where each belongs
Mode Stated quality Stated latency Use for
balanced Good quality about 300ms Voice agents, assistants, anything interactive
normal Best quality about 500ms Narration and pre rendered audio
💡

Pro tip

Measure time to first audio byte at the client rather than the server. Server side timings routinely miss 100 to 300 milliseconds of buffering and playback startup, which is exactly the range where an interface stops feeling immediate.

What it costs, in numbers you can plan with

Billing is per million UTF-8 bytes rather than per character or per second, which is awkward to reason about until you convert it. The documentation puts one million UTF-8 bytes at roughly 180,000 English words, or about 12 hours of speech. At 15 dollars per million, that gives some useful reference points.

Working out real costs at 15 dollars per million UTF-8 bytes
Workload Approximate size Approximate cost
A 1,500 word article read aloud about 8,300 bytes about 0.13 dollars
100 such articles about 830,000 bytes about 12.50 dollars
12 hours of continuous speech about 1,000,000 bytes about 15 dollars
A 30 word notification, 10,000 times about 1,700,000 bytes about 25 dollars

Be careful with the UTF-8 detail if you work in non Latin scripts. Billing counts bytes, not characters, and Sinhala, Tamil, Chinese, Japanese, Arabic and similar scripts use multiple bytes per character. Text that looks the same length as an English sentence can cost two or three times as much, which is worth modelling before you launch a localised product rather than discovering it on an invoice.

The other limit to plan around is concurrency rather than volume. Rate limits are expressed as simultaneous requests and rise with lifetime spend.

Concurrency limits by tier
Tier Threshold Concurrent requests
Starter Under 100 dollars paid 5
Elevated 100 dollars or more paid 15
High Volume 1,000 dollars or more paid 50
Enterprise Custom Custom

Five concurrent requests is comfortable for a content pipeline and tight for anything user facing at scale, so queue your work rather than firing requests as they arrive. A simple worker pool sized to your tier, with retries on rejection, prevents the failure mode where a traffic spike turns into a wall of errors.

The production checklist

Four things separate a working prototype from something you can leave running.

1. Cache on a hash of text + voice + parameters.
   Applications repeat far more utterances than anyone expects,
   and cached audio costs nothing to serve.

2. Normalise text before synthesis.
   Expand currency, dates, units and known acronyms into the words
   you want spoken. This removes most reported quality complaints.

3. Queue to your concurrency tier.
   Five simultaneous requests on the starter tier. Size a worker pool
   to match and retry on rejection instead of failing the user.

4. Log voice provenance.
   Which voice, which account, which authorisation, retained.
   You cannot answer the only question that matters after an incident
   without it.

! Common mistakes to avoid

  • Putting the model name in the JSON body.

    The model is selected with a request header. A body field is silently ignored, so you get the default model and conclude your model choice does not work.

  • Evaluating quality on a weaker tier and shipping on the flagship.

    The free model is documented as the same model as s2.1-pro at zero cost. Test on that and change one header when you go live.

  • Passing a transcript that does not match the reference audio.

    Zero shot cloning uses the transcript as conditioning. A mismatched one produces a worse result than a careful, accurate transcript of exactly what was said.

  • Waiting for the full LLM response before calling synthesis.

    Use the websocket mode with a token generator. Generation and synthesis overlap, which removes the model thinking time from perceived latency entirely.

  • Budgeting per character when your content is not Latin script.

    Billing counts UTF-8 bytes. Many scripts use two to four bytes per character, so model your real languages before committing to a price per article.

  • Firing requests as they arrive and ignoring concurrency limits.

    Limits are concurrent requests, starting at five. Queue through a worker pool sized to your tier so a spike degrades gracefully instead of erroring.

? Frequently asked questions

What is the Fish Audio API endpoint? +

Text to speech is POST https://api.fish.audio/v1/tts with an Authorization bearer header. The model is chosen with a separate model header, and the response is a chunked audio stream rather than JSON.

Is there a genuinely free way to test it? +

Yes. The s2.1-pro-free model is documented as the same model as s2.1-pro priced at zero, intended for testing, prototyping and development, so you can evaluate production quality before paying.

How much does Fish Audio cost? +

The paid speech models are 15 dollars per million UTF-8 bytes, which the documentation puts at roughly 180,000 English words or about 12 hours of speech. Transcription is 0.36 dollars per audio hour.

How much reference audio do I need to clone a voice? +

A minimum of ten seconds per clip, with one to two minutes of clear single speaker audio described as optimal. Accepted formats are WAV, MP3, M4A and Opus, and audio enhancement is enabled by default.

What is the difference between reference_id and references? +

reference_id points at a stored voice model you created earlier and want to reuse. references carries the reference audio inline with a single request for zero shot cloning, with nothing persisted.

Can I stream audio from an LLM as it generates? +

Yes, that is what the websocket mode is for. It accepts a generator yielding text tokens and returns audio chunks as they are produced, so synthesis overlaps generation instead of following it.

What is the pip package called? +

Install fish-audio-sdk with hyphens, then import fishaudio without them. The mismatch between the package name and the module name is a common few minutes of confusion when starting out.

Which latency mode should I use? +

Balanced for anything interactive, documented at good quality and roughly 300 milliseconds against normal at best quality and roughly 500. The docs recommend balanced when audio is slow to start playing.

What are the rate limits? +

They are concurrency based and scale with lifetime spend: 5 simultaneous requests under 100 dollars, 15 at 100 dollars or more, 50 at 1,000 dollars or more, and custom limits for enterprise.

Putting it together

The integration itself is genuinely small. A bearer token, one endpoint, a header to pick the model, and a handful of parameters. The work that decides whether the result is any good sits either side of that call: normalising your text so numbers and acronyms are spoken correctly, caching so you are not paying twice for the same sentence, queueing to your concurrency tier, streaming from your language model so the two stages overlap, and keeping an honest record of whose voice you are using and why. Start on the free model with your worst content, and only move to the paid tier once you know exactly what you are buying.

Note

Specs in this post

Endpoint paths, parameter names, default values, prices and rate limits were taken from the official documentation at the time of writing and change without much notice. Check the current docs before depending on any figure here in production.

Bishrul Haq

Written by

Bishrul Haq

Software engineer writing practical tutorials on Laravel, PHP, Python, and the tools behind real projects. More about me

Newsletter

Want more posts like this?

Get practical software notes and tutorials delivered when something new is published.

No spam. Unsubscribe anytime.

How did this land?

Comments

0
Log in or sign up to join the discussion and react to this post.

No comments yet. Be the first to share your thoughts.

Related posts

Important functionalities of Pandas in Python : Tricks and Features

Pandas is one of my favorite libraries in python. It’s very useful to visualize the data in a clean structural manner. Nowadays Pandas is widely used in Data Science, Machine Learning and other areas.

6 years ago

How to get data from twitter using Tweepy in Python?

To start working on Python you need to have Python installed on your PC. If you haven’t installed python. Go to the Python website and get it installed.

6 years ago

Predicting per capita income of the US using linear regression

Python enables us to predict and analyze any given data using Linear regression. Linear Regression is one of the basic machine learning or statistical techniques created to solve complex problems.

6 years ago

Essential Sorting Algorithms for Computer Science Students

Algorithms are commonly taught in Computer Science, Software Engineering subjects at your Bachelors or Masters. Some find it difficult to understand due to memorizing.

6 years ago

Build an AI-Powered Blog SEO Generator with Laravel and Ollama

Create a practical AI SEO generator for a Laravel blog. Generate meta titles, descriptions, keywords, FAQ schema, slugs, and internal link ideas with Ollama or OpenAI.

3 months ago