Skip to content
Docs
opendesk.shOpen app

Events catalog

Every event opendesk emits, its visibility and its data.

Every state change that someone else should see is written as an event, in the same database transaction as the change. Events are delivered three ways: on the realtime gateway, to webhooks, and through replay with GET /v1/events?since=<seq>.

How a change reaches everyone. A request writes the change and an event row in one Postgres transaction. The relay, on the worker role, reads events in seq order and publishes each to NATS on od.events.<wid>; it also queues signed webhook deliveries. The realtime gateway pushes each event to its topics: workspace:<wid> gets every event, or only a stand-in for an event about a private team's ticket; team:<wid>:<tid> gets that team's events in full; user and contact topics get notifications and the chat widget's events. Browsers apply events, track seq on the workspace channel and fetch any gap from GET /v1/events.one Postgres transactionA requestfrom the app, a key,a job or an emailthe changetickets, messages…an event rowseq 1042Relayworker role,in seq orderod.events.<wid>NATS subjectWebhookssigned, retriedYour endpointa private team's eventsonly when grantedRealtime gatewaychecks the token,the origin and theteams claimworkspace:<wid>every event; a stand-in foran event of a private teamteam:<wid>:<tid>that private team's events,in full, to its membersuser:<uid>, contact:…notifications; the chat widgetBrowsers and appsapply each event to what is on screentrack seq on the workspace channelfetch a gap from GET /v1/eventsapply team events when they arrivequeues
One write, one event, delivered live and to webhooks. A private team's events go only to that team.
  1. A request writes its change and the event in one transaction, so an event exists exactly when its change does.
  2. The relay reads new events in seq order and publishes each to NATS on od.events.<workspace_id>. It also queues a signed delivery for each matching webhook.
  3. The realtime gateway pushes each event to the topics it names. Most events go to workspace:<wid>, which everyone in the workspace joins.
  4. An event about a ticket in a private team goes in full to team:<wid>:<tid>, which only people who can see that team’s tickets can join. workspace:<wid> gets a stand-in with the same seq and no data (type: "event.hidden"), so seq stays contiguous for everyone. Replay answers the same stand-in for events you may not see, and a webhook receives them only when it was granted that team.
{
"id": "0192f0c4-...", // UUID v7, unique
"seq": 1042, // contiguous per workspace
"workspace_id": "…",
"type": "message.created",
"visibility": "agents", // "agents" | "public"
"topics": ["workspace:<wid>", "user:<uid>", "contact:<wid>:<contact_id>"],
"data": { },
"occurred_at": "2026-10-11T09:00:00Z"
}
  • seq increases by exactly one per event within a workspace. A jump means you missed events.
  • visibility: "public" events may reach the end customer in the chat widget. Everything else is "agents": internal notes, assignment, tags, SLA and all settings.
  • topics decide realtime fan-out. Webhooks receive the envelope without topics.
  • Objects in data are the same JSON the REST API returns.
Type Visibility Extra topics Data
ticket.created agents { ticket }
ticket.updated agents user:<assignee> when the assignee changes { ticket, changes: ["status", "assignee_id", "fields", ...] }
ticket.deleted agents { ticket_id }
message.created see below contact:<wid>:<cid> when public { ticket_id, message }
message.updated agents { ticket_id, message }, for delivery status changes
activity.created agents { ticket_id, activity }
contact.created, contact.updated agents { contact }
contact.deleted agents { contact_id }
company.created, company.updated agents { company }
company.deleted agents { company_id }
tag.created, tag.updated agents { tag }
tag.deleted agents { tag_id }
tag.merged agents { tag_id, into } (records that carried it change without their own events; refetch lists)
tag_group.created, tag_group.updated agents { tag_group }
tag_group.deleted agents { tag_group_id }
custom_field.created, custom_field.updated agents { custom_field }
custom_field.deleted agents { custom_field_id, entity, key }
mailbox.created, mailbox.updated agents { mailbox }
mailbox.deleted agents { mailbox_id }
member.added, member.updated agents user:<uid> { member }, also on a picture, availability or cap change
member.removed agents user:<uid> { user_id }
workspace.updated agents { workspace }, when the name, settings, logo or a brand image changes
notification.created agents user:<uid> { notification }
saved_reply.*, view.*, automation.*, sla_policy.*, kb_article.*, kb_collection.*, team.*, webhook.* agents { <resource> } on create and update, { <resource>_id } on delete
billing.updated agents Billing, see Billing API. Cloud only.
webhook.test sent only to the webhook being tested
Message Visibility
A note agents
A reply on a chat ticket public, also sent to the contact’s topic
A reply on an email ticket agents (the customer receives it by email)

chat.typing is not stored, has no seq and never reaches webhooks. The gateway sends typing indicators directly to the ticket and contact topics. See Realtime.

AI summaries and triage results arrive as ticket.updated with custom in changes (Ollie’s decisions in custom.ai_decisions, pending suggestions in custom.ai_suggestions); fields triage applied are listed too, such as priority, severity and tags. Chat widget AI answers arrive as a normal message.created.

Events are kept for 7 days (EVENT_RETENTION on self-hosted instances) for replay, then pruned.

Edit this page on GitHub