Skip to main content
The Bot Gateway is a WebSocket endpoint that provides real-time events for bot applications. Bots authenticate with their bot token and receive events from channels they’ve been added to as members.

Getting Started

1

Create a Bot

Create a bot using any of these methods:
  • Web UI: Server Settings → Bots → Create Bot
  • CLI: meepachat bots create --username mybot --server <serverID>
  • API: POST /api/bots with a human user’s Bearer token
All three return a bot token — save it immediately, it’s only shown once.
2

Add Bot to a Server

The bot is automatically added to the server specified during creation. To add it to additional servers:
  • Web UI: Server Settings → Bots → Add Existing Bot
  • CLI: meepachat bots add-to-server <botID> --server <serverID>
  • API: POST /api/servers/{serverID}/bots/{botID}
3

Connect to the Gateway

Open a WebSocket connection to /api/bot-gateway using the bot token. The bot receives a ready event with its current state.
4

Handle Events

Listen for real-time events (message.created, reaction.sync, etc.) and respond via the REST API using the bot token.

Connection

Endpoint: GET /api/bot-gateway Authenticate using the Authorization header:
GET /api/bot-gateway
Authorization: Bot <bot_token>
Bot tokens follow the format <bot_id>.<secret> and are obtained when creating a bot via POST /api/bots.
const WebSocket = require('ws');

const botToken = 'bot-id.bot-secret';
const ws = new WebSocket('wss://your-meepa-instance.example.com/api/bot-gateway', {
  headers: {
    'Authorization': `Bot ${botToken}`
  }
});
Passing the token as a ?token= query parameter works but is deprecated. Use the Authorization: Bot <token> header instead.

Connection Parameters

ParameterValue
Max message size4096 bytes
Ping intervalServer sends WebSocket ping frames every 30 seconds
Pong timeoutBot must respond within 60 seconds or the connection is closed
Write deadline10 seconds
Channel subscriptionBots receive events only from channels they are members of

Ready Event

Immediately after connecting, the bot receives a ready event containing:
  • Bot user object (id, username, avatar, etc.)
  • All servers the bot is a member of, with channels the bot has been added to
  • All DM channels the bot is a participant in
This is the only time the bot receives this initialization data. Store it for the duration of the session.
{
  "type": "ready",
  "data": {
    "user": {
      "id": "bot-user-id",
      "username": "MyBot",
      "bot": true,
      "createdAt": "2026-02-14T10:00:00Z",
      "avatarUrl": null,
      "botOwnerId": "owner-user-id"
    },
    "servers": [
      {
        "id": "server-id-1",
        "name": "My Server",
        "channels": [
          {
            "id": "channel-id-1",
            "serverId": "server-id-1",
            "name": "general",
            "type": "text",
            "groupId": null,
            "position": 0,
            "createdAt": "2026-02-14T10:00:00Z"
          },
          {
            "id": "channel-id-2",
            "serverId": "server-id-1",
            "name": "bot-commands",
            "type": "text",
            "groupId": null,
            "position": 1,
            "createdAt": "2026-02-14T10:00:00Z"
          }
        ]
      }
    ],
    "dmChannels": [
      {
        "id": "dm-channel-id",
        "type": "dm",
        "memberIds": ["bot-user-id", "human-user-id"],
        "createdAt": "2026-02-14T11:00:00Z"
      }
    ]
  }
}

Client-to-Server Events

ping

Heartbeat. The server responds with a pong event.
{
  "type": "ping"
}

subscribe

Subscribe to additional channels at the WebSocket level. Use this for channels added to the bot’s membership after the initial connection, or for DM channels opened after connect. This only adds the channel to the in-memory subscription — the bot must already be a member of the channel to receive events via the REST API.
{
  "type": "subscribe",
  "data": {
    "channelIds": ["550e8400-e29b-41d4-a716-446655440000"]
  }
}

typing

Broadcast a typing indicator to a channel. The bot must be subscribed to the channel.
{
  "type": "typing",
  "data": {
    "channelId": "550e8400-e29b-41d4-a716-446655440000"
  }
}
Other clients in the channel will receive a typing event with the bot’s user ID.

Server-to-Client Events

Bots receive the same real-time events as human WebSocket clients. All events follow this structure:
{
  "type": "event_type",
  "data": { ... }
}
EventDescription
pongResponse to bot ping event
message.createdA new message was posted in a subscribed channel
message.updatedA message was edited
message.deletedA message was deleted
reaction.syncReactions were added or removed (full state)
channel.createdA new channel was created in a bot’s server
channel.updatedA channel was renamed, moved, or reordered
channel.deletedA channel was deleted
typingA user is typing in a subscribed channel
presence.initialAll currently online user IDs (sent on connection)
presence.updateA user went online or offline
server.addedBot was added to a new server (includes server and channel list)
See the WebSocket page for detailed payload documentation for each event type.

