Speech-to-Text API
The USF Speech-to-Text (ASR) product is a high-accuracy transcription API. Unlike the rest of the platform — which is authenticated with your dashboard login (JWT) and has no user-facing API keys — Speech-to-Text is a true API-key product: you mint an organization sk- key, call a billed HTTPS endpoint (batch or streaming), and pay per audio-second from your workspace credits.
Base URL:
https://ultravoice.us.incAuth header:X-API-Key: sk-…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 uses organization-scoped secret keys:
- A key looks like
sk-followed by 48 random alphanumeric characters (51 characters total), e.g.sk-a1B2c3…(placeholder). - Each key also has a public key id (
key_…) used to identify it in lists. - The full secret is shown exactly once, at creation time — copy it then. Afterwards only a masked form (
sk-…+ last 4 characters) is ever displayed. - Secrets are encrypted at rest (AES-256-GCM) and stored only as a hash for lookup; UltraVoice cannot show you a key again after creation.
Pass the key on every transcription request in the X-API-Key header:
X-API-Key: sk-YOUR_ORGANIZATION_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), not the 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://ultravoice.us.inc/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://ultravoice.us.inc/api/v1/asr/transcribe \
-H "X-API-Key: sk-YOUR_ORGANIZATION_KEY" \
-F [email protected] \
-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://ultravoice.us.inc/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://ultravoice.us.inc/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=sk-…— your organization 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.
Mint a ticket
POST https://ultravoice.us.inc/api/v1/asr/stream-ticket (same auth as transcribe) returns { "ticket": "…", "expires_in_s": 60 }.
Connect
GET wss://ultravoice.us.inc/api/v1/asr/stream?model=usf-asr-en&ticket=…
Protocol
- Send raw PCM16, mono, 16 kHz audio as binary WebSocket frames.
- 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}.
Streaming is billed the same way as batch — per audio-second, metered from the bytes you stream.
Native streaming — Python
# Real-time streaming — native WebSocket dialect (direct server access)
# pip install websockets
import asyncio, json, wave, websockets
KEY = "YOUR_USF_ASR_SERVER_KEY"
URL = (
"wss://api-asr.us.tech/v1/audio/transcriptions/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 = "YOUR_USF_ASR_SERVER_KEY";
const url =
"wss://api-asr.us.tech/v1/audio/transcriptions/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 underlying USF ASR engine also speaks the wire formats of common transcription SDKs, so you can point an existing integration at it by changing the base URL. These dialects talk directly to the inference server (https://api-asr.us.tech) and authenticate with Authorization: Bearer <server key> — they are not billed through your platform sk- credits.
OpenAI-compatible — Python
# OpenAI-compatible — the official openai SDK works unchanged.
# pip install openai
from openai import OpenAI
client = OpenAI(
base_url="https://api-asr.us.tech/v1", # ← point the SDK here
api_key="YOUR_USF_ASR_SERVER_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://api-asr.us.tech/v1", // ← point the SDK here
apiKey: "YOUR_USF_ASR_SERVER_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 and ElevenLabs-compatible endpoints for teams migrating from those providers — 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 | KEY_NOT_FOUND | Revoke target key doesn't exist. |
| 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. |
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. - Self-hosted / inference server: transcription is served by the USF ASR inference server (
api-asr.us.tech). The platform holds a single server credential and proxies yoursk--authenticated requests to it, metering usage against your credits. The direct SDK-compatible dialects above talk to that server directly. - 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.