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

  1. 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.

  2. GET /v1/gateway with 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 } }
    
  3. Open the WebSocket at url with the same Authorization: Bearer header. 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:

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

The TypeScript SDK runs this loop for you: heartbeats, resume, and committing seq only after your handler succeeds.