Speech-to-Text API
The USF Speech-to-Text (ASR) product is a high-accuracy transcription API: authenticate with your organization's usf- key — the single key that also authorizes the dashboard and every other API — call a billed HTTPS endpoint (batch or streaming), and pay per audio-second from your workspace credits. A dedicated Speech-to-Text sk- key (minted in this product) works identically, if you prefer a scoped key just for ASR.
Base URL:
https://your-infravoice-hostAuth header:X-API-Key: usf-…(orsk-…) Model:usf-asr-enPrice: $0.22 per audio-hour, metered per second
Create and manage keys in the dashboard under Products → Speech-to-Text → API Keys.
Authentication
API keys
Speech-to-Text accepts either organization-scoped key:
- Organization key (
usf-…) — your one key for the whole platform (dashboard + every API), managed in Settings → Organization key. A request with no workspace bills your organization's primary workspace; passworkspace_idto bill a specific one. - Speech-to-Text key (
sk-…) — a scoped key just for ASR, minted in this product.sk-followed by 48 random alphanumeric characters (51 characters total), e.g.sk-a1B2c3…(placeholder). Each also has a public key id (key_…) shown in lists. - Either secret is shown exactly once, at creation/rotation — copy it then. Afterwards only a masked form (last 4 characters) is displayed.
- Secrets are encrypted at rest (AES-256-GCM) and stored only as a hash for lookup; InfraVoice cannot show you a key again after creation.
Pass the key on every transcription request in the X-API-Key header:
X-API-Key: usf-YOUR_ORG_KEY # or a Speech-to-Text sk- keyKeys are workspace-scoped and billed to that workspace's credit balance. Treat them like a password — never embed them in client-side/browser code or commit them to source control. If a key leaks, revoke it (below) and mint a new one.
Key management
These endpoints are authenticated with your dashboard login (JWT) or usf- org key — not a Speech-to-Text sk- key — and are workspace-scoped:
| Method | Endpoint | Purpose |
|---|---|---|
POST | /api/v1/asr/keys | Create a key. Body: { "workspace_id": "…", "name": "…" }. Returns the plaintext secret_key once. |
GET | /api/v1/asr/keys?workspace_id=… | List keys (masked), newest first, including revoked ones. |
DELETE | /api/v1/asr/keys/{id}?workspace_id=… | Revoke a key (soft delete — it stops working immediately). |
In practice you'll create and revoke keys from the dashboard's Speech-to-Text → API Keys tab; the endpoints above are what that UI calls.
Transcribe a file (batch)
POST https://your-infravoice-host/api/v1/asr/transcribe
Send an audio file as multipart/form-data and receive the transcript synchronously.
Request (multipart form fields)
| Field | Required | Description |
|---|---|---|
file | yes | The audio file to transcribe. |
model | no | Model id. Defaults to usf-asr-en. |
language | no | Language hint (e.g. en). |
Limits
- Maximum upload size: 100 MB per request.
- Requests to the upstream engine time out at 110 seconds.
Response — the standard envelope {"success":true,"data":{…}}, where data is:
{
"text": "the full transcript …",
"model": "usf-asr-en",
"duration_seconds": 42.5,
"billed_cents": 1,
"balance_cents": 4873,
"elapsed_ms": 1830
}duration_seconds is the measured audio length used for billing; billed_cents is what this request cost; balance_cents is your remaining workspace credit.
cURL
curl -X POST https://your-infravoice-host/api/v1/asr/transcribe \
-H "X-API-Key: usf-YOUR_ORG_KEY" \
-F file=@audio.wav \
-F model=usf-asr-enPython
# Platform API — authenticate with your organization's sk- key.
# Billed per audio-second to your organization's credits.
# pip install requests
import requests
KEY = "sk-YOUR_ORGANIZATION_KEY" # created in the dashboard → Speech-to-Text → API Keys
with open("meeting.wav", "rb") as f:
res = requests.post(
"https://your-infravoice-host/api/v1/asr/transcribe",
headers={"X-API-Key": KEY},
files={"file": f},
data={"model": "usf-asr-en"},
timeout=120,
)
data = res.json()["data"]
print(data["text"])
print(f'{data["duration_seconds"]}s billed — balance {data["balance_cents"]}¢')JavaScript
// Platform API — authenticate with your organization's sk- key.
// Billed per audio-second to your organization's credits.
import fs from "node:fs";
const KEY = "sk-YOUR_ORGANIZATION_KEY"; // created in the dashboard → Speech-to-Text → API Keys
const form = new FormData();
form.append("file", new Blob([fs.readFileSync("meeting.wav")]), "meeting.wav");
form.append("model", "usf-asr-en");
const res = await fetch("https://your-infravoice-host/api/v1/asr/transcribe", {
method: "POST",
headers: { "X-API-Key": KEY },
body: form,
});
const { data } = await res.json();
console.log(data.text);
console.log(`${data.duration_seconds}s billed — balance ${data.balance_cents}¢`);Real-time streaming (WebSocket)
For live transcription, stream raw audio over a WebSocket and receive partial + final transcripts as the speaker talks.
Because browsers cannot set headers on a WebSocket, streaming authenticates via the query string. Three options:
?api_key=usf-…(org key) or?api_key=sk-…(Speech-to-Text key) — simplest for servers.?ticket=…— a single-use 60-second ticket minted from your dashboard session, so the key/JWT never appears in the URL (recommended for browsers).?token=<jwt>&workspace_id=<id>— a dashboard JWT directly.
An org key uses your primary workspace by default; add &workspace_id=<id> to bill a specific one.
Mint a ticket
POST https://your-infravoice-host/api/v1/asr/stream-ticket (same auth as transcribe) returns { "ticket": "…", "expires_in_s": 60 }.
Tickets are held in the deployment's short-lived key store, so this is the one streaming auth option that is deployment-dependent: where that store is not reachable, the mint answers 503 TICKET_UNAVAILABLE and options 1 and 3 above are the way in. Treat a 503 here as "use ?api_key=", not as an outage — the stream itself is unaffected.
Connect
GET wss://your-infravoice-host/api/v1/asr/stream?model=usf-asr-en&audio_format=pcm_s16le&sample_rate=16000&ticket=…
Protocol
- Send raw mono audio as binary WebSocket frames, in the format you declared.
audio_formatandsample_rateare required, and must describe the audio you actually send. Supported:audio_format=pcm_s16le|pcm_f32le|pcm_mulaw;sample_rate=8000|16000|22050|24000|44100|48000. Anything missing or unsupported is rejected with400at the handshake — the connection is metered from these two values, so the server will not guess them.- Receive JSON transcript messages:
{"type":"transcript","text":"…","is_final":true|false}. - Send
{"type":"finalize"}to close the current turn and flush a final transcript. - On connect the server emits
{"type":"ready","model":"usf-asr-en","sample_rate":16000}echoing the rate you declared.
Streaming is billed the same way as batch — per audio-second, metered from the bytes you stream: bytes ÷ (sample_rate × bytes-per-sample).
Native streaming — Python
# Real-time streaming — native WebSocket dialect (direct server access)
# pip install websockets
import asyncio, json, wave, websockets
KEY = "usf-YOUR_ORG_KEY"
URL = (
"wss://your-infravoice-host/api/v1/asr/stream"
"?model=usf-asr-en&audio_format=pcm_s16le&sample_rate=16000"
f"&partial_results=true&language=en&api_key={KEY}"
)
async def main():
# PCM16 mono 16 kHz audio (e.g. from a WAV file or a live mic)
with wave.open("speech.wav", "rb") as w:
pcm = w.readframes(w.getnframes())
async with websockets.connect(URL, max_size=None) as ws:
async def receive():
async for msg in ws:
data = json.loads(msg)
if data.get("type") == "transcript" and data.get("is_final"):
print("FINAL:", data["segment"]["text"])
recv = asyncio.create_task(receive())
# stream in 50 ms chunks, like a live microphone
for i in range(0, len(pcm), 1600):
await ws.send(pcm[i : i + 1600])
await asyncio.sleep(0.05)
# your app decides when the turn ends:
await ws.send(json.dumps({"type": "finalize"}))
await asyncio.sleep(2) # wait for the final transcript
await ws.send(json.dumps({"type": "done"}))
recv.cancel()
asyncio.run(main())Native streaming — JavaScript (browser mic)
// Real-time streaming from the browser microphone — native dialect
const KEY = "usf-YOUR_ORG_KEY";
const url =
"wss://your-infravoice-host/api/v1/asr/stream" +
"?model=usf-asr-en&audio_format=pcm_s16le&sample_rate=16000" +
"&partial_results=true&language=en&api_key=" + KEY;
const ws = new WebSocket(url);
ws.onmessage = (e) => {
const data = JSON.parse(e.data);
if (data.type === "transcript" && data.is_final) {
console.log("FINAL:", data.segment.text);
}
};
// Microphone → PCM16 mono 16 kHz → WebSocket
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const ctx = new AudioContext();
const source = ctx.createMediaStreamSource(stream);
const proc = ctx.createScriptProcessor(4096, 1, 1);
proc.onaudioprocess = (e) => {
if (ws.readyState !== WebSocket.OPEN) return;
const f32 = e.inputBuffer.getChannelData(0);
const ratio = ctx.sampleRate / 16000;
const out = new Int16Array(Math.round(f32.length / ratio));
for (let i = 0; i < out.length; i++) {
const s = Math.max(-1, Math.min(1, f32[Math.floor(i * ratio)]));
out[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
}
ws.send(out.buffer);
};
source.connect(proc);
proc.connect(ctx.destination);
// when your app decides the user finished speaking:
function endTurn() {
ws.send(JSON.stringify({ type: "finalize" }));
}Drop-in SDK compatibility
The platform also speaks the wire formats of common transcription SDKs, so you can point an existing integration at it just by changing the base URL to our endpoint. These dialects go through the platform (https://your-infravoice-host/api/v1/asr/v1) and authenticate with your usf- organization key (or a Speech-to-Text sk- key) — and are billed to your credits exactly like every other call. You never call the inference server directly.
OpenAI-compatible — Python
# OpenAI-compatible — the official openai SDK works unchanged.
# pip install openai
from openai import OpenAI
client = OpenAI(
base_url="https://your-infravoice-host/api/v1/asr/v1", # ← point the SDK here
api_key="usf-YOUR_ORG_KEY",
)
with open("meeting.mp3", "rb") as f:
result = client.audio.transcriptions.create(
model="usf-asr-en",
file=f,
)
print(result.text)OpenAI-compatible — JavaScript
// OpenAI-compatible — the official openai SDK works unchanged.
// npm install openai
import OpenAI from "openai";
import fs from "node:fs";
const client = new OpenAI({
baseURL: "https://your-infravoice-host/api/v1/asr/v1", // ← point the SDK here
apiKey: "usf-YOUR_ORG_KEY",
});
const result = await client.audio.transcriptions.create({
model: "usf-asr-en",
file: fs.createReadStream("meeting.mp3"),
});
console.log(result.text);The engine additionally offers Deepgram-compatible (POST /api/v1/asr/v1/listen, raw audio bytes as the body with an audio/* content type) and ElevenLabs-compatible (POST /api/v1/asr/v1/speech-to-text, multipart with a file field) endpoints for teams migrating from those providers. All three dialects take the same X-API-Key credential as the rest of this page and return their provider's native response shape unchanged — see the ready-to-copy snippets for every dialect in the dashboard's Speech-to-Text tab.
Errors
Errors use the standard envelope {"success":false,"error":{"code":"…","message":"…"}}.
| HTTP | Code | Meaning |
|---|---|---|
| 400 | INVALID_MULTIPART | Request wasn't multipart/form-data. |
| 400 | MISSING_FILE | No file field was provided. |
| 400 | MISSING_PARAM | A required field (e.g. workspace_id on the JWT path) is missing. |
| 401 | INVALID_API_KEY | Unknown or revoked sk- key. |
| 401 | AUTH_REQUIRED | No X-API-Key header and no bearer token. |
| 402 | insufficient_credits | Your workspace credit balance is empty — top up to continue. |
| 403 | WORKSPACE_FORBIDDEN | The token isn't a member of the workspace. |
| 404 | NOT_FOUND | Revoke target key doesn't exist, or isn't yours. |
| 502 | ASR_UPSTREAM_ERROR | The transcription engine was unreachable or returned an error. |
| 503 | ASR_NOT_CONFIGURED | Speech-to-Text isn't configured on this deployment. |
| 503 | TICKET_UNAVAILABLE | stream-ticket only: the deployment cannot mint tickets right now — authenticate the WebSocket with ?api_key= instead. |
For streaming, pre-connection failures are returned as plain-text WebSocket handshake errors (authentication required, insufficient_credits, ASR not configured).
Billing & pricing
- Rate: $0.22 per audio-hour, metered per audio-second (not wall-clock). A 42-second clip costs
ceil(42/3600 × $0.22 × 100)= 1¢ (a 1¢ minimum applies to any non-zero audio). - Free credit: new workspaces start with free credits, so you can try transcription before adding funds.
- Pre-flight check: if your balance is empty, the request is rejected up front with 402
insufficient_creditsbefore any audio is processed. - Ledger: every transcription posts an
asr_chargeline to your workspace's billing history, with the audio duration and cost — visible under Billing.
The rate is the UltraSafe enterprise default. Batch and streaming are billed identically, on measured audio duration.
Notes
- Model:
usf-asr-enis the current English model and the default for every endpoint. - Managed inference: transcription runs on UltraSafe's managed ASR infrastructure. The platform holds the server credential and proxies your
usf-/sk--authenticated requests to it, metering usage against your credits. Every dialect — native streaming, OpenAI, Deepgram, ElevenLabs — is routed the same way through the platform; you never call the inference server directly, and your requests only ever carry your own key. - Audio format for streaming: raw PCM16, mono, 16 kHz. Batch uploads accept standard audio containers (WAV, MP3, etc.).
Ready to build? Create a key in Products → Speech-to-Text → API Keys, then start with the cURL example above.