Realtime
Cubster is the front door for a realtime voice session: it mints a short-lived connect token, a separate broker relays the audio and holds the vendor conversation, and when the call ends the broker posts the finished transcript back here. Cubster never touches the audio itself — no upload, no storage, no vendor key.
Three parties, three jobs: Cubster mints the session token and stores the transcript at the end; the broker verifies that token, opens the WebSocket, relays audio frames to the voice vendor, and captures the transcript; the client just asks Cubster for a session and then talks to the broker directly. Cubster is on the request path only at the start (mint) and the end (transcript) — the audio itself never crosses it.
API key only — the key must include the realtime service (see Authentication); a session credential can't
mint one. Optional JSON body:
name
— a display name for the resulting transcript, a string of at most 255 characters, defaulting to
"Talk <ISO minute>" (e.g. Talk 2026-08-28T15:03). Returns 201.
Hand endpoint and token straight to the client that opens the WebSocket — it sends the token as
Authorization: Bearer <token> on the upgrade request to endpoint. What happens on the WebSocket after that — audio framing, control events, close codes — is the
broker's own wire contract, not Cubster's.
base64url(JSON payload) + "." + base64url(Ed25519 signature over the payload segment). Cubster signs with a private key only it holds (REALTIME_SIGNING_KEY); the broker verifies with the matching public key and cannot mint tokens itself. It authorizes
exactly one WebSocket connect and expires 60 seconds after it's minted; nothing in it is secret, the
signature is what matters.
| 401 | Missing or invalid API key. |
| 403 | The key doesn't include the 'realtime' service (standard forbiddenService body), or the request used a session credential instead of a key. |
| 400 | name is longer than 255 characters. |
| 503 | Realtime is not configured (REALTIME_ENDPOINT / REALTIME_SIGNING_KEY / REALTIME_BROKER_SECRET unset). |
Broker-only. Auth is
Authorization: Bearer <REALTIME_BROKER_SECRET> — a plain shared secret that gates this route only (it proves nothing about identity), compared
timing-safe, never an API key and never logged. An API key presented here is simply rejected, the same
as any wrong bearer value. :id must be a UUID. Identity — workspace, profile, display name — is not read from the body; it comes from token, the same signed session token minted by POST /api/v1/realtime/sessions, which the broker echoes back here. The token's 60-second exp is not enforced on this route (a session can run for minutes), but its iat must be no more than 24 hours old, and its sessionId must match :id.
text is optional — when omitted it's derived by joining segments[].text with a space. status is completed or failed (a session that died before any turn is still stored — an empty failed transcript is a useful trace).
A second POST for the same session id is rejected as a duplicate.
| 401 | Missing/wrong bearer secret, or token missing, malformed, tampered, or older than 24 hours. |
| 403 | The token's session id doesn't match :id. |
| 400 | Body failed validation — the message names the field. |
| 404 | :id isn't a UUID, or the token's workspace doesn't exist. |
| 409 | A transcript for this session id already exists. |
| 503 | Realtime is not configured. |
A successful post to the transcript endpoint returns 201 with the same v1 Transcript JSON shape (documented on the
Transcription page) that a batch upload produces, with these differences: no audio asset, so assetId is null and every source.* field but kind/durationSeconds is null too; words is always []; and realtimeSessionId is present (it's absent on an uploaded transcript).
It shows up in GET /api/v1/transcripts and GET /api/v1/transcripts/:id exactly like an uploaded one — there's no separate realtime listing endpoint. On the dashboard,
a live transcript shows a LIVE tag instead of the
audio player.
Sessions route: 401 / 403 (standard forbiddenService body) / 503. Transcript ingest route: 401 bad secret / 400 body / 404 id or workspace / 409 duplicate / 503.
client.realtime.createSession() mints the token — key-only, same as uploads.createGrant(), throwing a client-side CubsterError (no request sent) if the client wasn't configured with an API key.
When the transcript comes back, it reads through the same transcripts.list/transcripts.get calls as an uploaded one. Gate any UI that shows an <audio> player on source.url being non-null rather than assuming source.kind === "upload".