Skip to main content

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.

Typed events in the SDK

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:

interface DomainEvent<T> {
id: string; // unique id — dedupe on this
type: string; // e.g. "message.created"
source: string; // emitting service, e.g. "cortex:api", "cortex:bot", "cortex:plugin"
tenantId: string;
projectId?: string;
resourceId: string; // affected resource, e.g. "session:abc-123"
timestamp: string; // ISO-8601
payload: ActionPayload<T>;
}

Action-typed payloads​

Every payload is discriminated on an action:

ActionMeaningShape
createdA new entity{ action, data }
updatedAn entity changed{ action, data, changes } — changes holds the old values of fields that differed
deletedAn entity was removed{ action, data }
occurredA 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.

if (event.type === "session.updated" && event.payload.changes?.status !== undefined) {
const before = event.payload.changes.status;
const after = event.payload.data.status;
// react to the transition, e.g. "active" → "inactive" when a session ends
}
userData is not included in events

ODIN 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, export eventScopes plus a generic eventHandler.

  • 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 domainRequired API-key scope
    session.*, bot.*, session.annotation.createdsessions
    message.*, message.annotation.createdmessages
    participant.*participants
    gathering.*gatherings
    sanction.*sanctions or sanctions.read
    webhook.*webhooks

Catalog​

Session​

TypeActionEmitted whenpayload.data
session.createdcreatedA session is createdSession
session.updatedupdatedA session changesSession (+ changes)
session.deleteddeletedA session is deletedLast 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​

TypeActionEmitted whenpayload.data
message.createdcreatedA transcribed utterance (or a system message) is storedSee below
{
id: string; // message id
sessionId: string;
seq: number; // per-session sequence number — the transcript cursor
participantId: string | null; // null for system messages
externalUserId: string | null; // null for system messages
type?: "user" | "system"; // only set on system messages
senderName: string; // "[System]" for system messages
content: string; // the transcribed text
timestamp: string;
hasDebugAudio?: boolean; // user messages only
}

Use seq to resume after a reconnect: read anything you missed with session.listMessages({ after: lastSeenSeq }). See Transcription.

Participant​

TypeActionEmitted whenpayload.data
participant.createdcreatedA participant first appears in a projectParticipant (without userData)
participant.updatedupdatedA participant changes (display name, email, user data)Participant including the new userData (+ changes)
participant.deleteddeletedA participant is erasedThe anonymized participant (see below)
participant.joinedoccurredA participant joined a session’s roomParticipant (without userData)
participant.leftoccurredA participant left a session’s roomParticipant (without userData) plus sessionId
participant.deleted carries the scrubbed record

Erasing 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​

TypeActionEmitted whenresourceId
message.annotation.createdcreatedA plugin produced a message-scoped annotationmessage:{messageId}
session.annotation.createdcreatedA 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):

// message.annotation.created
{
annotationId: string;
messageId: string;
participantId?: string | null; // when known from the source message
externalUserId?: string | null; // when known from the source message
type: string; // e.g. "profanity"
content: Record<string, unknown>; // free-form, plugin-defined
}

// session.annotation.created
{
annotationId: string;
sessionId: string;
type: string; // e.g. "summary"
content: Record<string, unknown>;
}

See Plugins for which plugins produce which annotations.

Sanction​

TypeActionEmitted whenpayload.data
sanction.createdcreatedA moderation action is appliedSanction
sanction.updatedupdatedA sanction is modified (extended, reason changed, …)Sanction (+ changes)
sanction.revokedoccurredA sanction is lifted before it endsSanction

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​

TypeActionEmitted whenpayload.data
gathering.createdcreatedA gathering is createdGathering
gathering.updatedupdatedA gathering changesGathering (+ changes)
gathering.startedoccurredA gathering starts (optionally creating a session)Gathering
gathering.endedoccurredA gathering endsGathering
gathering.cancelledoccurredA gathering is cancelled before it startsGathering

Gathering members​

TypeActionEmitted whenpayload.data
gathering.member.joinedoccurredA member joined{ gatheringId, member }
gathering.member.readyoccurredA member marked ready (lobby){ gatheringId, member }
gathering.member.leftoccurredA member left{ gatheringId, participantId }

Gathering invitations​

TypeActionEmitted whenpayload.data
gathering.invitation.sentoccurredAn invitation was created (email may be queued)The invitation — or just { email, name } for invitations created together with the gathering
gathering.invitation.acceptedoccurredAn invitee accepted{ invitationId, gatheringId, participantId }
gathering.invitation.declinedoccurredAn invitee declined{ invitationId, gatheringId }

Bot​

TypeActionEmitted when
bot.status.changedoccurredReserved 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)​

TypeActionEmitted whenpayload.data
webhook.delivery.updatedoccurredA 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:

{
"id": "8f3c…",
"type": "message.created",
"source": "cortex:bot",
"tenantId": "…",
"projectId": "…",
"resourceId": "session:abc-123",
"timestamp": "2026-10-14T10:00:00.000Z",
"payload": {
"action": "created",
"data": {
"id": "d1e7…",
"sessionId": "abc-123",
"seq": 42,
"participantId": "…",
"externalUserId": "user-123",
"senderName": "Alex",
"content": "Hello everyone, let's get started.",
"timestamp": "2026-10-14T10:00:00.000Z"
}
}
}