InfraVoice

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-host Auth header: X-API-Key: usf-… (or sk-…) Model: usf-asr-en Price: $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; pass workspace_id to 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:

Shell
X-API-Key: usf-YOUR_ORG_KEY      # or a Speech-to-Text sk- key

Keys 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:

MethodEndpointPurpose
POST/api/v1/asr/keysCreate 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)

FieldRequiredDescription
fileyesThe audio file to transcribe.
modelnoModel id. Defaults to usf-asr-en.
languagenoLanguage 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:

JSON
{
  "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

Shell
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-en

Python

Python
# 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

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:

  1. ?api_key=usf-… (org key) or ?api_key=sk-… (Speech-to-Text key) — simplest for servers.
  2. ?ticket=… — a single-use 60-second ticket minted from your dashboard session, so the key/JWT never appears in the URL (recommended for browsers).
  3. ?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_format and sample_rate are 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 with 400 at 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

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)

JavaScript
// 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

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

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":"…"}}.

HTTPCodeMeaning
400INVALID_MULTIPARTRequest wasn't multipart/form-data.
400MISSING_FILENo file field was provided.
400MISSING_PARAMA required field (e.g. workspace_id on the JWT path) is missing.
401INVALID_API_KEYUnknown or revoked sk- key.
401AUTH_REQUIREDNo X-API-Key header and no bearer token.
402insufficient_creditsYour workspace credit balance is empty — top up to continue.
403WORKSPACE_FORBIDDENThe token isn't a member of the workspace.
404NOT_FOUNDRevoke target key doesn't exist, or isn't yours.
502ASR_UPSTREAM_ERRORThe transcription engine was unreachable or returned an error.
503ASR_NOT_CONFIGUREDSpeech-to-Text isn't configured on this deployment.
503TICKET_UNAVAILABLEstream-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_credits before any audio is processed.
  • Ledger: every transcription posts an asr_charge line 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-en is 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.