.md

Overview

Welcome to the Fluxions API. Our hosted endpoints cover three product surfaces:

  • Transcription — akro-v1, our listening model: speech-to-text, speaker diarization, and non-speech events (breaths, laughter, hesitations) in one call. Production-ready today.
  • Text-to-Speech — hosted VUI for expressive, low-latency TTS over HTTP or WebSocket. Live today — see Speech.
  • Realtime Voice — end-to-end streaming voice conversations over WebSocket or WebRTC, with your own persona and your own tools. See the Realtime Voice API reference and the bring-your-own-tools guide.

This page covers the basics that apply across all surfaces: authentication, base URL, and a health check.

Authentication

All API requests require authentication using an API key. Include your API key in the Authorization header:

curl "https://api.fluxions.ai/endpoint" \
-H "Authorization: YOUR_API_KEY"

A Bearer prefix is optional — Authorization: YOUR_API_KEY and Authorization: Bearer YOUR_API_KEY authenticate identically.

Base URL

Base URL
https://api.fluxions.ai

GET /health — Health Check

Check the API status and version information. No authentication required.

Request

curl "https://api.fluxions.ai/health"

Response

GET /health · response
{
"status": "ok",
"gateway": "api.fluxions.ai"
}

GET /billing/balance — Account Balance

Check your remaining balance and the rates you are being charged. Cheap and read-only, so it is safe to poll — use it to top up before your traffic starts failing rather than discovering the problem through a 402.

Request

curl "https://api.fluxions.ai/billing/balance" \
-H "Authorization: YOUR_API_KEY"

Response

GET /billing/balance · response
{
"balance_usd": 44.68,
"balance_microcents": 4468381000,
"gated": false,
"plan": null,
"rates": {
"tts_per_mchar_usd": 10.0,
"transcription_per_hour_usd": 0.20,
"transcription_minimum_usd": 0.01
}
}
FieldDescription
balance_usdRemaining balance in dollars
balance_microcentsThe same balance, exact. Money is stored in microcents (1 cent = 106); use this field if you need to avoid float rounding
gatedtrue when your next billable request will be refused with a 402. Poll this
planYour subscription plan key, or null for pay-as-you-go. Subscribers are governed by their plan allowance rather than the wallet, so they are never gated here
ratesWhat you are charged. transcription_minimum_usd is a per-request floor — see Transcription

Balances are live: renders are debited as they happen, so the number here reflects usage up to the moment you asked.

Transcription

Our akro-v1 model is a comprehensive listening model that performs:

  • Transcription — Convert speech to text with high accuracy
  • Speaker Diarization — Identify and separate different speakers ("who said what")
  • Non-Speech Detection — Capture breathing, laughter, hesitation, and other contextual sounds

This makes it ideal for transcribing meetings, interviews, podcasts, and any audio where understanding the full context matters.

All transcription endpoints require authentication — see Overview for API key setup.

Pricing: $0.20 per hour of audio processed, billed by the second, with a $0.01 minimum per request.

The floor covers per-request overhead, and it binds on anything under 3 minutes — a 5-second clip costs the same $0.01 as a 3-minute one. If you are sending many short utterances (a live voice agent, say), concatenate them into fewer, longer requests to pay the per-second rate rather than the floor. See pricing.

POST /transcriptions — Submit Transcription

Submit audio and get a job ID back immediately. Poll /transcriptions/{id} for the results: transcription, speaker diarization, and non-speech events.

Send the audio in one of two ways:

  • The file itself as the request body — any common audio format, up to 100 MB.
  • A URL to a file you already host (S3, GCS, a CDN): JSON {"url": "https://..."}, up to 1 GiB. We fetch it, so it must be publicly reachable or presigned, and served with a Content-Length.

Parameters

Query parameters for a file upload; JSON fields alongside url for a URL submit.

ParameterTypeDefaultDescription
non_speechbooleanfalseInclude non-speech sounds
filenamestring"audio" (upload), the URL's file name (URL)Name shown in your transcription list

Submitting a file you have already transcribed with the same settings completes at once from the earlier result, at no charge.

Request

Upload a file:

curl -X POST "https://api.fluxions.ai/transcriptions?filename=meeting.mp3" \
-H "Authorization: YOUR_API_KEY" \
--data-binary @meeting.mp3

