Skip to content

Live dictation

Speech to text and streamed chat corrections over one WebSocket per session, opened with a signed ticket.

Live dictation runs speech to text and chat corrections over one WebSocket per session, without a job per request. The stream is experimental and off unless the owner switches it on (503 dictation_off). Only keys of the albastov.com owner account carry the dictation scope. The API stores no audio, no transcript and no chat content.

Flow

  1. POST /v1/dictation/sessions with an API key. The response holds session_id, ticket and endpoints.
  2. Open the url of the relay endpoint as a WebSocket. Send {"op":"auth","ticket":"<ticket>"} as the first frame within 5 seconds.
  3. Send requests and audio. Replies arrive in any order; each reply carries the request's id.
  4. After a disconnect, call POST /v1/dictation/sessions/{id}/ticket and open the socket again with the new ticket. Resend the requests that had no final reply.

Session

A session belongs to the account of the key that opened the session. client names the app and its version, at most 64 printable characters.

curl

curl https://api.albastov.com/v1/dictation/sessions \
  -H "Authorization: Bearer $ALBASTOV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"client": "iridium-live 2.4"}'
Response
{
  "session_id": "ds_k3m2c7x4q5n6p2r7s3t4u5v6",
  "ticket": "eyJhbGciOiJFZERTQSIsImtpZCI6Ii4uLiIsInR5cCI6IkpXVCJ9.eyJzZXNzaW9uX2lkIjoiLi4uIn0.c2lnbmF0dXJl",
  "expires_at": 1793567400000,
  "endpoints": [
    {"kind": "relay", "url": "wss://api.albastov.com/v1/dictation/stream?session=ds_k3m2c7x4q5n6p2r7s3t4u5v6"}
  ]
}

expires_at is the ticket's expiry in Unix milliseconds. Open each endpoint's url exactly as returned, in list order; a later version adds endpoints of other kinds to the same list.

Ticket

The ticket is a compact JWS signed with Ed25519: base64url(header).base64url(claims).base64url(signature). GET /v1/dictation/keys returns the public keys that verify tickets.

PartContent
Header{"alg":"EdDSA","kid":"<key id>","typ":"JWT"}
session_idThe session the ticket opens
key_idThe id of the API key that opened the session
expExpiry in Unix seconds, 5 minutes after issue
iat, account, device, jtiIssue time, account id, stream target, unique ticket id
  • A ticket opens one socket at a time, and only before exp. An open socket outlives its ticket.
  • A second socket with a ticket that holds an open socket fails with ticket_in_use.
  • A socket opened with a newer ticket of the same session closes the older socket with 4409 replaced.
  • A ticket older than the ticket of the open socket fails with ticket_stale.
  • The API checks the key that opened the session when a socket opens and every 30 seconds after. A revoked key, or a key without the dictation scope, closes the socket with 4401 invalid_key or 4403 scope_missing.

Stream

GET /v1/dictation/stream?session=<session_id> upgrades to a WebSocket. Never put the ticket in the URL.

AuthenticationRefusal
First frame {"op":"auth","ticket":"..."} within 5 secondsClose frame with code 4000 + status and the error code as reason
Header Authorization: Bearer <ticket> or X-Dictation-Ticket: <ticket> on the upgradeHTTP status and JSON error body; no WebSocket

After authentication, every frame passes unchanged in both directions, in order per direction. The API adds no frame of its own.

Python
import json
import os
import urllib.request

from websockets.sync.client import connect  # pip install websockets

req = urllib.request.Request(
    "https://api.albastov.com/v1/dictation/sessions",
    data=json.dumps({"client": "example"}).encode(),
    headers={"User-Agent": "my-app/1.0", "Authorization": f"Bearer {os.environ['ALBASTOV_API_KEY']}", "Content-Type": "application/json"},
)
with urllib.request.urlopen(req, timeout=30) as res:
    session = json.load(res)

with connect(session["endpoints"][0]["url"]) as ws:
    ws.send(json.dumps({"op": "auth", "ticket": session["ticket"]}))
    ws.send(json.dumps({"op": "transcribe", "id": 1, "final": True}))
    ws.send(open("phrase.pcm", "rb").read())  # 16 kHz mono PCM16 little-endian, 1 MiB at most
    print(json.loads(ws.recv()))

