Skip to main content

Gatherings

A gathering is a coordination space — somewhere people are invited, gather, mark themselves ready, and optionally kick off a transcribed session. It’s pure backend: tables and API endpoints, not a 3D world.

There are three types, each tuned to a different scenario. Working with gatherings through an API key requires the gatherings scope.

The three types​

Lobby​

For game matchmaking and parties. Players join, mark themselves ready, and the host starts the gathering — which can auto-create a session so the bot joins the moment play begins.

const lobby = await project.gatherings.create({
name: "Ranked 5v5",
type: "lobby",
roomId: "ranked-5v5-room",
autoStartSession: true,
});
await lobby.addMember({ participantId: "p1", role: "owner" });
// players mark ready, then:
await lobby.start({ roomId: "ranked-5v5-room" });

Appointment​

For scheduled meetings. An appointment has startsAt / endsAt, sends email invitations with .ics calendar attachments, and can autoStartSession so it becomes a live, transcribed call at the scheduled time.

const appt = await project.gatherings.create({
name: "Quarterly Review",
type: "appointment",
startsAt: "2026-11-02T15:00:00Z",
endsAt: "2026-11-02T16:00:00Z",
timezone: "Europe/Berlin",
roomId: "quarterly-review",
autoStartSession: true,
});
await appt.sendInvitations({ participantIds: ["p2", "p3"], invitedById: "p1" });

Open​

Always-on, drop-in social spaces. People come and go using a join code; the space can be listed (public) or unlisted.

Lifecycle and the auto-started session​

A gathering moves through pending (a scheduled appointment that hasn’t begun) or active (lobbies and open spaces), then started, and finally ended or cancelled. Appointments start and end on their own when startsAt / endsAt are reached; any gathering can also be started, ended or cancelled explicitly. A gathering that has ended or was cancelled can’t be started again.

With autoStartSession: true, starting the gathering creates a transcription session for its room (gathering.sessionId). That session is tied to the gathering: ending or cancelling the gathering — explicitly or because an appointment reached endsAt — stops the session as well, so the bot leaves the room and no longer records.

await lobby.end();    // the auto-started session stops too
// or
await lobby.cancel(); // same effect on the session

Ownership​

A gathering can have an owner (a participant). Calls made by a trusted backend act with full rights. When your backend acts on behalf of a player — say, the player pressed “Start” in your game — pass that player as actorParticipantId. Cortex then accepts the change only if that participant owns the gathering:

await lobby.start({ actorParticipantId: playerParticipantId }); // 403 unless the player owns the lobby

Participants may always remove themselves.

Members & invitations​

A gathering separates who was invited from who is currently in it:

  • Invitations (gathering_invitations) are the source of truth for reaching people by email — including invitees who never sign in. This is what you’d use to email a summary afterwards.
  • Members (gathering_members) track current state with a simple lifecycle: invited → joined → ready → left, plus a role (owner / member).
const gathering = await project.gatherings.get(gatheringId);

await gathering.addMember({ participantId: "p1" });
const members = await gathering.listMembers();

// React to changes live:
gathering.watchMembers().onSnapshot((members, changes) => { /* ... */ });
gathering.watchInvitations().onSnapshot((invites) => { /* ... */ });

See Sessions & Gatherings for why invitation, membership and session attendance are tracked separately.

Join codes​

Every gathering has a short join code people can use to join — handy for sharing a lobby or open space. Codes are 8 alphanumeric characters, displayed as XXXX-XXXX and normalized on input (case-insensitive, hyphens optional), so ABCD-1234 and abcd1234 resolve to the same gathering.

// Preview a gathering before joining (e.g. to check free seats)
const preview = await project.gatherings.lookupByCode("ABCD-1234");

// Join it
const result = await project.gatherings.joinByCode({
joinCode: "ABCD1234",
participantId: "p1",
});
Join codes are bound to their project

A code only resolves inside the project it belongs to. A code from another project is answered like an unknown code (404), so the endpoints can’t be used to discover codes elsewhere. Both the lookup and the join are authenticated like every other gathering route — there is no public, unauthenticated lookup. Game clients should resolve codes through your own backend, which is also the right place for game-specific checks such as game mode, capacity or bans.

Email invitations​

When you invite people by email, Cortex sends a branded invitation. For appointments it includes a standards-compliant .ics attachment so the event lands in the recipient’s calendar, with a recording notice when autoStartSession is enabled.

You can invite by email right when you create the gathering — invitees without a userId get an invitation (and the email) but no membership:

await project.gatherings.create({
name: "Weekly Team Sync",
type: "appointment",
startsAt: "2026-11-04T09:00:00Z",
endsAt: "2026-11-04T10:00:00Z",
invitationUrl: "https://example.com/join/weekly-sync",
inviterName: "Alice Smith",
inviterEmail: "alice@example.com", // replies go to the inviter
participants: [
{ name: "Bob", email: "bob@example.com", userId: "bob-123" },
{ name: "Carol", email: "carol@example.com" },
],
});
Email is best-effort

Invitation emails are sent fire-and-forget — a delivery problem for one recipient never blocks creating the gathering or inviting others.

Lifecycle events​

Gatherings emit a rich set of domain events you can react to over webhooks, SSE or functions — for example:

  • gathering.created, gathering.started, gathering.ended, gathering.cancelled
  • gathering.member.joined, gathering.member.ready, gathering.member.left
  • gathering.invitation.sent, gathering.invitation.accepted, gathering.invitation.declined