UltraVoice

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.inc Auth header: X-API-Key: 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 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:

Shell
X-API-Key: sk-YOUR_ORGANIZATION_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), not the 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://ultravoice.us.inc/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://ultravoice.us.inc/api/v1/asr/transcribe \
  -H "X-API-Key: sk-YOUR_ORGANIZATION_KEY" \
  -F [email protected] \
  -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://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

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:

  1. ?api_key=sk-… — your organization 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.

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

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)

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

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

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

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.
404KEY_NOT_FOUNDRevoke target key doesn't exist.
502ASR_UPSTREAM_ERRORThe transcription engine was unreachable or returned an error.
503ASR_NOT_CONFIGUREDSpeech-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_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.
  • 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 your sk--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.