Sending Messages

Bots send messages via the standard REST API using their bot token:
curl -X POST https://your-meepa-instance.example.com/api/servers/{serverId}/channels/{channelId}/messages \
  -H "Authorization: Bot bot-id.bot-secret" \
  -H "Content-Type: application/json" \
  -d '{"content": "Hello from bot!"}'
The bot will receive its own message.created event over the gateway.

Full Example Bot (Node.js)

const WebSocket = require('ws');
const fetch = require('node-fetch');

const BOT_TOKEN = 'your-bot-id.your-bot-secret';
const BASE_URL = 'https://your-meepa-instance.example.com';

let ws;
let botUser;
let channels = new Map(); // channelId -> channel object

function connect() {
  ws = new WebSocket(`${BASE_URL}/api/bot-gateway`, {
    headers: { 'Authorization': `Bot ${BOT_TOKEN}` }
  });

  ws.on('open', () => {
    console.log('Connected to bot gateway');
  });

  ws.on('message', (data) => {
    const event = JSON.parse(data);
    handleEvent(event);
  });

  ws.on('close', () => {
    console.log('Disconnected, reconnecting in 5s...');
    setTimeout(connect, 5000);
  });

  ws.on('error', (err) => {
    console.error('WebSocket error:', err);
  });
}

function handleEvent(event) {
  switch (event.type) {
    case 'ready':
      botUser = event.data.user;
      console.log(`Bot ready: ${botUser.username} (${botUser.id})`);

      // Store channel data
      for (const server of event.data.servers) {
        for (const channel of server.channels) {
          channels.set(channel.id, channel);
        }
      }
      console.log(`Subscribed to ${channels.size} channels`);
      break;

    case 'message.created':
      handleMessage(event.data);
      break;

    case 'channel.created':
      // Bot will only receive this if it's a member of the channel
      channels.set(event.data.id, event.data);
      // Subscribe at the WebSocket level to receive real-time events
      ws.send(JSON.stringify({
        type: 'subscribe',
        data: { channelIds: [event.data.id] }
      }));
      break;

    case 'channel.deleted':
      channels.delete(event.data.channelId);
      break;
  }
}

async function handleMessage(msg) {
  // Ignore own messages
  if (msg.userId === botUser.id) return;

  const channel = channels.get(msg.channelId);
  if (!channel) return;

  console.log(`[${channel.name}] ${msg.content}`);

  // Respond to !ping command
  if (msg.content === '!ping') {
    await sendMessage(channel.serverId, channel.id, 'Pong!');
  }
}

async function sendMessage(serverId, channelId, content) {
  const res = await fetch(
    `${BASE_URL}/api/servers/${serverId}/channels/${channelId}/messages`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bot ${BOT_TOKEN}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ content })
    }
  );

  if (!res.ok) {
    console.error('Failed to send message:', await res.text());
  }
}

// Start bot
connect();

// Send heartbeat every 30s
setInterval(() => {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ type: 'ping' }));
  }
}, 30000);

Reconnection Strategy

Bots should implement reconnection with exponential backoff:
  1. Wait 1 second before the first retry
  2. Double the wait time on each failure (max 30 seconds)
  3. Reset the wait time after a successful connection
  4. After reconnecting, the server sends a new ready event with current state
  5. Use the ready event to resync channel subscriptions and local state

Lifecycle Events

When a bot is removed from a server via DELETE /api/servers/{serverID}/bots/{botID}, the server forcibly closes the bot’s gateway connection. The bot should reconnect and will receive an updated ready event reflecting the new server list.

Rate Limits

Bots share the same rate limits as human users:
LimitValue
HTTP API240 requests/minute, burst 60 (per IP)
WebSocket message size4096 bytes
Gateway connectionsOne per bot token (new connections disconnect the previous one)
The WebSocket gateway connection itself is not rate limited. Once connected, real-time events bypass the HTTP rate limiter. Only REST API calls (sending messages, etc.) count against the rate limit.

Security Best Practices

Bot tokens grant full access to all servers the bot belongs to. Treat them like passwords.
  • Keep bot tokens secret: Never commit tokens to source control or expose them in client-side code.
  • Rotate compromised tokens: If a token is leaked, regenerate it immediately via POST /api/bots/{botID}/regenerate-token.
  • Validate event data: Do not trust message content blindly. Sanitize before processing or displaying.
  • Log security events: Track unusual patterns such as spam or unauthorized access attempts.
  • Use environment variables: Store the bot token in an environment variable, not in source code.