Bot gateway
The gateway lets a bot hold one WebSocket and receive its org's events in real time, without a public URL. The bot still sends through the REST API (POST /v1/channels/{channelId}/messages, edits, reactions). A bot that can't keep a socket open, such as a serverless function, uses event subscriptions instead.
Connect
Make an app and a token for it (
msv_bot_…), and add the app to the channels it should hear. Personal tokens and sessions are refused.GET /v1/gatewaywith the token answers where to connect, the clocks, and which intents the install's scopes allow:{ "url": "wss://api.missived.com/v1/gateway", "gateway_version": 1, "heartbeat_interval": 30000, "resume_window_ms": 300000, "max_sessions": 5, "intents": [{ "id": "messages", "scope": null, "events": ["message.created", "…"], "granted": true }], "sessions": { "connected": 0 } }Open the WebSocket at
urlwith the sameAuthorization: Bearerheader. A bad token is an HTTP 401 and no socket. A client that can't set headers connects without one and puts"token"in its identify or resume frame.
Every frame is a JSON object with an op; frames you send are at most 4 KB.
server → { "op": "hello", "heartbeat_interval": 30000, "gateway_version": 1 }
client → { "op": "identify", "intents": ["messages", "message_content"] }
server → { "op": "ready", "session_id": "gws_…", "seq": 0, "org_id": "…",
"bot": { "id", "name", "handle" }, "intents": [...], "channel_ids": [...], "payload": "full" }
server → { "op": "event", "seq": 1, "id": "019…", "type": "message.created",
"payload": "full", "mentioned": true, "data": { "message": { … }, "channel": { … } } }
Identify (or resume) within 30 seconds, or the connection closes with 4003.
Heartbeat
Send { "op": "heartbeat", "seq": <last seq you processed, or null> } every heartbeat_interval milliseconds; the server answers { "op": "heartbeat_ack" }. If the server hears no heartbeat for the interval plus 15 seconds, it closes with 4009. If you get no ack for a whole interval, the connection is dead: close it and resume.
Intents
| Intent | Needs scope | Events |
|---|---|---|
messages |
none | message.created, message.updated, message.deleted |
message_content |
messages:read |
adds the text to message events (use with messages) |
reactions |
messages:read |
reaction.added, reaction.removed |
members |
members:read |
member.joined, member.left, member.role_changed, member.role_assigned, member.role_removed |
channels |
channels:read |
channel.created, channel.updated, channel.deleted |
meetings |
meetings:read |
meeting.scheduled, meeting.updated, meeting.cancelled |
shifts |
shifts:read |
shift.published |
tables |
tables:read |
sheet.row_created, sheet.row_updated, sheet.row_deleted — only Tables and shared views whose channel the bot was added to |
portals |
portals:read |
portal.request_completed, portal.file_uploaded, portal.stage_changed — none arrive today: a bot can't be added to a client portal (use an event subscription made with a personal token) |
stages |
shifts:read |
stage.scheduled, stage.updated, stage.cancelled, stage.started, stage.ended — none arrive today: a Stage belongs to a voice channel, and a bot can't be added to one (use an event subscription made with a personal token) |
bookings |
bookings:read |
booking.created, booking.rescheduled, booking.cancelled — none reach a bot: bookings:read is for people's own tokens only. Use an event subscription made with a personal token. |
An unknown intent, an empty list, or message_content alone closes with 4013. An intent the install's scopes don't allow closes with 4014, also later if an admin narrows the scopes.
What a bot receives
Events have the same names and data as event subscriptions, and:
- never a direct message, group chat or personal channel;
- message and reaction events only from channels the app was added to (or every channel it can see, with
messages:read:all); - the message text only when the message @mentions the bot (
"mentioned": true), is the bot's own, or with themessage_contentintent. Withoutmessages:read, themessagesintent still delivers the messages that mention the bot, which is enough for a bot that only answers when called; - thin payloads (
"payload": "thin") in an org that keeps text out of outgoing events; fetch the message over REST; - never an email address.
Removing the app from a channel stops that channel's events at once.
Sequence numbers and resume
seq counts the events of one session, with no gaps. Keep the last one you processed. When the connection drops, open a new one and, after hello:
client → { "op": "resume", "session_id": "gws_…", "seq": 41 }
server → every event after 41, then what happened while you were away, then
server → { "op": "resumed", "session_id": "gws_…", "seq": 57, "replayed": 16 }
A session can be resumed for 5 minutes after it was last seen, for at most 1 000 events. If it is too old or too far behind, you get { "op": "invalid_session", "resumable": false } on an open connection: identify again and catch up over REST. When the server restarts it sends { "op": "reconnect" } and closes with 1012; resume on the new connection.
Close codes
| Code | Resume? | Meaning |
|---|---|---|
| 1012 | yes | The server is restarting. |
| 4000 | yes | Something went wrong on our side. |
| 4001 | yes | Unknown op. |
| 4002 | yes | A frame wasn't a JSON object, or was over 4 KB. |
| 4003 | yes | Neither identify nor resume within 30 seconds. |
| 4004 | no | The token is missing, invalid, revoked or expired, isn't an app token, or the app was paused or uninstalled. |
| 4005 | yes | A second identify or resume on one connection. |
| 4008 | yes | Too many frames or identifies: wait, then resume. |
| 4009 | yes | No heartbeat in time. |
| 4010 | no | The session was resumed on another connection. |
| 4011 | no | The install already holds 5 connections. |
| 4013 | no | Invalid intents. |
| 4014 | no | An intent needs a scope the install doesn't have. |
A revoked token, a paused or uninstalled app, or narrowed scopes close a live connection within 30 seconds.
Limits
GET /v1/gatewayand the upgrade: 30 a minute per token.- Identify and resume: 6 a minute per bot.
- Heartbeats: 10 per 10 seconds per connection.
- 5 connections per install at once; each one receives every event.
- Sending is REST and spends the chat budget like everyone's.
The TypeScript SDK runs this loop for you: heartbeats, resume, and committing seq only after your handler succeeds.