events and reconnection
the gateway delivers authorized changes without polling. subscriptions reduce traffic; they never add permissions.
connection lifecycle
POST /gateway/tickets with the application token returns {url,ticket,expires_at}; tickets are single-use and expire in 60 seconds. connect to wss://sometimeschat.com/api/bots/v1/gateway with no credential in the URL. send identify within five seconds with ticket, subscriptions[] and optional resume_cursor. the server returns ready with session_id, cursor and heartbeat_interval_ms:30000. only one active gateway consumer is permitted per application in v1; a new consumer closes the old with 4009. distribute work inside your own service.
frames and acknowledgements
server frames are ready, heartbeat, event, resumed and resync_required. client frames are identify, heartbeat_ack and ack. respond to heartbeat’s nonce within 10 seconds. acknowledge the highest contiguous cursor only after durably handling every earlier event. the cursor is opaque and ordered only within this application stream. never compare its string contents. transport ACKs do not replace command acknowledgements. invalid frames close with 4002, missing identify with 4001, denied authentication with 4003 and missed heartbeat with 4008.
{
"op": "event",
"id": "evt_example",
"cursor": "opaque_cursor",
"type": "message.created",
"installation_id": "install_example",
"grant_revision": "2",
"occurred_at": "2026-10-20T07:00:00Z",
"resource_revision": "1",
"origin": {
"kind": "user",
"application_id": null
},
"data": {
"chat_id": "chat_example",
"message": {
"id": "msg_example",
"content": "hello",
"chat_id": "chat_example",
"sequence": "1",
"revision": "1",
"kind": "text",
"author": {
"kind": "user",
"id": "user_example",
"display_name": "Avery",
"avatar_url": null
},
"via_bot": null,
"edited_via_bot": null,
"created_at": "2026-10-20T07:00:00Z",
"edited_at": null,
"reply_to": null,
"attachment_ids": null,
"audience": "chat",
"expires_at": null
}
}
}delivery and replay
durable changes use an outbox and at-least-once delivery. duplicate event IDs are expected. order is guaranteed per installation/resource, not across chats or applications. retain up to 10,000 events per installation for at most 24 hours, whichever is reached first. replay always rechecks current permissions, audience, scope and blocks; deleted content is never resurrected from an event payload. lost access emits only a safe invalidation without old content. bot self-events include origin for loop prevention.
reconnect and recover
reconnect with a fresh ticket and the last durably acknowledged cursor, using exponential backoff with jitter from one second to 60 seconds. a valid cursor yields resumed then filtered replay. an expired cursor yields resync_required with scope:application, a reason and a new cursor; take authorized REST snapshots, reconcile resource revisions, then reconnect using that cursor so changes during snapshotting are replayed. keep a durable event-ID deduplication store for 24 hours. slow consumers receive resync_required then close 4010; maximum unacknowledged frames is 256.
event catalogue
lifecycle events are always available to the owning application with minimal metadata. content events require the listed read grants, including history for pre-boundary content. basic own-resource mutation receipts do not turn into an unrestricted read stream. fields in parentheses are the payload shape; shared object definitions are in the relevant resource chapter.
scroll the table horizontally to see all columns.
| event | authorization | data |
|---|---|---|
| installation.created / updated / revoked | own application; safe lifecycle metadata | Installation, or {id,reason,grant_revision} on revoke |
| application.updated / disabled | own application | {id,published_revision,status} |
| chat.access_changed | affected installation only | {chat_id,accessible,reason}; no hidden title/content |
| message.created / updated | applicable author read + history if needed | {chat_id,message:Message} |
| message.deleted | currently eligible author/category; previously visible ID | {chat_id,message_id,revision} |
| scribble.created / updated / erased | scribbles.read | {chat_id,stroke:Scribble} or {chat_id,ids,cuts,revision} |
| scribble.preview | scribbles.read; transient | {chat_id,actor,stroke,expires_at}; no replay |
| placement.created / updated / removed | scribbles.read or readable parent message | {chat_id,placement:Placement} or {chat_id,id,revision} |
| reaction.updated / pin.updated | readable target message | {chat_id,message_id,reactions} or {chat_id,message_id,pins} |
| poll.updated / closed | polls.results + readable poll | {chat_id,poll:Poll}; anonymous voters omitted |
| presence.updated | presence.read; eligible user; transient | {user_id,online,expires_at}; no replay |
| typing.started / stopped | typing.read; authorized chat; transient | {chat_id,user_id,expires_at}; no draft text |
| group.updated / members.updated | authorized group; roster only with groups.members.read | {chat_id,revision,group?} or {chat_id,members} |
| friends.updated / friend_request.updated | friends.read / friends.requests.read | {revision,refresh:true} or {request_id,revision,request} |
| profile.updated | matching profile read grant | {user_id,profile:Profile,revision} |
| settings.updated / stickers.updated | settings.write / stickers.manage | {target,patch,revision} / {sticker_id,operation,revision} |
| call.created / updated / ended | start or join grant; in-scope call | {chat_id,call:Call}; no raw SFU credentials |
| call.track.added / removed | joined session + matching media receive grant | {session_id,track:Track} or {session_id,track_id} |
| call.invitation.updated | bot.calls.invite; own proposal | {invitation_id,status,expires_at} |
| interaction.created | commands for private command invocations; bot.messages.components for component actions with the actual member actor_id | Interaction; explicit arguments only |
| game.started / ended | bot.games.start; own-started game | {chat_id,game_session_id,status}; no player game state |
revocation during delivery
uninstall and invite removal atomically revoke grants and enqueue a minimal lifecycle notification. dispatch, replay, upload completion and media authorization must consult current revocation state. purge pending content and tear down streams; do not rely on eventual cache expiry for enforcement. if a resource became invisible after a client saw it, send only an invalidation for its known ID. downloaded data cannot be recalled; developers must honor deletion and retention commitments. webhooks are not a second event transport in v1, avoiding two delivery contracts.