Messages

A client text frame is one JSON object; audio is a binary frame. id is a whole number or a string of 1 to 64 characters, unique among the requests in flight. Every request receives exactly one final reply: text, done or error.

Client sendsReply
{"op":"transcribe","id":n,"final":bool}, then one binary frame of 16 kHz mono PCM16 little-endian audio{"op":"text","id":n,"final":bool,"text":"...","model":"parakeet","model_state":"loaded","ms":{...}}
{"op":"chat","id":n,"body":{...}} with an OpenAI chat body, model and messages included{"op":"delta","id":n,"content":"..."} per token chunk, then {"op":"done","id":n,"text","finish_reason","model","usage","ms":{...}}
{"op":"cancel","id":n}{"op":"error","id":n,"code":"cancelled"} for the cancelled request
{"op":"ping","ts"?:n}{"op":"pong","ts"?:n}

ms holds received (Unix milliseconds when the request was read) and total (milliseconds from received to the reply), plus queued and inference for transcribe, and queued and ttft for chat.

Final phrases run before captions. A new caption request ("final": false) replaces the session's pending caption, and the replaced request receives error with code replaced. Send a final at each pause in speech.

Error replies

{"op":"error","id":n,"code":"...","message":"...","ms":{...}} ends one request; the socket stays open.

codeMeaning
bad_requestThe frame is not JSON, or id, final or body is missing or invalid
unknown_opop is not transcribe, chat, cancel or ping
duplicate_idA request with the same id is in flight
unexpected_audioA binary frame without a transcribe before it
bad_audioEmpty audio, or an odd number of bytes
too_largeAudio longer than 120 seconds
replacedA newer caption request took the caption's place
cancelledThe client cancelled the request
asr_unavailable, asr_failedSpeech to text is offline, or failed on the request
model_not_loadedThe chat model in body.model is not loaded
chat_failedThe chat request was refused, or its stream broke

Limits

LimitValue
Frame size1 MiB (32 seconds of audio); a larger frame closes the socket with 1009
Frames queued for speech to text1,024 frames or 4 MiB per socket, 8 MiB across all sockets; past a limit the socket closes with 1008 flood
Frames waiting for the client1,024 frames or 4 MiB, and 10 seconds per write; past a limit the socket closes with 1008 slow_reader
Open sockets4 per account; a fifth closes with 4429 too_many_streams
Socket lifetime12 hours; then the socket closes with 1001 max_lifetime
Upgrades60 a minute per client address; past the limit 429 rate_limited
KeepaliveA ping every 20 seconds; no pong within 20 seconds closes the socket with 1001 keepalive_timeout

An idle WebSocket closes after 100 seconds without traffic: send a frame at least every 90 seconds, or answer pings. {"op":"ping"} is an end-to-end check and receives {"op":"pong"}.

Close codes

CodeReasonCause
1000Normal close by the client or the server
1001dictation turned offDictation was switched off; open sockets close
1001keepalive_timeoutThe client missed a ping
1001max_lifetimeThe socket was open for 12 hours; reconnect with a new ticket
1008flood, slow_readerA frame limit above
1009A frame over 1 MiB
1011edge_error, stream_errorThe stream broke on the server; reconnect with a new ticket
1012device disconnectedSpeech to text disconnected; reconnect with a new ticket
1013device_busySpeech to text did not take a frame within 5 seconds; reconnect
4000 + statusThe error codeA refusal with the HTTP status of the same refusal on the upgrade, such as 4401 ticket_expired, 4403 scope_missing, 4408 auth_timeout, 4409 ticket_in_use, 4429 too_many_streams or 4504 device_timeout
4409replacedA newer socket of the same session opened

Every refusal code: Dictation reference.

Records

Sessions, sockets (frame and byte counts) and one row per request (operation, outcome, model, audio length, character and token counts, and the ms timings) are kept for 90 days. Audio, transcripts and chat content are never stored.

Reference