Skip to main content

Events & Real-time

Almost everything that happens in Cortex emits a domain event: a session starts, a message is transcribed, a participant joins, a sanction is created, an invitation is accepted. These events are the backbone of real-time integrations — and they’re all drawn from a single catalog.

One catalog, three delivery surfaces​

You choose how to receive events based on what you’re building:

Webhooks

Durable, retried, signed delivery to your backend. Best for server-to-server.

Webhooks
Server-Sent Events

A live stream for dashboards and UI. The SDK’s watch() is built on this.

Functions

Run your own code on each event with named on<Event> handlers.

The event envelope​

Every event shares the same envelope:

interface DomainEvent<T> {
id: string; // unique id — dedupe on this
type: string; // e.g. "message.created"
source: string; // which service emitted it
tenantId: string;
projectId?: string;
resourceId: string; // the affected resource, e.g. "session:abc-123"
timestamp: string; // ISO-8601
payload: {
action: "created" | "updated" | "deleted" | "occurred";
data: T; // the resource state
changes?: Partial<T>; // on "updated": the previous values of changed fields
};
}

The payload is action-typed. On an updated event, changes carries the old values of the fields that changed — so you can tell what actually changed without diffing against your own copy.

See the Domain Events reference for the full list of event types and their payloads.

Server-Sent Events (SSE)​

For live UI, open an SSE stream and receive events as they happen. The simplest way is the SDK’s watch(), which loads the current state over REST and then keeps it in sync over SSE — no manual EventSource, filtering or merge logic:

const sub = project.sessions.watch();

sub.onSnapshot((sessions, changes) => {
for (const change of changes) {
console.log(change.type, change.id); // "added" | "modified" | "removed"
}
});

sub.onError((err) => console.error(err));
// later: sub.unsubscribe();

watch() works for collections and single objects, and resilient reconnection is handled for you — every reconnect re-syncs state so you’re never left stale. See the Real-time updates guide.

The raw stream​

If you’re not using the SDK, open the stream yourself:

GET /api/projects/{projectId}/events/stream?types=message.*,session.updated
X-API-Key: ots_live_...
Accept: text/event-stream
  • types (optional) is a comma-separated list of event patterns; wildcards like message.* work.
  • Authenticate with a header. API keys are never accepted as a query parameter, so a plain browser EventSource (which can’t set headers) can’t carry one — use a fetch-based SSE client instead, as the SDK does. Keep API keys on the server in any case.
  • Each SSE message carries the event type as event:, the event id as id:, and the full JSON-encoded domain event as data:. Every ~15 seconds the server also sends an event: heartbeat message — if nothing at all arrives for much longer, reconnect.
  • With an API key you only receive events for the resource families its scopes cover — e.g. message.* needs messages, sanction.* needs sanctions or sanctions.read. A key without any event-bearing scope can’t open a stream.
  • Long-lived streams opened with an API key are fully supported. Opening (or re-opening) a stream counts against the key’s rate limit, so back off between reconnects.
SSE is live, not replayed

Server-Sent Events are for now: events that happen while you’re disconnected are not replayed on reconnect. Re-read the current state after reconnecting (watch() does this for you), or poll with a cursor. If you need durable, retried delivery so nothing is missed while your service is offline, use webhooks or functions instead.

Subscribing from functions​

In a serverless function, subscribe to an event by exporting a handler named after it (message.created → onMessageCreated). The subscription is derived from your exports — there’s no list to maintain. The handler receives the domain event exactly as shown above.

exports.onMessageCreated = async (event, ctx) => {
// event.payload.data is the new message
};

Subscribing from webhooks​

Pass the event names (or wildcards) when you create a subscription:

await project.webhooks.createSubscription({
url: "https://example.com/hook",
events: ["session.*", "message.created", "gathering.invitation.sent"],
secret: webhookSecret,
});

Webhook deliveries are only created for events that belong to a project.