DocsModalities
Audio & speech
Text-to-speech with POST /v1/audio/speech and speech-to-text with POST /v1/audio/transcriptions: voices, formats, timestamps, and billing.
INFRO is in private early access. Running these examples requires an early-access account, an active trial or subscription entitlement, and your own provider connection. Self-serve purchasing is not generally open. See Early access.
Two endpoints cover audio. /v1/audio/speech turns text into spoken audio, and /v1/audio/transcriptions turns an uploaded recording into text. Both are synchronous — there is no job to poll, unlike video.
The shapes differ from the rest of the API in one way each: speech returns raw audio bytes rather than JSON, and transcription takes a multipart/form-data upload rather than a JSON body. Both are served by connections whose provider kind speaks OpenAI's audio API — OpenAI, Azure OpenAI, Groq, and OpenAI-compatible endpoints.
POST https://api.infro.io/v1/audio/speech
POST https://api.infro.io/v1/audio/transcriptionsText to speech
modelstringrequired- A speech model, e.g.
openai/tts-1-hd, orelevenlabs/eleven-v3,cartesia/sonic-3, orminimax/speech-2.6where one of your connections serves them through an OpenAI-compatible speech API. Latency, voice range, and language coverage differ sharply — compare them in the catalog. inputstringrequired- The text to speak, up to 1,000,000 characters (the request body is limited to 1 MB; your provider may allow far less). Punctuation drives prosody, so keep sentence marks intact.
voicestringrequired- Voice ID from the model's voice list, up to 128 characters. IDs are vendor-specific and are not portable, which is why
fallbacksacross TTS vendors rarely works — a voice that exists on one vendor does not exist on another. speednumber0.5–2.0. Forwarded when set; the provider's default applies otherwise. Models without a rate control ignore it or refuse it.formatstring- Container and codec of the audio:
mp3(default),wav,opus,flac, orpcm. Sent to the provider as itsresponse_format; the exact sample rate and encoding ofpcmare the provider's. response_formatstringaudio(default) returns the audio bytes as the response body.urlstores the audio and returns JSON with a signed link that stops working 24 hours after the request, like generated images.
curl https://api.infro.io/v1/audio/speech \
-H "Authorization: Bearer $INFRO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "elevenlabs/eleven-v3",
"input": "Your deployment finished. Two of forty checks were skipped.",
"voice": "aria",
"format": "mp3"
}' \
--output speech.mp3import os
import requests
API_KEY = os.environ["INFRO_API_KEY"]
with requests.post(
"https://api.infro.io/v1/audio/speech",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"model": "elevenlabs/eleven-v3",
"input": "Your deployment finished. Two of forty checks were skipped.",
"voice": "aria",
"format": "mp3",
},
stream=True,
timeout=120,
) as res:
res.raise_for_status()
with open("speech.mp3", "wb") as f:
for chunk in res.iter_content(chunk_size=8192):
f.write(chunk)import { writeFile } from "node:fs/promises";
const res = await fetch("https://api.infro.io/v1/audio/speech", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.INFRO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "cartesia/sonic-3",
input: "Your deployment finished. Two of forty checks were skipped.",
voice: "aria",
format: "mp3",
}),
});
if (!res.ok) throw new Error(await res.text());
await writeFile("speech.mp3", Buffer.from(await res.arrayBuffer()));import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
Future<void> main() async {
final apiKey = Platform.environment['INFRO_API_KEY']!;
final res = await http.post(
Uri.parse('https://api.infro.io/v1/audio/speech'),
headers: {
'Authorization': 'Bearer $apiKey',
'Content-Type': 'application/json',
},
body: jsonEncode({
'model': 'elevenlabs/eleven-v3',
'input': 'Your deployment finished. Two of forty checks were skipped.',
'voice': 'aria',
'format': 'mp3',
}),
);
await File('speech.mp3').writeAsBytes(res.bodyBytes);
}Returning a link instead of audio
With "response_format": "url" the gateway reads the whole file from your provider, stores it, and answers with JSON. audio.url is signed and stops working at audio.expires_at. Usage is billed the same way in both modes, and the X-INFRO-* headers are sent on this response too.
{
"id": "req_7d2e4b90",
"created": 1755950000,
"model": "openai/tts-1-hd",
"route": "primary",
"route_name": null,
"provider": "openai",
"connection": "conn_4f10a2c7",
"audio": {
"url": "https://api.infro.io/media/aud/2026-09-24/org_.../req_7d2e4b90.mp3?exp=1756036400&sig=...",
"expires_at": "2026-09-25T12:00:00.000Z",
"mime_type": "audio/mpeg"
},
"usage": {
"characters": 58,
"cost": 0.00174,
"price_source": "list"
}
}Streaming playback
INFRO passes the provider's audio body through as it arrives, with the matching Content-Type. There is no separate streaming parameter — read the body as a stream instead of buffering it, as the Python tab above does, and playback can start as soon as your provider sends the first bytes.
Because the body is audio, the INFRO fields travel as response headers: X-INFRO-Cost (the estimated provider cost in USD, empty when the model is unpriced), X-INFRO-Route, X-INFRO-Provider, and X-INFRO-Request-Id.
mp3 and pcm are the practical choices for play-while-downloading. wav writes a length header some players wait for, and flac has to be decoded in full before it is useful, so both are better suited to files you save rather than play live.
Speech to text
filefilerequired- The recording, sent as
multipart/form-data.mp3,wav,m4a,flac,ogg, andwebmare accepted, up to 25 MB; a larger file is refused with400 invalid_request_error, and a request body over 26 MB with413 request_too_largebefore it is read. modelstringrequired- A transcription model, e.g.
openai/whisper-v3-turbo, ordeepgram/nova-4orassemblyai/universal-3where one of your connections serves them through an OpenAI-compatible transcription API — see the catalog. languagestring- ISO 639-1 hint, e.g.
enorde, forwarded to the provider. Omit to let the provider detect it. timestampsboolean- When
true, INFRO asks the provider for segment timestamps (verbose_jsonwith segment granularity) and the response includes asegmentsarray withstartandendoffsets in seconds. diarizeboolean- Implies
timestamps. INFRO does not send a diarization option to the provider; a segment carries aspeakerlabel only if the provider's response includes one.
curl https://api.infro.io/v1/audio/transcriptions \
-H "Authorization: Bearer $INFRO_API_KEY" \
-F file=@standup.m4a \
-F model="deepgram/nova-4" \
-F language="en" \
-F timestamps=true \
-F diarize=trueimport os
import requests
API_KEY = os.environ["INFRO_API_KEY"]
with open("standup.m4a", "rb") as audio:
res = requests.post(
"https://api.infro.io/v1/audio/transcriptions",
headers={"Authorization": f"Bearer {API_KEY}"},
files={"file": ("standup.m4a", audio, "audio/mp4")},
data={
"model": "deepgram/nova-4",
"language": "en",
"timestamps": "true",
"diarize": "true",
},
timeout=600,
)
res.raise_for_status()
transcript = res.json()
print(transcript["text"])
print(transcript["usage"]["cost"])import { openAsBlob } from "node:fs";
const form = new FormData();
form.append("file", await openAsBlob("standup.m4a"), "standup.m4a");
form.append("model", "deepgram/nova-4");
form.append("language", "en");
form.append("timestamps", "true");
form.append("diarize", "true");
const res = await fetch("https://api.infro.io/v1/audio/transcriptions", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.INFRO_API_KEY}` },
body: form,
});
if (!res.ok) throw new Error(await res.text());
const transcript = await res.json();
console.log(transcript.text, transcript.usage.cost);Transcription response
{
"text": "Deploys are green. Two checks were skipped on the mobile build.",
"language": "en",
"model": "deepgram/nova-4",
"route": "primary",
"route_name": null,
"provider": "openai-compatible",
"connection": "conn_4f0a9d17",
"segments": [
{
"id": 0,
"start": 0.0,
"end": 4.8,
"speaker": "speaker_0",
"text": "Deploys are green."
},
{
"id": 1,
"start": 4.8,
"end": 12.4,
"speaker": "speaker_1",
"text": "Two checks were skipped on the mobile build."
}
],
"usage": {
"seconds": 12.4,
"cost": 0.0014,
"price_source": "custom"
}
}text is always present and is the full transcript. language is what the provider reports, or null. segments appears only when timestamps (or diarize) is true, and speaker only when the provider labels speakers. route, provider, and connection name where the request ran; usage.cost is null when the model is unpriced.
INFRO does not store synthesized audio, input text, uploaded recordings, or transcripts from these two endpoints; it records request metadata (model, provider, status, duration or characters, estimated cost). See Privacy & data.
Billing
| Endpoint | Unit | Where cost is reported |
|---|---|---|
/v1/audio/speech | Per 1M characters of input | The X-INFRO-Cost response header, and the request log |
/v1/audio/transcriptions | Per hour of audio, metered by the second | usage.seconds and usage.cost in the response |
Providers typically bill speech per character, and INFRO's estimate counts every character of input, including whitespace and punctuation. Transcription is estimated on the duration the provider reports, so silence and music in a recording count the same as speech — trim dead air before uploading if you are processing hours of it. Vendor list rates are in the catalog.
Sound effects and music
POST /v1/audio/sound-effects and POST /v1/audio/music take { model, prompt, duration_seconds?, response_format? }; music also takes instrumental and lyrics where the model does (MiniMax sings the lyrics it is given and needs them; Lyria makes instrumental clips of a fixed length). Each model's lengths are checked before the call — ElevenLabs sound effects 0.5–30 s, Stable Audio 2.5 1–190 s — and a model that sets its own length refuses duration_seconds. The default answer is JSON with a signed link to the file, kept 24 hours; response_format: "audio" returns the file itself with its cost in X-INFRO-Cost. Priced per second, or per track. Music licences are the provider's own and narrower than "commercial use" on some plans; read them before you publish.
The text of a speech request, and the prompt of a sound or music request, passes your organization's input guardrails like a chat message: a blocking rule refuses it before any call, and a redacting rule changes what the provider receives.
Long recordings
Transcription holds the connection open for the whole job, and a provider that does not answer within your connection's timeout counts as a failed attempt. Uploads are also capped at 25 MB. Split anything past about an hour into chunks on silence boundaries, transcribe them in parallel, and concatenate the text — offsetting each chunk's segments by its start time keeps timestamps continuous. Status codes and retry guidance are in Errors, and per-key throughput in Rate & spend limits.