DEV Community

Cover image for How to Add Async Text-to-Speech to an App with the Fish TTS API
Germey
Germey

Posted on • Originally published at platform.acedata.cloud

How to Add Async Text-to-Speech to an App with the Fish TTS API

Long text-to-speech jobs are easy to prototype, but they can become awkward in production when your app has to hold a request open while audio is being generated.

What you can do

The Fish TTS endpoint on Ace Data Cloud gives you a simple way to turn text into audio while keeping the request shape compatible with the Fish Audio text-to-speech API. In practice, that means you can send a familiar JSON body, authenticate with an Ace Data Cloud bearer token, and receive an audio_url that your app can play, download, or pass to the next step in a workflow.

The base request is intentionally small:

  • Base URL / endpoint: POST https://api.acedata.cloud/fish/tts
  • Authentication: Authorization: Bearer {token}
  • Content type: application/json
  • Model header: model: s2-pro or model: s1 (s2-pro is the default)
  • Required body field: text
  • Optional voice field: reference_id
  • Optional output fields: format, sample_rate, mp3_bitrate
  • Async extension: callback_url
  • Typical result field: audio_url

This makes the endpoint useful for product narration, voice notes, generated learning content, internal tools, or any workflow where the output should become a real audio file instead of just a browser-side demo.

How it works

The synchronous version is the smallest useful call. Send text, include your token, choose the model in the request header, and read the returned audio_url:

curl -X POST 'https://api.acedata.cloud/fish/tts' \
  -H 'accept: application/json' \
  -H 'authorization: Bearer {token}' \
  -H 'content-type: application/json' \
  -H 'model: s2-pro' \
  -d '{
    "text": "今天天气真好,我们一起出去散散步吧。"
  }'
Enter fullscreen mode Exit fullscreen mode

A successful response returns an audio file URL:

{
  "audio_url": "https://platform.r2.fish.audio/task/8a72ff9840234006a9f74cb2fa04f978.mp3"
}
Enter fullscreen mode Exit fullscreen mode

If your app is already written against Fish Audio's official request body, the important migration detail is that the body structure stays the same. The main change is authentication: use Authorization: Bearer {token} with the token from Ace Data Cloud. The endpoint also accepts Fish request fields such as text, reference_id, references, prosody, format, sample_rate, mp3_bitrate, chunk_length, temperature, and top_p.

Add a cloned voice or explicit audio format

For many real apps, plain TTS is only the first step. You may want a known voice, a predictable file type, or a fixed sample rate for a downstream media pipeline. The endpoint supports reference_id, format, and sample_rate in the JSON body:

curl -X POST 'https://api.acedata.cloud/fish/tts' \
  -H 'accept: application/json' \
  -H 'authorization: Bearer {token}' \
  -H 'content-type: application/json' \
  -H 'model: s2-pro' \
  -d '{
    "text": "今天天气真好,我们一起出去散散步吧。",
    "reference_id": "d7900c21663f485ab63ebdb7e5905036",
    "format": "mp3",
    "sample_rate": 44100
  }'
Enter fullscreen mode Exit fullscreen mode

That is the shape I would use when integrating TTS into a service that stores generated audio assets. Your backend can submit the text, save the resulting audio_url, and associate it with the original content record.

Use callbacks for longer jobs

For short snippets, a direct request is fine. For longer text, keeping an HTTP connection open can make the rest of your system more fragile: workers time out, clients retry, and duplicate jobs become harder to reason about.

Ace Data Cloud adds an async callback extension through callback_url. When you include this field, the API immediately returns a task_id and started_at timestamp. Later, when generation completes, the final payload is posted to your callback URL with the same task_id and the generated audio_url.

curl -X POST 'https://api.acedata.cloud/fish/tts' \
  -H 'accept: application/json' \
  -H 'authorization: Bearer {token}' \
  -H 'content-type: application/json' \
  -H 'model: s2-pro' \
  -d '{
    "text": "今天天气真好,我们一起出去散散步吧。",
    "callback_url": "https://webhook.site/4815f79f-a40f-4078-ac85-1cc126b6bb34"
  }'
Enter fullscreen mode Exit fullscreen mode

Immediate response:

{
  "task_id": "2725a2d3-f87e-4905-9c53-9988d5a7b2f5",
  "started_at": "2025-05-09T12:34:56.789Z"
}
Enter fullscreen mode Exit fullscreen mode

Callback payload:

{
  "task_id": "2725a2d3-f87e-4905-9c53-9988d5a7b2f5",
  "audio_url": "https://platform.r2.fish.audio/task/b627c2f7d38a4083a837570ba6d0962f.mp3"
}
Enter fullscreen mode Exit fullscreen mode

In a production app, store the task_id as soon as the first response arrives. When your webhook receives the callback, look up the pending job, save the audio_url, and mark the job complete. If you need active status checks instead, the documentation notes that the task_id can also be used with the Fish Tasks API.

Handle errors as part of the workflow

Do not treat TTS as a fire-and-forget side effect. The endpoint preserves upstream HTTP status behavior and returns a unified platform error format. The common cases to handle are 400 token_mismatched, 400 api_not_implemented, 401 invalid_token, 429 too_many_requests, and 500 api_error.

A typical error response looks like this:

{
  "success": false,
  "error": {
    "code": "api_error",
    "message": "fetch failed"
  },
  "trace_id": "2cf86e86-22a4-46e1-ac2f-032c0f2a4e89"
}
Enter fullscreen mode Exit fullscreen mode

Keep the trace_id in logs. It is the kind of small detail that saves time when debugging a failed media generation job later.

A practical integration pattern

For a builder-friendly implementation, I would start with three states in my own database: queued, processing, and ready. The app creates a row when a user submits text, calls POST /fish/tts with callback_url, stores the returned task_id, and moves the row to processing. The webhook validates the payload, saves audio_url, and marks the row as ready.

This keeps your UI responsive and avoids making users wait on a long request. It also gives you a clean retry boundary: retry the job submission if the initial request fails, and retry webhook processing if your own server has a temporary issue.

If you want the exact compatibility notes and field list, read the full Fish TTS integration guide on Ace Data Cloud: https://platform.acedata.cloud/documents/fish-tts-integration

Top comments (0)