Or submit a URL:

curl -X POST "https://api.fluxions.ai/transcriptions" \
-H "Authorization: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://my-bucket.s3.amazonaws.com/meeting.mp3", "non_speech": true}'

Response

POST /transcriptions · response
{
"id": 124,
"status": "submitted",
"created_at": "2026-10-03T10:35:00.000000",
"filename": "meeting.mp3",
"non_speech": false,
"query_urls": {
"get_transcription": "https://api.fluxions.ai/transcriptions/124",
"list_transcriptions": "https://api.fluxions.ai/transcriptions"
}
}

Errors

StatusWhen
400Empty body, a URL that isn't http(s), or a URL that didn't return the file
402Your balance is below the $0.01 minimum
411The upload was sent without a Content-Length (e.g. a chunked stream) — send the file or its bytes
413The file is over 100 MB (upload) or 1 GiB (URL)

The charge itself is made when processing starts, once the audio's duration is known. If your balance can't cover it, the job ends with status: "failed" and an error_message saying how much was needed.

Workflow

  1. Submit audio to POST /transcriptions and receive a job ID
  2. Poll /transcriptions/{id} to check status
  3. When status is "completed", retrieve full results

GET /transcriptions/{id} — Get Transcription Results

Retrieve the full results for a specific job: transcription, speaker diarization, and non-speech events.

Parameters

ParameterTypeDefaultDescription
word_level_timestampsbooleanfalseInclude each segment's words, with per-word start/end

Request

curl "https://api.fluxions.ai/transcriptions/124" \
-H "Authorization: YOUR_API_KEY"

Response

GET /transcriptions/{id} · response
{
"id": 124,
"status": "completed",
"created_at": "2025-10-24T10:35:00.000Z",
"updated_at": "2025-10-24T10:35:20.000Z",
"filename": "interview.mp3",
"audio_duration": 300.0,
"audio_format": "opus",
"processing_time": 245.5,
"language": "en",
"non_speech": false,
"num_chunks": 11,
"num_segments": 25,
"num_speakers": 2,
"text": "SPEAKER_0: Yeah, let's actually start off exactly, where we initially began.\nSPEAKER_1: Sounds perfect. That makes complete sense to me.\nSPEAKER_0: So I started thinking about what if this is just a construct?",
"segments": [
{
"speaker": "0",
"text": "Yeah, let's actually start off exactly, where we initially began.",
"start": 0.86,
"end": 6.42,
"segment_idx": 0
},
{
"speaker": "1",
"text": "Sounds perfect",
"start": 6.0,
"end": 7.2,
"segment_idx": 0
},
{
"speaker": "1",
"text": "That makes complete sense to me.",
"start": 7.5,
"end": 9.8,
"segment_idx": 1
}
],
"audio_url": "https://...r2.cloudflarestorage.com/..."
}

