Tutorial
How to Call gemini-3.1-flash-tts on HiAPI: curl, Python, and a Working Request
What gemini-3.1-flash-tts gives you on HiAPI
gemini-3.1-flash-tts is a text-to-speech model offered through the HiAPI unified API. You send text plus a voice configuration, and the platform returns generated audio. Because it is exposed through a single, consistent interface, you do not need separate integrations per model — the same auth header, the same base URL, and the same async job pattern apply to every TTS model on the platform.
Reach for this model when you need:
- Multi-speaker narration. Scripts, dialogues, or podcast-style content where different segments should sound like different voices.
- Fast turnaround. The
flashfamily is aimed at low-latency generation, which matters for interactive apps or batch jobs with tight SLAs. - A predictable integration surface. One request shape covers everything from a single line of text to a multi-speaker scene.
If you are building an app that already calls other HiAPI models, adding speech is a matter of swapping the model identifier and adapting the payload — no new SDK, no new credentials flow.
Prerequisites and authentication setup
Before your first call, make sure you have:
- A HiAPI account and API key. Generate a key from your dashboard. Treat it as a secret — do not commit it to source control.
- The HiAPI base URL. All requests go to the same host; only the path changes per endpoint.
- A tool to make HTTP requests.
curlfor quick tests, and Python 3.9+ with therequestslibrary for scripts.
Authentication is a bearer token in the Authorization header:
Authorization: Bearer $HIAPI_API_KEY
Store the key in an environment variable rather than hard-coding it:
export HIAPI_API_KEY="your_key_here"
Every endpoint described below — job submission, status checks, and file retrieval — uses this same header. If you get a 401, the header is missing, malformed, or the key has been revoked. Rotate keys from the dashboard rather than sharing one across environments.
Building the request payload for multi-speaker speech
The request body is JSON. At minimum it contains the model identifier and the input. For multi-speaker output you add a speaker configuration that maps named speakers to voices, and you mark up the input text with speaker tags.
A typical payload looks like this:
{
"model": "gemini-3.1-flash-tts",
"input": {
"text": "[Host] Welcome back to the show. [Guest] Thanks for having me.",
"multi_speaker": true,
"speakers": [
{ "name": "Host", "voice": "voice_a" },
{ "name": "Guest", "voice": "voice_b" }
]
}
}
Key points that trip people up:
- Speaker names must match exactly. The
namein thespeakersarray must match the tag used in the text (inside square brackets, in this example). Case-sensitive mismatch is the most common cause of a failed multi-speaker render. - Every tag must be declared. If the text references
[Narrator]but no speaker withname: "Narrator"is defined, the request will be rejected. - Voice identifiers come from the platform's voice catalog. Use the identifiers exposed by HiAPI rather than guessing. For single-speaker jobs, omit
multi_speakerandspeakersand set a singlevoicefield instead. - Text is sent as one string. Long scripts are fine, but keep an eye on per-request limits documented on the platform.
Once the payload validates, the job is accepted and returns a job identifier you will use for polling.
Running it with curl
Save the payload above as payload.json, then submit it:
curl -X POST "$HIAPI_BASE_URL/v1/audio/speech/jobs" \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Content-Type: application/json" \
-d @payload.json
The response contains a job object with an id and a status field (typically queued or processing):
{ "id": "job_abc123", "status": "queued" }
To check progress, fetch the job by ID:
curl "$HIAPI_BASE_URL/v1/audio/speech/jobs/job_abc123" \
-H "Authorization: Bearer $HIAPI_API_KEY"
When status becomes succeeded, the response includes an audio file reference (a URL or file ID) you can download with a follow-up request — see the polling section below. If the job fails, status will be failed and an error field will explain why.
For quick experiments, you can skip the file and inline the JSON:
curl -X POST "$HIAPI_BASE_URL/v1/audio/speech/jobs" \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"gemini-3.1-flash-tts","input":{"text":"Hello from HiAPI.","voice":"voice_a"}}'
Running it with Python
The Python equivalent uses requests and a small helper to keep the polling loop readable. The payload is identical to the curl example.
import os
import time
import requests
API_KEY = os.environ["HIAPI_API_KEY"]
BASE_URL = os.environ["HIAPI_BASE_URL"]
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
payload = {
"model": "gemini-3.1-flash-tts",
"input": {
"text": "[Host] Welcome back to the show. [Guest] Thanks for having me.",
"multi_speaker": True,
"speakers": [
{"name": "Host", "voice": "voice_a"},
{"name": "Guest", "voice": "voice_b"},
],
},
}
resp = requests.post(
f"{BASE_URL}/v1/audio/speech/jobs",
headers={**HEADERS, "Content-Type": "application/json"},
json=payload,
timeout=30,
)
resp.raise_for_status()
job = resp.json()
job_id = job["id"]
print("submitted", job_id)
while True:
r = requests.get(f"{BASE_URL}/v1/audio/speech/jobs/{job_id}", headers=HEADERS, timeout=30)
r.raise_for_status()
status = r.json()
if status["status"] in ("succeeded", "failed"):
break
time.sleep(2)
if status["status"] == "failed":
raise RuntimeError(status.get("error"))
print("audio ready:", status["output"])
Notes:
- Timeouts matter. Set them explicitly on every request; a hung polling loop is worse than a failed one.
- Don't poll too aggressively. A two-second interval is reasonable for short scripts; back off for long ones.
- Keep the payload in a function. In real code, parameterize text, speakers, and voice so you can reuse the same submission logic for single-speaker jobs.
Polling the async job and downloading the audio
Speech generation is asynchronous: you submit, then check status until the job reaches a terminal state. Two terminal states exist — succeeded and failed. Treat everything else as "keep waiting."
A robust polling routine should:
- Cap total wait time. Even a fast model can queue behind other work. Stop after a reasonable ceiling and surface the job ID so the caller can retry.
- Exponential backoff. Start at one or two seconds and double up to a cap. This avoids hammering the API on long jobs.
- Handle failures explicitly. When
status == "failed", read theerrorfield and log it — it will usually point at a payload problem (undeclared speaker, empty text, unsupported voice).
When the job succeeds, the status response includes an output reference. Download it with the same auth header:
out = status["output"]
audio = requests.get(out["url"], headers=HEADERS, timeout=60)
audio.raise_for_status()
with open("narration.wav", "wb") as f:
f.write(audio.content)
If the response returns a file ID instead of a URL, fetch it through the corresponding file endpoint on HiAPI. Store the original job ID alongside the saved file — it is the only reliable way to trace a bad render back to its request later.
Understanding per-character pricing
gemini-3.1-flash-tts on HiAPI is billed by character count of the input text. There are no per-request or per-minute components to model — the cost of a job scales with how much text you send, which makes estimation straightforward: total characters across all scripts, times the current per-character rate.
Practical implications:
- Estimate before you ship. Sum your expected monthly text volume and check it against the current rate shown on the pricing panel. Do not hard-code rates into your code; read them from the platform at build time or keep them in configuration.
- Long-form content dominates cost. A ten-minute narration is mostly text cost. If you are generating audiobooks or course material, model the volume first.
- Retries cost money. A failed job that failed after generation may still be billed depending on the failure mode. Validate payloads client-side — check that every speaker tag is declared and that text is non-empty — before submitting.
- Cache aggressively. If the same script is rendered repeatedly (e.g., a demo or test fixture), save the resulting audio and reuse it rather than re-billing the same characters.
For current rates and any volume tiers, consult the pricing panel on the site — that is the single source of truth.
Common failure points and how to fix them
Most integration issues fall into a handful of buckets. Work through these in order.
401 Unauthorized
Your Authorization header is missing or malformed. Confirm the key is set in the environment and that the header reads exactly Bearer <key>. If the key was rotated, update your environment.
400 Bad Request — unknown speaker
The text contains a [Name] tag that is not declared in speakers, or the casing differs. Make the tag and the name field byte-for-byte identical.
400 Bad Request — empty or oversized input
Empty text fails validation immediately. Very long scripts may exceed per-request limits; split them into multiple jobs rather than truncating.
400 Bad Request — invalid voice identifier
Voice names must come from the platform's catalog. Copy the identifier exactly; do not invent friendly names.
Job stuck in processing
This is usually queueing, not a bug. Apply backoff and a total timeout. If it persists well beyond normal, log the job ID and retry once with a smaller payload to isolate whether size is the issue.
Job failed with a generation error
Read the error field. It typically points at a content or configuration issue rather than a transient fault. Fix the payload, resubmit, and avoid blind retry loops — each attempt may bill characters.
Audio downloads but won't play
Check the content type of the downloaded file before assuming corruption. Save with the correct extension for the returned format and verify the byte length is non-zero. A truncated file usually means the download request was interrupted — retry the download, not the generation.
With these checks in place, most first-day integration problems resolve in minutes rather than hours.
Related articles
View all articles →-
Tutorial
How to Call hailuo-2.3-fast/image-to-video via the HiAPI API: curl and Python
A first working request for the fast, low-cost image-to-video tier — from model ID to downloaded MP4
9 min read -
Tutorial
How to Use hailuo-2.3/text-to-video via the HiAPI API: curl, Python, and a Working Request
11 min read -
Tutorial
Kandinsky 6.0 Pro Text-to-Video on HiAPI: curl and Python Tutorial
A request-first walkthrough: build the prompt-to-clip call, submit it, poll for completion, and download the result
11 min read