Domain Events
This is the catalog of events Cortex emits. The same events are delivered over webhooks, Server-Sent Events and serverless functions — so this reference applies no matter which surface you consume.
The TypeScript SDK ships a typed union of every event below, generated from
the same OpenAPI schema (GET /api/events/schema). Narrowing on event.type gives you the
exact payload.data shape.
The envelope
Every event is a DomainEvent:
Action-typed payloads
Every payload is discriminated on an action:
| Action | Meaning | Shape |
|---|---|---|
created | A new entity | { action, data } |
updated | An entity changed | { action, data, changes } — changes holds the old values of fields that differed |
deleted | An entity was removed | { action, data } |
occurred | A lifecycle moment (no create/update/delete) | { action, data } |
On updated, changes lets you see exactly what changed without keeping your own copy.
Nested objects are atomic — if any field within one changes, the whole previous nested
object is included.
userData is not included in eventsODIN peer userData can be large, so it is removed from every event — wherever it
appears, including inside nested objects and changes. The one exception is
participant.updated, whose payload.data carries the participant with the new
userData (the old value in changes is still removed). Fetch the current value when you
need it: project.participants.get(id).
Subscribing
-
Webhooks — list event names when creating a subscription. Wildcards are supported: exact (
message.created), domain (message.*) or global (*). -
Functions — export a handler named after the event (
message.created→onMessageCreated,gathering.member.joined→onGatheringMemberJoined). The subscription is derived from your exports. For wildcards, exporteventScopesplus a genericeventHandler. -
SSE — open the event stream (the SDK’s
watch()does this for you) and filter by type. An API key only receives the event domains its scopes cover:Event domain Required API-key scope session.*,bot.*,session.annotation.createdsessionsmessage.*,message.annotation.createdmessagesparticipant.*participantsgathering.*gatheringssanction.*sanctionsorsanctions.readwebhook.*webhooks
Catalog
Session
| Type | Action | Emitted when | payload.data |
|---|---|---|---|
session.created | created | A session is created | Session |
session.updated | updated | A session changes | Session (+ changes) |
session.deleted | deleted | A session is deleted | Last known session |
There is no separate “session ended” event: when a session stops — manually, by idle
timeout, or because its gathering ended or was cancelled — you receive session.updated
with status changed to inactive and endedAt set. changes.status holds the previous
status.
Message
| Type | Action | Emitted when | payload.data |
|---|---|---|---|
message.created | created | A transcribed utterance (or a system message) is stored | See below |
Use seq to resume after a reconnect: read anything you missed with
session.listMessages({ after: lastSeenSeq }). See
Transcription.
Participant
| Type | Action | Emitted when | payload.data |
|---|---|---|---|
participant.created | created | A participant first appears in a project | Participant (without userData) |
participant.updated | updated | A participant changes (display name, email, user data) | Participant including the new userData (+ changes) |
participant.deleted | deleted | A participant is erased | The anonymized participant (see below) |
participant.joined | occurred | A participant joined a session’s room | Participant (without userData) |
participant.left | occurred | A participant left a session’s room | Participant (without userData) plus sessionId |
participant.deleted carries the scrubbed recordErasing a participant anonymizes it, and the event is sent after that: data has
displayName: null and an externalUserId of the form deleted:<uuid>. Correlate on
data.id (or resourceId), which stays the same as in earlier events — don’t expect
externalUserId to identify the erased user.
participant.joined and participant.left can also be reported by the ODIN Voice server
itself (source: "voice:server"), so treat their data as an open object and rely on the
identifiers (id, externalUserId).
Annotation
| Type | Action | Emitted when | resourceId |
|---|---|---|---|
message.annotation.created | created | A plugin produced a message-scoped annotation | message:{messageId} |
session.annotation.created | created | A plugin produced a session-scoped annotation (e.g. a summary) | session:{sessionId} |
Annotation events carry a lean shape, not the full annotation record. The plugin-defined
body lives under content (so you read event.payload.data.content):
See Plugins for which plugins produce which annotations.
Sanction
| Type | Action | Emitted when | payload.data |
|---|---|---|---|
sanction.created | created | A moderation action is applied | Sanction |
sanction.updated | updated | A sanction is modified (extended, reason changed, …) | Sanction (+ changes) |
sanction.revoked | occurred | A sanction is lifted before it ends | Sanction |
The sanction always carries its projectId, plus the participantId / externalUserId it
targets and, when known, an embedded participant (without userData) — whether it was created through the API, by a plugin
(e.g. Auto-Sanction or Amanda) or from a moderator. See
Moderation.
Gathering
| Type | Action | Emitted when | payload.data |
|---|---|---|---|
gathering.created | created | A gathering is created | Gathering |
gathering.updated | updated | A gathering changes | Gathering (+ changes) |
gathering.started | occurred | A gathering starts (optionally creating a session) | Gathering |
gathering.ended | occurred | A gathering ends | Gathering |
gathering.cancelled | occurred | A gathering is cancelled before it starts | Gathering |
Gathering members
| Type | Action | Emitted when | payload.data |
|---|---|---|---|
gathering.member.joined | occurred | A member joined | { gatheringId, member } |
gathering.member.ready | occurred | A member marked ready (lobby) | { gatheringId, member } |
gathering.member.left | occurred | A member left | { gatheringId, participantId } |
Gathering invitations
| Type | Action | Emitted when | payload.data |
|---|---|---|---|
gathering.invitation.sent | occurred | An invitation was created (email may be queued) | The invitation — or just { email, name } for invitations created together with the gathering |
gathering.invitation.accepted | occurred | An invitee accepted | { invitationId, gatheringId, participantId } |
gathering.invitation.declined | occurred | An invitee declined | { invitationId, gatheringId } |
Bot
| Type | Action | Emitted when |
|---|---|---|
bot.status.changed | occurred | Reserved for transcription-bot status transitions |
bot.status.changed is part of the catalog (and has a named function handler,
onBotStatusChanged) but its data is an open object. To follow a session’s
transcription lifecycle today, watch session.updated and its status.
Webhook delivery (meta)
| Type | Action | Emitted when | payload.data |
|---|---|---|---|
webhook.delivery.updated | occurred | A webhook delivery changed status | { id, eventType, status, attemptsCount, lastError?, projectId? } |
status is pending, delivered or failed. Subscribe to it to alert on the health of
your own webhook endpoints.
Example
A message.created event delivered to a webhook or function: