Skip to content

User Events (SSE)

The User API can stream workspace events to you in real time over a single long-lived HTTP connection using Server-Sent Events (SSE). This is the lowest-latency way to react to changes without polling, and — unlike webhooks — it needs no public endpoint of your own.

GET https://api.contio.ai/v1/user/events
Requirement Value
Method GET
Required scope any of meetings:read, action-items:read, calendar:read, workspace:read
Response Content-Type text/event-stream

Connecting

Send your bearer token and keep the connection open:

curl -N https://api.contio.ai/v1/user/events \
  -H "Authorization: Bearer $CONTIO_TOKEN"

On success the server immediately sends a preamble comment and then begins streaming events:

: connected

id: 1609459200000-0
event: meeting.created
data: {"type":"meeting.created","event_id":"...","workspace_id":"...","actor_user_id":"...","occurred_at":"2024-01-15T10:30:00Z","data":{}}

Event framing

Each event is a standard SSE frame with three fields:

Line Description
id: The stream ID of this event (e.g. 1609459200000-0). Use it as a resume token.
event: The event type (see the table below).
data: A JSON payload describing the event.

The data payload has a consistent shape:

{
  "type": "meeting.created",
  "event_id": "123e4567-e89b-12d3-a456-426614174000",
  "workspace_id": "123e4567-e89b-12d3-a456-426614174002",
  "actor_user_id": "123e4567-e89b-12d3-a456-426614174001",
  "occurred_at": "2024-01-15T10:30:00Z",
  "data": { }
}
Field Description
type The event type (matches the SSE event: line).
event_id UUID of the underlying domain event.
workspace_id The workspace the event belongs to.
actor_user_id The user who triggered the event. Empty for system-triggered events.
occurred_at RFC 3339 timestamp of when the event was produced.
data Optional event-specific payload; omitted when empty.

Event types

Category Types
Meeting meeting.created, meeting.updated, meeting.started, meeting.completed, meeting.deleted
Participant participant.added, participant.removed
Action item action_item.created, action_item.updated, action_item.completed, action_item.deleted
Agenda item agenda_item.created, agenda_item.updated, agenda_item.deleted
Meeting context meeting.context.created, meeting.context.processed, meeting.context.deleted
Calendar event calendar_event.created, calendar_event.updated

Access control and filtering

Opening the stream requires any of these read scopes:

  • meetings:read — opens the stream; receive meeting.*, participant.*, agenda_item.*, and meeting.context.* events.
  • action-items:read — opens the stream; receive action_item.* events.
  • calendar:read — opens the stream; receive calendar_event.* events.
  • workspace:read — admin-only; receive all event types and all actors in the workspace.

After the connection is open, events are screened in two ways:

  1. Scope screening: each event type is delivered only if your token has the matching read scope. For example, action_item.updated is not sent to a token that only has meetings:read.
  2. Actor screening: unless your token has workspace:read (admin), you receive only:
  3. Events with an empty actor_user_id (system-triggered events, e.g. auto-finalization).
  4. Events where actor_user_id matches your user ID.

This means non-admin users can open the stream with the least-privilege scope they need, while admins with workspace:read can observe activity from every user in the workspace. The actor_user_id field lets you distinguish who triggered each event.

Heartbeats

To keep the connection alive through idle-timeout proxies, the server sends a comment heartbeat every 15 seconds:

: ping

Comment lines (those beginning with :) carry no data and should be ignored by your client. Standard SSE libraries do this automatically.

Resuming after a disconnect

If your connection drops, reconnect and send the stream ID of the last event you processed in the Last-Event-ID header. The server replays every event after that ID (gap-free, no duplicates) before resuming the live stream:

curl -N https://api.contio.ai/v1/user/events \
  -H "Authorization: Bearer $CONTIO_TOKEN" \
  -H "Last-Event-ID: 1609459200000-0"

If you omit Last-Event-ID, you receive only events that occur after you connect.

Replay window

Recent events are retained for a short window per workspace, so brief reconnections resume seamlessly. After an extended outage some events may fall outside the replay window — reconcile by fetching current state via the list endpoints.

Connection limits

Concurrent event-stream connections are capped:

Scope Maximum concurrent connections
Per user 5
Per workspace 20

Exceeding a cap returns 429 Too Many Requests with Retry-After: 5 and this body:

{
  "error": "Too many concurrent event connections",
  "code": "too_many_requests",
  "request_id": "abc123xyz"
}

Distinct from request rate limiting

This concurrent-connection cap (code = too_many_requests) is separate from the per-minute request rate limit (code = rate_limit_exceeded). Both use HTTP 429.

Consuming the stream

import { EventSource } from "eventsource"; // Node polyfill; browsers have EventSource built in

const es = new EventSource("https://api.contio.ai/v1/user/events", {
  fetch: (url, init) =>
    fetch(url, { ...init, headers: { ...init.headers, Authorization: `Bearer ${token}` } }),
});

es.addEventListener("meeting.created", (e) => {
  const event = JSON.parse(e.data);
  console.log("New meeting:", event.event_id, "in", event.workspace_id);
});

es.onerror = () => console.warn("stream error; the client will auto-reconnect");

The EventSource API automatically reconnects and sends Last-Event-ID for you.

Next steps