Room Push
While a session is running, the Cortex bot sits in the ODIN room as a regular peer. Cortex uses that seat as a back channel into the room: game clients receive Cortex data over the voice connection they already hold — no extra connection, no polling, no endpoint to expose. ODIN fans each message out to the players, so pushing to a full room costs the same as pushing to one player.
What travels through the room:
| Frame | Sent when | Switched on by |
|---|---|---|
cortex.transcript | A segment of speech was transcribed (live captions) | Project setting transcriptPush |
cortex.sanction | A sanction of a player in the room changed | Project setting sanctionPush |
cortex.mutes | A peer joins — the complete list of peers it must not hear | Project setting sanctionPush |
Your own type | Your backend or function sends a message | Always available |
All of these are sent by the bot, so room push works only while a bot is in the room — that is, while a session for the room is active.
Sending your own messages
Push any JSON object to the players of a live session — a countdown, a match result, a
notification — with project.rooms.send():
Over REST this is POST /api/projects/{projectId}/sessions/{sessionId}/room-messages — see
Send a message into the ODIN room of a session. The
call answers 202 with { id, sessionId, type, sizeBytes, acceptedAt } once the bot has the
message.
| Rule | Value |
|---|---|
type | 1–64 characters: letters, digits and ._:-, starting with a letter or digit. Must not start with cortex. (reserved for Cortex’s own frames) |
data | Any JSON object |
| Size | At most 8 KB per encoded frame, otherwise 413 |
| Rate | 20 messages per second per room (shared by all senders), otherwise 429 with Retry-After |
| Targets | targetExternalUserIds (up to 64) reaches every peer of those users. Users who aren’t in the room are skipped — a list where nobody is present sends nothing; it never turns into a broadcast |
| Presence | No bot in the room → 409. The message is not queued |
API keys need the rooms.push scope (see Scopes).
From a serverless function
Inside a serverless function, ctx.cortex is already
authorized — no API key needed:
ctx.cortex.rooms.send() needs function runtime v6 or newer. If your project is pinned to an
older runtime, upgrade it from the dashboard.
Delivery is real-time but best effort: a client that was disconnected for a moment misses frames. For state a client must not miss, keep an authoritative copy it can fetch after a reconnect (for the transcript, that is the message cursor).
Receiving frames in the client
ODIN delivers every frame as an ordinary room message (MessageReceived) from the bot peer.
Since the room’s message channel is shared with your own app traffic (chat, game state), the rule
is simple: a message from the bot peer is Cortex; everything else is yours.
Recognising the bot peer
- With the
access_keytoken provider, Cortex signs the tagcortex:botinto the bot’s token. Clients see it in the peer’s tags onPeerJoined, and players cannot forge it. - With the
roomsorwebservicetoken provider, the bot joins without tags. Identify it by its user id instead (project settingbotUserId, defaulttranscription-bot). That’s fine for a trusted game, but a peer joining with the same user id could imitate the bot.
Frame format
| Bytes | Content |
|---|---|
| 0–1 | Magic 0x43 0x58 (ASCII CX) |
| 2 | Protocol version, currently 0x01 |
| 3… | UTF-8 JSON { "type": string, "id": string, "seq": number, "data": object } |
type—cortex.*for frames Cortex emits itself, otherwise thetypeyou sent.id— unique frame id. For your own messages, theidthe API returned to the sender.seq— a counter of the bot’s connection, starting at 1 when the bot joins. A gap means the client missed frames; a restart at 1 means the bot rejoined.data— the payload.
Bytes without the magic, or with an unknown version, are not Cortex frames — ignore them.
Decoding in TypeScript
Web and Node clients can use the decoder in @4players/odin-cortex (version
1.10.0 or newer). It returns null for anything that isn’t a Cortex frame:
Other engines (Unity, Unreal, native) implement the same four steps: accept only the bot peer,
check magic and version, watch seq for gaps, switch on type.
Cortex frames
cortex.transcript
One transcribed segment of one speaker, sent right after Cortex stores it — typically a few seconds after the speaker finishes a sentence.
Attach the line to peerId. seq is the transcript cursor (see below). interim is part of the
protocol for future streaming speech-to-text providers: an interim segment is later replaced by a
frame with the same segmentId. Today every segment is final.
The project setting transcriptPush decides who receives it:
| Value | Recipients |
|---|---|
off (default) | Nobody |
all | Every peer in the room — live captions |
own | Only the peer who spoke the segment |
Every client that receives transcript text can store and forward it. Turning it on is a deliberate decision — a recording announcement covers recording, not handing the text to every player.
cortex.sanction and cortex.mutes
With the project setting sanctionPush on, the bot tells the room about sanctions of players
who are in it, and enforces voice mutes on the receiving side. cortex.mutes is the complete list
of muted peers, sent to every peer that joins. Frame shapes and the client rules are described in
Moderation → Enforcement.
Configuring the settings
Both settings are part of the project settings — set them in the dashboard or with the SDK. The bot reads them when it joins a room, so a change applies from the next session.
Reconnects and gaps
Room push removes the need to poll — it doesn’t replace the REST reads. When a client
reconnects, or sees a gap in the frame seq, it catches up on the transcript through the cursor:
every cortex.transcript frame carries the transcript seq to resume from.
Limits
- Only while a bot is in the room. No active session, no push (
409for your own messages). - ODIN 1.x and 2.x both deliver frames. Receiver-side mutes need channel masks (ODIN 2.x).
- End-to-end encrypted rooms work — frames go through the bot’s cipher like everything else it sends.
Related
- Moderation — sanction frames and enforcement
- Transcription — sessions and the bot
- Serverless functions —
ctx.cortex.rooms.send() - SDK samples
- REST API: send a room message