Status Values

  • submitted — Job has been submitted
  • processing — Transcription in progress
  • completed — Transcription finished successfully
  • failed — Transcription failed (check error_message — e.g. undecodable audio, or a balance that couldn't cover the charge)

GET /transcriptions — List Transcriptions

List all transcriptions for your account.

Parameters

ParameterTypeDefaultDescription
limitinteger50Number of results per page (max: 100)
offsetinteger0Pagination offset

Request

curl "https://api.fluxions.ai/transcriptions?limit=10&offset=0" \
-H "Authorization: YOUR_API_KEY"

Response

GET /transcriptions · response
{
"total": 150,
"limit": 10,
"offset": 0,
"transcriptions": [
{
"id": 150,
"status": "completed",
"created_at": "2025-10-24T10:40:00.000Z",
"filename": "interview.mp3",
"audio_duration": 1800.0,
"audio_format": "opus",
"processing_time": 45.2,
"num_speakers": 2,
"num_segments": 142,
"original_audio_url": "https://...",
"language": "en"
}
]
}

Response Format

Text Field

The text field contains the full transcription with speaker labels and optional non-speech events:

  • Speaker Labels: SPEAKER_0:, SPEAKER_1:, etc. prefix each speaker's utterances
  • Line Breaks: Newlines (\n) separate different speaker turns
  • Non-speech Events: When enabled, events like [breath], [pause] appear inline

Example:

Response Format · text field
SPEAKER_0: Yeah, let's start [breath] where we began.
SPEAKER_1: Sounds good. That makes sense.
SPEAKER_0: So I was thinking about [pause] what if this is a construct?

Segments Array

The segments array provides precise timing and speaker information for each utterance:

  • speaker: Speaker ID as a string ("0", "1", etc.)
  • text: The spoken text for this segment (without non-speech events)
  • start: Start time in seconds (decimal precision)
  • end: End time in seconds (decimal precision)
  • segment_idx: Sequential index for this segment

Non-Speech Events

When non_speech=true, our listening model captures various non-speech sounds and events that provide additional context to the conversation.

Common Non-Speech Sounds

EventTagDescriptionExample Usage
Breath[breath]Audible breathing sounds...end of sentence. [breath] Now this is important.
Laugh[laugh] or hahahaLaughter - can be written as text or tagged for longer laughsOh wow! hahaha [breath] that's hilarious.
Hesitation[hesitation] or [hesitate]Unclear thinking noises or mouth sounds while pausing - not specific wordsWell [hesitation] um I'm not really sure.
Pause[pause]Unnaturally long, noticeable pause (e.g., looking something up)Let me just uh... [pause] Let me look this up.
Environment[env]Background noise or environmental soundsI was thinking [env] about what you said.
Tut[tut]Tongue click or lip smack sound[tut] That's not quite right.
Sigh[sigh]Expressive exhale sound[sigh] I suppose you're right.
Sniff[sniff]Nasal inhale or sniffing sound[sniff] Something smells good in here.
Cough[cough]Coughing soundSorry, excuse me [cough] as I was saying...

Usage Notes

  • Non-speech events are placed inline with the transcribed text
  • Events appear at their natural position in the conversation flow
  • Word elongation is marked with ellipsis: um... so... I think...
  • Emphasis on words uses asterisks: I *really* think so

Speech

Hosted VUI — expressive, low-latency text-to-speech. Send text, get back audio in a natural voice, with support for non-verbal cues like [sigh] and [laugh].

Two ways to render text:

  • HTTP (POST /v1/tts) — one request, one render. Simplest to integrate.
  • WebSocket (/v1/tts/ws) — keep a warm socket open across renders so each one skips the TLS/TCP handshake and reaches first audio sooner. Use this for interactive UIs.

Pricing: $9 per 1M characters (≈ $0.48 per hour of audio). See pricing.

Base URL

Speech is served through the unified Fluxions API gateway under the /vui namespace:

Base URL
https://api.fluxions.ai/vui

Authentication

All speech endpoints require your API key in the Authorization header — see Overview for API key setup.

Over WebSocket the key rides on the connection: send it as the Authorization header on the handshake, or as ?token=YOUR_API_KEY on the URL from a browser.

GET /voices — List Voices

List the built-in voices.

Request

curl "https://api.fluxions.ai/vui/voices" \
-H "Authorization: YOUR_API_KEY"

Response

GET /voices · response
{
"voices": [
{ "voice_id": "maeve.h8ff7e07da", "preview_text": "Hi there. Here's a quick sample of my voice — nice to meet you." },
{ "voice_id": "abraham.h8ff7e07da", "preview_text": "Hi there. Here's a quick sample of my voice — nice to meet you." },
{ "voice_id": "harry.h8ff7e07da", "preview_text": "Hi there. Here's a quick sample of my voice — nice to meet you." }
]
}

Pass any voice_id as the voice field when rendering. preview_text is what that voice's sample says: GET /vui/voices/<voice_id>/sample.wav.

POST /v1/tts — Render (HTTP)

Synthesize speech from text. Returns a complete WAV by default, or streams audio chunk-by-chunk when stream=1.

Parameters

JSON body:

ParameterTypeDefaultDescription
voicestring(required)A voice_id from GET /voices
inputstring(required)Text to speak. Supports non-verbal cues (see below)
response_formatstring"wav""wav" (complete file) or "pcm" (raw s16le @ 24 kHz)
streambooleanfalseStream audio as it's generated instead of buffering the whole file
max_secsfloat(auto)Hard ceiling on output length. Auto-estimated from text length when omitted
verify_chunksbooleanfalseRe-checks each rendered chunk with a fast speech-to-text pass and re-renders any that misread the text. Improves reliability at the cost of latency; leave it off for the lowest-latency stream (see Streaming)

Request

curl -X POST "https://api.fluxions.ai/vui/v1/tts" \
-H "Authorization: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"voice": "maeve.h8ff7e07da", "input": "[sigh] fine, I will say it one more time."}' \
--output speech.wav

Response

200 OK with the audio bytes. Content-Type is audio/wav (or audio/L16 when response_format is "pcm").

Streaming

Add stream=1 (query param or body field) to receive audio as it's generated, delivered as chunked transfer encoding.

By default each chunk streams the instant the model produces it, so first bytes land within ~80 ms. Set verify_chunks: true to have each chunk checked — and re-rendered if it misreads the text — before it streams: first audio then lands once the first chunk is rendered and verified (~0.5–1 s for a typical sentence), and later chunks are checked while the next one renders.

curl -X POST "https://api.fluxions.ai/vui/v1/tts?stream=1" \
-H "Authorization: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"voice": "maeve.h8ff7e07da", "input": "Streaming starts playing almost immediately."}' \
--output speech.wav

WebSocket /v1/tts/ws — Render (warm socket)

Identical render logic to POST /v1/tts, but the socket stays open between renders. Hold it open and the TLS/TCP/tunnel handshake is paid once — each subsequent speak goes straight to synthesis. Ideal for typing UIs or back-to-back lines.

Audio is delivered as binary frames of s16le PCM, mono, 24 kHz (no WAV header — assemble it yourself if you need a file).

Protocol

Client → server (text JSON):

WebSocket /v1/tts/ws · protocol
{ "type": "speak", "voice": "<id>", "input": "<text>", "max_secs": 0, "verify_chunks": false }
{ "type": "session.close" }

max_secs and verify_chunks are optional. verify_chunks defaults to false (lowest latency); set it true to check each chunk before it plays (see Streaming).

Authentication. The key is checked once, when the socket opens — send Authorization: YOUR_API_KEY on the handshake. Browsers can't set headers on a WebSocket, so from browser JS pass it on the URL instead: wss://api.fluxions.ai/vui/v1/tts/ws?token=YOUR_API_KEY. Every speak on that socket is then authenticated, including renders with your own cloned voices.

Server → client:

MessageMeaning
{"type": "start"}The worker stream opened — audio frames follow
(binary frame)A chunk of s16le PCM @ 24 kHz
{"type": "done"}Current render finished — socket stays open for the next speak
{"type": "error", "message": "..."}Render failed (socket stays open)

One render = one speak → start → binary PCM* → done. Send another speak on the same socket to render again.

Request

import asyncio, json, websockets
async def render(text, voice='maeve.h8ff7e07da'):
pcm = bytearray()
async with websockets.connect(
'wss://api.fluxions.ai/vui/v1/tts/ws',
additional_headers={'Authorization': 'YOUR_API_KEY'},
) as ws:
await ws.send(json.dumps({'type': 'speak', 'voice': voice, 'input': text}))
async for msg in ws:
if isinstance(msg, bytes):
pcm += msg # s16le PCM @ 24 kHz
elif json.loads(msg)['type'] == 'done':
break
return bytes(pcm)
audio = asyncio.run(render('[sigh] so you want to force me to say things.'))

Conversation context

By default every speak renders from the voice alone. For a voice agent, open the socket in conversation mode instead: the voice then hears the conversation so far — what the caller said and what it said itself — and replies in context, the way a person carries tone and pacing from one turn to the next.

Send session.update as the first message. The voice is fixed for the life of the socket.

WebSocket /v1/tts/ws · conversation context
{ "type": "session.update", "voice": "<id>", "context": true, "language": "fr" }

language is optional. The server answers {"type": "session.updated", "voice": "<id>", "context": true, "caller_audio": true} — caller_audio says whether this server uses caller audio (if false, send text only; binary frames are ignored). Then:

Client → serverMeaning
(binary frames)Optional: the caller's audio, s16le PCM, mono, 16 kHz, streamed while they speak — it's encoded as it arrives, so the reply doesn't wait for it. Send only the caller's speech (after your VAD), not an open mic.
{"type": "user", "text": "..."}Ends the caller's turn: the text (your transcript), plus any audio streamed since the voice's last reply started. Up to the last 10 s of a turn's audio is used.
{"type": "speak", "input": "..."}Your reply. Several speaks in a row (e.g. one per sentence from your LLM) form one reply until the next user.
{"type": "interrupt", "played_ms": 1200}Barge-in: stop, and keep only the part of the reply the caller actually heard. played_ms counts the reply's audio as played, across its speaks.
{"type": "reset"}Forget the conversation and start again from the voice.
{"type": "session.close"}Close.

The server replies to each speak with start, binary PCM and done, as above. An interrupt is answered with {"type": "interrupted", "kept_ms": ...}: what stays in context is cut on a phrase boundary, so kept_ms is at most played_ms. A reset is answered with {"type": "reset.done"}. Errors are {"type": "error", "code": "...", "message": "..."}. bad_request (a malformed message) and rate_limited / insufficient_credits (that speak was not rendered) leave the socket open; no_slot means there was no capacity to open the conversation, and the socket closes — retry shortly.

Messages apply in the order you send them: queue a user and three speaks back to back and they render in that order. interrupt and reset also drop any speak still waiting its turn.

WebSocket /v1/tts/ws · conversation context
async with websockets.connect('wss://api.fluxions.ai/vui/v1/tts/ws',
additional_headers={'Authorization': 'YOUR_API_KEY'}) as ws:
await ws.send(json.dumps({'type': 'session.update', 'voice': 'maeve.h8ff7e07da', 'context': True}))
await ws.send(json.dumps({'type': 'user', 'text': "Hi, I'd like to check my balance."}))
await ws.send(json.dumps({'type': 'speak', 'input': 'Of course, I can help with that.'}))
await ws.send(json.dumps({'type': 'speak', 'input': 'Could you give me the name on the account?'}))
# ... read start / PCM / done for each speak, then the next user turn

Audio the caller speaks over a reply (barge-in) belongs to their next turn: stream it, send interrupt, then user when they finish.

Caller audio or text only? Text is enough and is the safer default. Sending the caller's audio as well lets the voice react to how something was said, but a caller's voice can occasionally colour the reply.

Non-Verbal Cues

Wrap a cue in square brackets inside input and the model renders it as an expressive sound rather than reading the word aloud:

CueEffect
[sigh]Audible sigh
[laugh]Laughter
[gasp]Sharp intake of breath
[sniff]Sniffle
[cough]Cough
[hesitate]Filler / thinking sound

Example: "[gasp] you did NOT just put pineapple on that pizza! [laugh] okay, okay."

Voices

Render with any built-in voice from GET /voices, or clone a custom voice from a short reference clip. Cloned voices are private to your account — render them exactly like a built-in one, with your API key on the request.

POST /v1/voices — Clone a Voice

Upload a reference clip plus its transcript; the model encodes a private voice you can render with. Sent as multipart/form-data.

FieldTypeRequiredDescription
audiofileyesReference clip (wav/opus/etc.). A few clean seconds is enough. Max 25 MB
textstringnoExact transcript of the reference clip. Omit it and we transcribe the clip for you before cloning
namestringnoDisplay label (defaults to the filename)

Leave text out and the server runs your clip through transcription automatically — so the simplest clone is just an audio file. Pass text yourself when you want exact control over the transcript.

curl -X POST "https://api.fluxions.ai/vui/v1/voices" \
-H "Authorization: YOUR_API_KEY" \
-F "text=This is exactly what the reference clip says." \
-F "name=My Voice"

Response: { "voice_id": "u-<user>-<hash>", "name": "My Voice", "frames": 173, "seconds": 13.8 }. Pass the returned voice_id as voice in any render call (with your token).

GET /v1/voices/mine — List Your Cloned Voices

curl "https://api.fluxions.ai/vui/v1/voices/mine" \
-H "Authorization: YOUR_API_KEY"

Returns { "voices": [ { "voice_id": "u-...-ab12cd34", "name": "My Voice" } ] }.

POST /v1/voices/delete — Remove a Cloned Voice

curl -X POST "https://api.fluxions.ai/vui/v1/voices/delete" \
-H "Authorization: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"voice_id": "u-...-ab12cd34"}'

Note: cloned voices currently live in the running server's memory, not a database — they're tied to your account but are not guaranteed to survive a server restart. Re-upload if a voice_id stops resolving.

Output Format

  • Sample rate: 24,000 Hz
  • Channels: mono
  • Sample format: signed 16-bit little-endian PCM
  • HTTP wav: PCM wrapped in a standard WAV container
  • HTTP pcm / WebSocket binary frames: raw s16le PCM (no header)

History

The History API is one read-only surface over everything you've done on the platform — transcriptions, TTS renders, and voice conversations — under a single host. Use it to list, page, filter, and search your activity, and to fetch download links for the underlying audio and transcripts.

All history endpoints require authentication — see Overview for API key setup.

Base URL: https://api.fluxions.ai

One shape for everything

Every list response uses the same envelope:

One shape for everything
{
"object": "list",
"page": 1,
"limit": 20,
"total": 137,
"has_more": true,
"data": [ /* items */ ]
}

Every item carries an object field telling you its type ("transcription", "tts", or "conversation") plus its native id. To fetch one item's detail, call /history/{type}s/{id} (e.g. /history/tts/123). Timestamps are ISO-8601 UTC; costs are in US dollars.

Shared query parameters

These work on every collection (and the unified feed):

ParameterTypeDefaultDescription
pageinteger1Page number (1-based)
limitinteger20Results per page (max: 100)
orderstringdescSort by time: asc or desc
sincestring—Only items at/after this time (ISO-8601 or epoch seconds)
untilstring—Only items at/before this time (ISO-8601 or epoch seconds)

Collection-specific filters: voice (tts, conversations), status (transcriptions), type (the unified feed).

GET /history — Unified Feed

A merged, reverse-chronological feed across all three types. Filter the streams with type (comma-separated).

Parameters

ParameterTypeDefaultDescription
typestring(all)Restrict to transcription, tts, and/or conversation (csv)

(plus all shared parameters above)

Request

curl "https://api.fluxions.ai/history?limit=10&type=tts,conversation" \
-H "Authorization: YOUR_API_KEY"

Response

GET /history · response
{
"object": "list",
"page": 1, "limit": 10, "total": 84, "has_more": true,
"data": [
{ "object": "conversation", "id": "sess_abc", "created_at": "2026-06-29T10:40:00Z",
"cost_usd": null, "voice": "maeve.en-us", "duration_secs": 312.4, "turn_count": 18 },
{ "object": "tts", "id": 123, "created_at": "2026-06-29T10:32:00Z",
"cost_usd": 0.0123, "voice": "maeve.en-us", "chars": 842, "audio_secs": 58.4 }
]
}

The feed is lightweight: it does not include presigned download_urls. Use the typed collection or detail endpoints to get them.

GET /history/transcriptions — Transcription History

List your transcriptions.

Parameters

ParameterTypeDefaultDescription
statusstring—Filter by status (e.g. completed)
include_download_urlbooleanfalseInclude a presigned audio URL per item

(plus all shared parameters)

Request

curl "https://api.fluxions.ai/history/transcriptions?status=completed&limit=5" \
-H "Authorization: YOUR_API_KEY"

Response

GET /history/transcriptions · response
{
"object": "list",
"page": 1, "limit": 5, "total": 42, "has_more": true,
"data": [
{
"object": "transcription",
"id": 456,
"created_at": "2026-06-29T10:35:00Z",
"cost_usd": 0.10,
"status": "completed",
"filename": "interview.mp3",
"audio_duration_secs": 1800.0,
"audio_format": "opus",
"language": "en",
"num_speakers": 2,
"num_segments": 142
}
]
}

GET /history/transcriptions/{id} — One Transcription

Returns the full record with presigned download_url (audio), text_url, and segments_url. 404 if it isn't yours.

GET /history/transcriptions/{id}
curl "https://api.fluxions.ai/history/transcriptions/456" \
-H "Authorization: YOUR_API_KEY"

GET /history/tts — TTS Render History

List your text-to-speech renders.

Parameters

ParameterTypeDefaultDescription
voicestring—Filter by voice id
include_download_urlbooleantrueInclude a presigned Opus URL per item

(plus all shared parameters)

Request

curl "https://api.fluxions.ai/history/tts?voice=maeve.en-us&limit=10" \
-H "Authorization: YOUR_API_KEY"

Response

GET /history/tts · response
{
"object": "list",
"page": 1, "limit": 10, "total": 60, "has_more": true,
"data": [
{
"object": "tts",
"id": 123,
"created_at": "2026-06-29T10:32:00Z",
"cost_usd": 0.0123,
"voice": "maeve.en-us",
"chars": 842,
"audio_secs": 58.4,
"download_url": "https://...r2.cloudflarestorage.com/...opus"
}
]
}

GET /history/tts/{id} — One Render

Returns one render with a fresh signed download_url. 404 if it isn't yours.

GET /history/tts/{id}
curl "https://api.fluxions.ai/history/tts/123" \
-H "Authorization: YOUR_API_KEY"

GET /history/conversations — Conversation History

List your voice conversations (agent calls).

Parameters

ParameterTypeDefaultDescription
voicestring—Filter by voice id

(plus all shared parameters)

Request

curl "https://api.fluxions.ai/history/conversations?limit=10" \
-H "Authorization: YOUR_API_KEY"

Response

GET /history/conversations · response
{
"object": "list",
"page": 1, "limit": 10, "total": 23, "has_more": true,
"data": [
{
"object": "conversation",
"id": "sess_abc123",
"created_at": "2026-06-29T10:40:00Z",
"cost_usd": null,
"voice": "maeve.en-us",
"started_at": "2026-06-29T10:40:00Z",
"ended_at": "2026-06-29T10:45:12Z",
"duration_secs": 312.4,
"turn_count": 18
}
]
}

GET /history/conversations/{id} — One Conversation

Returns the session plus its turn-by-turn transcript. 404 if it isn't yours.

Parameters

ParameterTypeDefaultDescription
include_turnsbooleantrueInclude the transcript turns
include_tool_callsbooleanfalseInclude tool invocations (calendar, email, …)
turns_limitinteger500Max turns to return (max: 2000)

Request

GET /history/conversations/{id} · request
curl "https://api.fluxions.ai/history/conversations/sess_abc123?include_tool_calls=true" \
-H "Authorization: YOUR_API_KEY"

Response

GET /history/conversations/{id} · response
{
"object": "conversation",
"id": "sess_abc123",
"created_at": "2026-06-29T10:40:00Z",
"voice": "maeve.en-us",
"duration_secs": 312.4,
"turn_count": 18,
"turns": [
{ "object": "conversation_turn", "id": 9001, "session_id": "sess_abc123",
"role": "user", "text": "What's on my calendar today?", "created_at": "2026-06-29T10:40:05Z" },
{ "object": "conversation_turn", "id": 9002, "session_id": "sess_abc123",
"role": "assistant", "text": "You have two meetings...", "created_at": "2026-06-29T10:40:08Z" }
],
"tool_calls": [
{ "object": "tool_call", "id": 51, "tool": "calendar",
"args": {"range": "today"}, "result": "2 events", "created_at": "2026-06-29T10:40:07Z" }
]
}

GET /history/conversations/search — Search Turns

Full-text search across your conversation turns (Postgres websearch_to_tsquery).

Parameters

ParameterTypeDefaultDescription
qstring(required)Search query
limitinteger20Max results (max: 100)

Request

GET /history/conversations/search · request
curl "https://api.fluxions.ai/history/conversations/search?q=dentist+appointment" \
-H "Authorization: YOUR_API_KEY"

Response

GET /history/conversations/search · response
{
"object": "list",
"query": "dentist appointment",
"data": [
{ "object": "conversation_turn", "id": 9100, "session_id": "sess_def456",
"role": "user", "text": "remind me about the dentist appointment",
"created_at": "2026-06-20T14:02:00Z" }
]
}

Search across your whole history in one call. Conversation turns are matched by full text; transcriptions are matched by filename (their text lives in object storage, not the database). Results are type-tagged via object.

Parameters

ParameterTypeDefaultDescription
qstring(required)Search query
limitinteger20Max results per domain (max: 100)

Request

GET /history/search · request
curl "https://api.fluxions.ai/history/search?q=interview" \
-H "Authorization: YOUR_API_KEY"

Response

GET /history/search · response
{
"object": "list",
"query": "interview",
"data": [
{ "object": "conversation_turn", "id": 9200, "session_id": "sess_ghi",
"role": "assistant", "text": "...the interview went well...", "created_at": "2026-06-25T09:00:00Z" },
{ "object": "transcription", "id": 456, "created_at": "2026-06-29T10:35:00Z",
"status": "completed", "filename": "interview.mp3", "audio_duration_secs": 1800.0 }
]
}

Errors

Every failed request returns a JSON body with a detail field and a meaningful HTTP status:

json
{ "detail": "invalid or expired API key" }

Check the status code first — it tells you whether to fix the request, top up, or retry.

StatusMeaningRetry?
400Malformed or incomplete request bodyNo — fix the request
401Missing, invalid, or expired API keyNo — fix the key
402Out of creditsNo — top up at billing
403No Authorization header on a request that needs oneNo — send your key
404Unknown endpoint, voice, or job idNo — check the id
409That upload was already submittedNo — submit the file again
411Upload sent without a Content-LengthNo — send the file, not a stream
413File too large (100 MB upload, 1 GiB by URL)No — submit it by URL, or split it
422Body parsed, but a value failed validationNo — fix the value
429Too many requestsYes — back off and retry
502 / 503A model server is restarting or saturatedYes — retry in a moment

401 — Bad or missing key

401 — Bad or missing key
{ "detail": "invalid or expired API key" }
{ "detail": "Invalid API key" }
{ "detail": "Authentication credentials not provided" }

The key didn't resolve to an account. Check that you're sending the whole key, that it hasn't been revoked, and that the header reached us — some HTTP clients drop Authorization when following a redirect.

402 — Out of credits

Speech:

402 — Out of credits
{ "detail": "insufficient_credits" }

Transcription refuses a submit when the balance is below its $0.01 minimum, and returns the shortfall so you can show it to a user:

402 — Out of credits
{
"detail": {
"error": "insufficient_funds",
"message": "Insufficient funds. Need $0.01, have $0.00",
"balance_microcents": 400000,
"required_microcents": 1000000
}
}

Top up at billing; the same request then succeeds.

The full charge is made when a transcription starts processing, so a job longer than your balance covers ends failed, with the same message in its error_message.

To avoid hitting this at all, poll GET /billing/balance (see Overview) and top up when gated turns true — the speech 402 above carries no balance, so it tells you that you have run out but not by how much.

403 — No key on the request

403 — No key on the request
{ "detail": "human verification required" }

The request arrived with no Authorization header at all. Send your API key — see Overview.

404 — Unknown voice

404 — Unknown voice
{ "detail": "render failed: {\"detail\":\"unknown voice: maeve.<old-checkpoint>\"}" }

Voice ids are exact and include the serving checkpoint's suffix, which changes when we ship a new model. Don't hard-code one from an old integration — read the current id from GET /voices and pass it straight through.

A 404 {"detail": "Not Found"} with no further text means the path itself doesn't exist: check the service prefix (/vui, /akro) and the version segment.

400 / 422 — Bad request body

400 / 422 — Bad request body
{ "detail": "voice and input required" }

400 means a required field was absent; 422 means a field was present but out of range or the wrong type (the body then lists the offending fields). Both are deterministic — retrying without changing the request will fail identically.

429 — Rate limited

Back off and retry. Exponential backoff starting around one second is plenty; a burst that trips this will usually clear within a few seconds.

502 / 503 — Capacity

502 / 503 — Capacity
{ "detail": "generation unavailable" }

A model server is restarting or every worker is busy. These are transient: retry the identical request after a short pause rather than treating it as a bad request.

WebSocket errors

The speech WebSocket reports failures two ways.

A render that fails leaves the socket open and sends an error frame — send another speak when you've fixed the input:

WebSocket errors
{ "type": "error", "code": "insufficient_credits", "message": "insufficient credits" }

A connection that isn't authenticated is closed immediately with code 1008 (policy violation), before any speak is read. The key is checked when the socket opens, so a credential sent inside the first message is too late — put it on the handshake as the Authorization header, or on the URL as ?token=.

Cold starts

The GPU fleet powers down when idle. The first request after a quiet period is held at the gateway while a machine comes up — expect up to ~30 seconds, then normal latency for subsequent calls. Set your client timeout accordingly; a short timeout will look like a hang or a 502 when the request would have succeeded.