Realtime transcription
Your backend creates a realtime transcription with its API key. The response carries a wss:// URL with a single-use ticket. A browser, or any other client, opens that socket and sends raw audio. The session sends back partial and final text while the speaker talks.
A partial updates about every 2 seconds of speech. A final arrives about 2 to 4 seconds after the speaker pauses. English and French work in the same session, and the model follows a switch between them.
The API key never leaves your backend. The client holds only the ticket.
The server keeps no transcript after close. Persist every final message yourself, for example POST it to your backend. To get a stored, diarized transcript, set finalPass: true at create.
Routes
The resource is on api.cloudraker.com only. api.paperwork.sh does not serve it.
Send a User-Agent header from server code. Some HTTP clients, such as Python’s urllib, send a default agent that our edge blocks with 403. fetch, httpx, requests, and websockets are fine.
Concepts
- Session. One live audio stream. The object is
realtime_transcriptionand the id starts withrtt_. - Ticket. A single-use capability in the socket URL. A create or re-mint ticket expires after 60 seconds.
- Resume ticket. Each
readymessage carries a new single-useresumeTicket. Use it to reconnect without a backend call. - Timeline. Timeline seconds count the audio samples that the session received. The
startandendfields use timeline seconds, not wall time. - Partial and final. A
partialshows the current guess for the open utterance. Afinalcommits a span of text and never changes. - Final pass. Opt-in. At close, the session saves the audio as a file and runs the batch diarized transcription on it.
States
A client disconnect alone does not change the state. The session stays active until 5 minutes pass without audio. That is your reconnect window.
closeReason is one of client_end, stopped, idle_timeout, max_duration, credits_exhausted, worker_unavailable, too_fast, or internal_error.
Create a session
Call create from your backend. Always send an Idempotency-Key. A retry with the same key and the same body returns the same session with a fresh ticket and the header idempotent-replay: true. The same key with a different body returns 409 idempotency_conflict.
Request body
Unknown keys return 400 invalid_request.
Example response
Only the create response carries websocket. To get another ticket, call POST /v1/realtime/transcriptions/{id}/tickets with an empty body. It returns a realtime_transcription_ticket object.
The API reference lists every field and response code.
WebSocket protocol
Open the websocket.url from create, from re-mint, or with a resumeTicket. Set binaryType to arraybuffer.
Tickets
- A ticket is 43 characters of base64url. Each ticket works once.
- A bad, used, or expired ticket closes the socket with
4401. - Each new ticket has a higher generation than the previous one. A socket with a newer ticket evicts the current client with
4409. A socket with an older ticket gets4409itself. - So a stolen ticket shows the finals so far. Your next re-mint evicts the thief for good.
Audio: client to server, binary
- Send raw PCM16 little-endian, 16 000 Hz, mono. No header and no container.
- Each binary message holds an even number of bytes, from 640 to 32 000 (20 ms to 1 s). Send 100 ms frames: 3 200 bytes.
- A shorter frame is allowed only as the last frame before
end. - Send at most 50 messages per second, binary and text together.
- Send audio at real time. The session allows 1.25 times real time with a 30-second burst. Faster audio closes the socket with
4429. - Send audio only after you receive
ready. - Frames after
endare ignored.
Control: client to server, JSON text
Text messages are at most 1 KiB. Any other text, an odd byte length, or a binary size out of range sends error with code protocol_error and closes with 4400. The session stays active: fix the client, then reconnect.
Results: server to client, JSON text
Live output has no language and no speaker fields. The model detects the language. Speaker labels come only from the final pass.
Close codes
These are WebSocket close codes, not HTTP statuses. 4402 is not HTTP 402.
Keepalive and mute
- To mute, keep the stream open and send silence. In a browser,
track.enabled = falsesends zeros through the audio pipeline. - Silence costs the same as speech. Stop the stream only to end the session.
- If you pause audio, send
pingevery 20 seconds. The session still closes after 5 minutes without audio.
Reconnect
Deploys drop every open socket. This is routine: the session state survives, so plan for reconnects.
- Reconnect only when this socket received
readyand did not receiveclosed, and the close code is in the reconnect row above. - A failure before the first
readyof the first connect is fatal. Show an error and do not retry. - Retry with backoff from 0.5 s to 5 s, with full jitter, for at most 60 seconds.
- Connect with the last
resumeTicket. On4401, ask your backend for a new ticket once. - Add
&afterSeq=<last final seq>. The session sends the stored finals with a higherseq, thenready. - Resend the audio that the session did not receive (see below), then continue live.
Resend rule. Keep a local ring of captured audio since the last ack, at most 30 seconds. Record R_last, the audioSeconds of the last ready or ack, and L_last, your local capture position at that moment. On the new ready with audioSeconds = R, resend from local position L_last + (R − R_last).
Audio you send twice is transcribed twice and billed twice. Audio you do not resend is absent from the timeline.
Stream audio
Node.js
Node 22 and later have a global WebSocket. ffmpeg -re converts any recording to PCM16 16 kHz mono at real time.
Run it with node stream.mjs call.m4a.
Python
Use httpx for the create call so that the event loop never blocks. Use websockets for the socket.
ffmpeg.stdout.read(3200) can return fewer bytes than asked. Frames under 640 bytes are allowed only as the last frame. For production, buffer to exact 3 200-byte frames, as the Node sample does.
Browser
Your backend creates the session and returns only websocket.url to the page. The page never sees the API key.
Follow these rules:
- Create the
AudioContextand callctx.resume()in the click handler, before anyawait. Do not passsampleRate. - Call
getUserMediawithchannelCount: 1and the three browser processing flags. Do not passsampleRate. The worklet resamples. - Load the worklet from a
BlobURL. - Send audio only after
ready. Keep a 30-second ring and reconnect withresumeTicket, as in Reconnect. - To mute, set
track.enabled = false. The worklet then sends zeros. - To stop, send
end, stop the tracks, and callctx.close(). Also do this onpagehide. - POST each
finalto your backend. The server keeps no transcript.
This sample omits the reconnect ring for brevity. Add it before you ship: deploys drop sockets.
Pricing
- You pay nothing when the session fails on our side. This covers
503 capacity_unavailableat create and a session where no decoder ever joins (4503). - Audio in a
gapmessage is not billed. - No minutes accrue before the first decoder joins.
- Silence costs the same as speech.
- Examples after a decoder joins: 0 s costs 5 credits, 59 s costs 7, 61 s costs 9.
- A create without an
Idempotency-Key, retried, makes two sessions. The unused one expires and pays its 5-credit fee.
GET returns billedMinutes, the minutes charged so far. The closed message carries the final count.
Final pass
Set finalPass: true at create to get a stored, diarized transcript.
- The session saves the received audio as a WAV file in your organization’s API workspace.
- At close, the batch diarized transcription runs on that file.
finalPass.fileIdnames a normal file. Read it withGET /v1/files/{fileId}.- When
finalPass.statusisready, readprocessed.jsonfrom the file’surls.json.
The file carries the standard audio transcript: ordered segments with start, end, text, speaker, and words.
The timestamps use the same timeline as the live finals.
finalPass.status is one of these values:
Billing. The existing audio meters bill the final pass, on top of the realtime price. The diarized transcription costs 20 credits per minute, with a minimum of 1 minute. A 30-minute session with finalPass costs 5 + 60 + 600 = 665 credits.
Visibility. The file is an organization file. Every API key and admin in the organization can read it through /v1/files. It stays until you delete it with DELETE /v1/files/{id}.
Limit. With finalPass, a session lasts at most 2 hours.
Limits
Data handling
Location. Montreal decode, North America session state, the final pass may run in the US; no residency guarantee.
Retention without finalPass. Audio exists only in decoder memory and in a short rolling buffer of the session. The finals stay only for reconnect replay. Both are deleted at close. Logs carry counters, codes, and ids, never audio or text.
Retention of session metadata. Ids, status, durations, billed minutes, and metadata values stay 30 days after close. Then the session is deleted.
With finalPass. The audio and the transcript are a normal file in your organization. You control it.
Security notes
- The API key stays on your backend. Give the client only
websocket.url. - Tickets appear in URLs, so each ticket works once and expires after 60 seconds.
- An API key sees only the sessions it created. A session from another key returns
404. An admin session sees every session in the organization. - A revoked API key does not end its live sessions. A live session runs until it closes, at most 4 hours. Revocation blocks new creates, tickets, and reads with that key.
- The MCP server code mode can create a session and mint tickets. It cannot stream audio.
Errors
The HTTP routes return the standard error envelope.
stop on a terminal session returns 202, not an error. For socket failures, see Close codes.