Features Developers About Support
Register
Developer Documentation

Build on Echoed

Full REST API with 75+ endpoints, real-time WebSocket events, OAuth2 integration, and LiveKit voice. Build moderation bots, leveling systems, polls, reaction roles, custom commands, music bots — the full Discord-style ecosystem.

Introduction

The Echoed Bot API lets you build bots that interact with servers, channels, members, messages, tasks, and calendar events. Bots authenticate with a simple token and can access any server they've been invited to.

Quick Start

  1. Create a bot account in your Settings → Bot Profile
  2. Copy your Bot API Key (starts with zbot_)
  3. Invite your bot to a server via the Bot Discovery page or directly
  4. Start making API requests with the X-Bot-Token header

Authentication

All Bot API requests require the X-Bot-Token header with your bot's API key.

HTTP Headers
X-Bot-Token: zbot_your_api_key_here
Content-Type: application/json
Keep your bot token secret. Never commit it to version control or share it publicly. If compromised, regenerate it immediately from your bot settings.

Base URL

All Bot API endpoints are prefixed with:

Base URL
https://go.echoed.gg/v1/bots

For example, to validate your token: GET https://go.echoed.gg/v1/bots/validate

Rate Limits

The Bot API enforces a per-bot sliding-window rate limit of 120 requests per minute across all endpoints. When the limit is hit, the API returns 429 Too Many Requests with a retryAfter field (seconds) in the response body.

429 Response
{
  "message": "Rate limited. Please wait.",
  "code": 429,
  "retryAfter": 12
}
For high-volume operations like message purges, prefer the messages/bulk-delete endpoint over many single deletes — one bulk request handles up to 100 messages.

Error Handling

The API returns standard HTTP status codes. Error responses include a JSON body with a message field.

Status Meaning
200Success
201Created (new resource)
400Bad Request — invalid parameters
401Unauthorized — invalid or missing token
403Forbidden — insufficient permissions
404Not Found — resource doesn't exist
409Conflict — e.g., already banned
429Too Many Requests — rate limit exceeded (see Rate Limits)
500Internal Server Error
503Service Unavailable — database/transient
Error Response
{
  "message": "Bot is not a member of this server",
  "success": false
}

Bot Profile

GET /v1/bots/validate
Validate your bot token and confirm it's active.
Response
{
  "valid": true,
  "bot_id": "67e537d200011f5275d2",
  "message": "Bot API key is valid."
}
GET /v1/bots/profile
Get your bot's profile information.
Response
{
  "id": "67e537d200011f5275d2",
  "username": "my-awesome-bot",
  "isBot": true,
  "botDescription": "A helpful server bot",
  "botCategory": "utility",
  "isPublic": true,
  "avatarUrl": "https://s3.echoed.gg/..."
}
PUT /v1/bots/profile
Dashboard endpoint — JWT auth only. Used by the bot owner from the Echoed dashboard to update marketplace metadata (description, categories, support server, version, features). For programmatic name/avatar updates, use PATCH /v1/bots/me below.
Request Body
{
  "botDescription": "Updated bot description",
  "botCategories": ["utility", "moderation"],
  "botSupportServer": "https://echoed.gg/invite/...",
  "botVersion": "1.2.0"
}
PATCH /v1/bots/me
Bot self-update via API key — change the bot's global display name and/or avatar URL. At least one field must be present. Pass an empty string for avatar to clear it. name is bounded to 32 characters; avatar URLs are bounded to 1024 characters. The bot-key lookup table is automatically synced so cached display data stays consistent.
Request Body
{
  "name": "Panda Bot",
  "avatar": "https://cdn.example.com/panda.png"
}
Response
{
  "success": true,
  "name": "Panda Bot",
  "avatar": "https://cdn.example.com/panda.png"
}

Per-Server Customization

A bot can have a different display name in each server it's a member of — the same way a regular user does. Useful for region-specific deployments ("Panda EU" vs "Panda NA"), themed servers, or matching a server's naming convention. The per-server name shadows the global one for messages, member lists, and mentions.

PATCH /v1/bots/{server_id}/nickname
Set the bot's per-server nickname. Bounded to 32 characters. The bot must already be a member of the server. Realtime broadcast: every member of the server receives a memberNicknameUpdated socket event so clients update without refetching.
Request Body
{
  "nickname": "Panda EU"
}
Response
{
  "success": true,
  "nickname": "Panda EU"
}
DELETE /v1/bots/{server_id}/nickname
Clear the bot's per-server nickname. The bot reverts to its global name in this server.
POST /v1/bots/{server_id}/server-avatar
Upload a per-server avatar for the bot. Send as multipart/form-data with a single file field. Allowed formats: png, jpg, jpeg, gif, webp. Each upload replaces the previous server avatar for this bot in this server (old objects are deleted from storage). Realtime broadcast: every member of the server receives a memberAvatarUpdated socket event.
Response
{
  "success": true,
  "path": "public/servers/server-id/members/bot-id/avatar.png",
  "url": "https://s3.echoed.gg/public/servers/.../avatar.png",
  "timestamp": 1735531200000
}
DELETE /v1/bots/{server_id}/server-avatar
Clear the bot's per-server avatar. The bot reverts to its global avatar in this server.

Member Customization

Admin-style endpoints — the bot edits other members' per-server display data. Useful for moderation bots that implement !nick @user newname style commands. Required permission on the bot: MANAGE_SERVER. The server owner cannot be modified.

Authority is the bot's responsibility. These endpoints check the bot's perm. The command-issuing user's authority should be checked by the bot before calling — for example, gating !nick on the sender holding MANAGE_SERVER.

PATCH /v1/bots/{server_id}/members/{user_id}/nickname
Set another member's per-server nickname. Bounded to 32 characters. Empty string clears (or use the DELETE variant).
Request Body
{
  "nickname": "NewName"
}
Response
{
  "success": true,
  "userId": "target-user-id",
  "nickname": "NewName"
}
DELETE /v1/bots/{server_id}/members/{user_id}/nickname
Clear another member's per-server nickname.
POST /v1/bots/{server_id}/members/{user_id}/server-avatar
Upload a per-server avatar for another member. Multipart form with a file field. Allowed formats: png, jpg, jpeg, gif, webp.
Response
{
  "success": true,
  "userId": "target-user-id",
  "path": "public/servers/.../avatar.png",
  "url": "https://s3.echoed.gg/...",
  "timestamp": 1735531200000
}
DELETE /v1/bots/{server_id}/members/{user_id}/server-avatar
Clear another member's per-server avatar.

Voice

Bots can join a voice channel and publish audio (music, TTS, soundboards, etc.). Echoed uses LiveKit as the SFU; the bot endpoint mints an AccessToken so the bot can connect via @livekit/rtc-node (or any LiveKit client SDK) and publish a microphone track.

Bot must hold CONNECT + SPEAK in the target channel (channel-scoped — overrides honored). Returned token is a 24-hour LiveKit AccessToken with canPublishSources: ['microphone']; it cannot be used for video or screen-share.

POST /v1/bots/{server_id}/voice/{channel_id}/join
Mint a LiveKit AccessToken for the bot to join the voice channel. Idempotent — repeated calls re-issue a fresh token. The call row is auto-created if no one is in the channel yet (deterministic callId = channelId).
Response
{
  "success": true,
  "url": "wss://livekit.echoed.gg",
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "room": "call-channel-id",
  "callId": "channel-id",
  "identity": "bot-user-id",
  "expiresIn": 86400
}

Connect with livekit.Room.connect(url, token), then publish a LocalAudioTrack backed by an AudioSource (48 kHz, 2 channels). Frame size: 20 ms (960 samples per channel).

POST /v1/bots/{server_id}/voice/{channel_id}/leave
Notify the backend that the bot is leaving. The actual disconnect happens client-side (Room.disconnect()); this endpoint is for bookkeeping and audit logs.

Servers

GET /v1/bots/servers
List all servers your bot has been invited to.
Response
{
  "count": 2,
  "servers": [
    {
      "serverId": "6894d3d90011f842607c",
      "serverName": "My Server",
      "serverIcon": "",
      "invitedAt": "2026-01-06T13:41:44Z",
      "invitedBy": "user-id"
    }
  ]
}
GET /v1/bots/{server_id}/info
Get detailed information about a specific server.
Response
{
  "id": "6894d3d90011f842607c",
  "name": "My Server",
  "description": "A cool server",
  "ownerId": "user-id",
  "memberCount": 42,
  "channelCount": 8,
  "iconUrl": "",
  "createdAt": "2025-08-07T16:27:05Z"
}

Channels

GET /v1/bots/{server_id}/channels
List all channels in a server.
Response
{
  "channels": [
    {
      "id": "647a23fede03765e0348",
      "name": "general",
      "type": "text",
      "description": "",
      "createdAt": "2026-01-05T11:20:43Z"
    }
  ],
  "total": 8
}
POST /v1/bots/{server_id}/channels
Create a new channel. Valid types: text, video, tasks, calendar. Requires MANAGE_CHANNELS.
Request Body
{
  "name": "bot-updates",
  "type": "text",
  "description": "Automated bot announcements",
  "isPrivate": false,
  "isNsfw": false,
  "categoryId": "category-id",
  "position": 0
}
PUT /v1/bots/{server_id}/channels/{channel_id}
Edit a channel. All fields optional — only the ones you send are updated. Requires MANAGE_CHANNELS.
Request Body
{
  "name": "renamed-channel",
  "description": "Updated description",
  "isPrivate": false,
  "isNsfw": true,
  "slowModeSeconds": 10,
  "categoryId": "new-category-id",
  "position": 2
}

slowModeSeconds is clamped to 0–21600 (6 hours).

DELETE /v1/bots/{server_id}/channels/{channel_id}
Delete a channel permanently.
PUT /v1/bots/{server_id}/channels/reorder
Move a channel to a different position or category.
Request Body
{
  "channelId": "channel-id",
  "categoryId": "category-id",
  "position": 0
}

Categories

POST /v1/bots/{server_id}/categories
Create a new channel category.
Request Body
{
  "name": "Bot Channels",
  "position": 0
}
PUT /v1/bots/{server_id}/categories/{category_id}
Edit a category's name or position.
DELETE /v1/bots/{server_id}/categories/{category_id}
Delete a category. Channels within it become uncategorized.
PUT /v1/bots/{server_id}/categories/reorder
Reorder multiple categories at once.
Request Body
{
  "categories": [
    { "categoryId": "id1", "position": 0 },
    { "categoryId": "id2", "position": 1 }
  ]
}

Members

GET /v1/bots/{server_id}/members
List server members with pagination.
ParameterTypeDescription
limitintegerMax results (default 10)
offsetintegerPagination offset
GET /v1/bots/{server_id}/members/search?q={query}
Search members by username. Query must be at least 2 characters.
GET /v1/bots/{server_id}/members/{user_id}/permissions
Get a member's resolved permissions as a list of permission names. Useful for command gating — check if the caller has e.g. MANAGE_MESSAGES before running a moderation command.

Optional query parameter?channel_id={id}: when supplied, the returned list reflects per-channel role/user overrides. Without it the response is server-level only. Use the channel-scoped form for any command that operates on a specific channel (e.g. !purge in #announcements should check MANAGE_MESSAGES there, not server-wide).

Response (server-level)
{
  "userId": "user-id",
  "serverId": "server-id",
  "permissions": [
    "VIEW_CHANNELS", "SEND_MESSAGES",
    "READ_MESSAGE_HISTORY", "CONNECT", "SPEAK"
  ]
}
Response (with ?channel_id=...)
{
  "userId": "user-id",
  "serverId": "server-id",
  "channelId": "channel-id",
  "permissions": [
    "VIEW_CHANNELS", "SEND_MESSAGES",
    "READ_MESSAGE_HISTORY", "MANAGE_MESSAGES"
  ]
}
DELETE /v1/bots/{server_id}/members/{user_id}
Remove a member from the server (no ban). Equivalent to a kick that doesn't show up as a moderation action.

Moderation

Punitive member actions: kick, ban, unban, and timeout. Each requires the matching permission on the bot. The server owner cannot be targeted.

POST /v1/bots/{server_id}/members/{user_id}/kick
Kick a member. Requires KICK_MEMBERS.
Request Body
{ "reason": "Optional reason" }
POST /v1/bots/{server_id}/members/{user_id}/ban
Ban a member. Requires BAN_MEMBERS. Returns 409 if already banned.
Request Body
{ "reason": "Reason for ban" }
POST /v1/bots/{server_id}/members/{user_id}/unban
Unban a previously banned member. Requires BAN_MEMBERS.

Timeout

Timeouts mute a member for a duration: they cannot send messages or join voice. Enforcement is server-side, so clients can't bypass it. Requires MUTE_MEMBERS.

POST /v1/bots/{server_id}/members/{user_id}/timeout
Time a member out for durationSeconds (capped at 28 days). Re-applying overwrites the previous timeout.
Request Body
{
  "durationSeconds": 300,
  "reason": "Spamming"
}
Response
{
  "success": true,
  "userId": "user-id",
  "timeoutUntil": "2026-04-28T17:25:00Z",
  "durationSeconds": 300,
  "reason": "Spamming"
}
DELETE /v1/bots/{server_id}/members/{user_id}/timeout
End a timeout early. Idempotent — clearing a non-existent timeout returns success.
GET /v1/bots/{server_id}/members/{user_id}/timeout
Read current timeout state. Returns active: false if no timeout is set or it has expired.
Response (active)
{
  "active": true,
  "userId": "user-id",
  "timeoutUntil": "2026-04-28T17:25:00Z",
  "reason": "Spamming",
  "setBy": "bot-id"
}

Roles

GET /v1/bots/{server_id}/roles
List all roles in a server.
POST /v1/bots/{server_id}/roles
Create a new role. Requires MANAGE_ROLES. permissions accepts an array of permission names or an integer bitmask.
Request Body
{
  "name": "Moderator",
  "color": "#FF5733",
  "permissions": ["SEND_MESSAGES", "MANAGE_MESSAGES", "KICK_MEMBERS"],
  "position": 5
}
PUT /v1/bots/{server_id}/roles/{role_id}
Edit an existing role. All fields optional. System and default roles (@everyone, Server Owner) cannot be edited. Requires MANAGE_ROLES.
Request Body
{
  "name": "Senior Moderator",
  "color": "#9B59B6",
  "permissions": ["MANAGE_MESSAGES", "KICK_MEMBERS", "BAN_MEMBERS"],
  "position": 8,
  "mentionable": true,
  "hoist": true
}
DELETE /v1/bots/{server_id}/roles/{role_id}
Delete a role. The role is removed from every member that had it.
GET /v1/bots/{server_id}/members/{user_id}/roles
Get all roles assigned to a specific member.
PUT /v1/bots/{server_id}/members/{user_id}/roles
Set all roles for a member (replaces existing).
Request Body
{
  "roles": ["role-id-1", "role-id-2"]
}
POST /v1/bots/{server_id}/members/{user_id}/roles/{role_id}
Add a single role to a member.
DELETE /v1/bots/{server_id}/members/{user_id}/roles/{role_id}
Remove a single role from a member.

Messages

GET /v1/bots/{server_id}/messages?channel_id={id}
Get messages from a channel. Supports limit (max 100), before, after for pagination.
GET /v1/bots/{server_id}/messages/{message_id}
Fetch a single message by ID. Useful for validating a target before pinning, polling reaction state on a poll, or fetching the source of a reply.
POST /v1/bots/{server_id}/messages/send
Send a message to a channel. content may be empty as long as embeds or attachmentIds is provided. See Rich Embeds for the embed object schema (max 10 per message).
Request Body
{
  "channelId": "channel-id",
  "content": "Hello from my bot! 🤖",
  "attachmentIds": [],
  "mentions": [],
  "replyToId": "",
  "embeds": []
}

Sending Messages with Attachments

To send a message with file attachments, upload the file first, then reference the returned fileId in your message.

POST /v1/bots/{server_id}/upload/{channel_id}
Upload a file attachment. Send as multipart/form-data with a file field. Max file size: 50 MB. Supported types: images, videos, audio, documents, archives, and code files.
Response
{
  "success": true,
  "fileId": "abc123",
  "path": "channels/server-id/channel-id/abc123.png",
  "url": "https://s3.echoed.gg/files/channels/...",
  "filename": "screenshot.png",
  "size": 184320,
  "contentType": "image/png",
  "width": 1920,
  "height": 1080,
  "blurhash": "U.P,rXfQkCbH"
}

width, height, and blurhash are only returned for image uploads.

Message with Attachment
{
  "channelId": "channel-id",
  "content": "Check out this image!",
  "attachmentIds": ["abc123"],
  "mentions": [],
  "replyToId": ""
}
PUT /v1/bots/{server_id}/messages/{message_id}
Edit a message. Bots can edit their own messages, or others' messages if they have MANAGE_MESSAGES. Either content or embeds (or both) can be supplied; omitted fields are left untouched. Pass an empty embeds array to clear all embeds while keeping content.
Request Body — text only
{
  "content": "Edited message content"
}
Request Body — live-updating embed
{
  "embeds": [
    {
      "type": "rich",
      "title": "Now playing",
      "description": "▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱\n2:14 / 6:42",
      "color": 16763688
    }
  ]
}

Edits are emitted to all server members as a message:updated realtime event so clients re-render in place — useful for progress-bar embeds in music bots.

DELETE /v1/bots/{server_id}/messages/{message_id}
Delete a message. Bots can delete their own messages, or others' if they have MANAGE_MESSAGES.
POST /v1/bots/{server_id}/channels/{channel_id}/messages/bulk-delete
Delete up to 100 messages in a single request. Messages that don't exist or aren't in the target server are silently skipped and reported in the response. Requires MANAGE_MESSAGES.
Request Body
{
  "messageIds": ["id1", "id2", "id3"]
}
Response
{
  "success": true,
  "deleted": ["id1", "id3"],
  "count": 2
}

Rich Embeds

Bots can ship up to 10 rich embeds per message — coloured cards with a title, description, fields, thumbnail/image, author and footer. Pass an embeds array on POST /v1/bots/{server_id}/messages/send. content may be empty when at least one embed is present.

Embed Object

Every field is optional except type. Use "rich" for bot-authored cards.

Embed
{
  "type": "rich",           // "rich" | "image" | "video" | "gifv" | "article" | "link" | "audio"
  "url": "https://...",      // makes the title clickable
  "title": "Card title",
  "description": "Body copy. Markdown is rendered.",
  "color": 16763688,         // 0xFFC928 — left-bar accent, decimal int
  "timestamp": "2026-04-29T12:00:00Z", // ISO-8601, rendered next to footer
  "author":    { "name": "…", "url": "…", "icon_url": "…" },
  "thumbnail": { "url": "https://…/thumb.png" },
  "image":     { "url": "https://…/banner.png" },
  "fields": [
    { "name": "Field", "value": "Value", "inline": true }
  ],
  "footer": { "text": "Footer copy", "icon_url": "…" }
}

Sub-Objects

EmbedAuthor / Provider
{
  "name": "Author or site name",
  "url": "https://…",
  "icon_url": "https://…/avatar.png"
}
EmbedMedia (thumbnail / image / video / audio)
{
  "url": "https://…/asset",
  "width": 1280,
  "height": 720
}
EmbedField
{
  "name": "Heading",
  "value": "Body — markdown supported.",
  "inline": false
}
EmbedFooter
{
  "text": "3 active",
  "icon_url": "https://…/icon.png"
}

Example: Embed-Only Message

Empty content with one rich embed — the canonical pattern for status / list / leaderboard cards.

POST /v1/bots/{server_id}/messages/send
{
  "channelId": "channel-id",
  "content": "",
  "embeds": [
    {
      "type": "rich",
      "title": "Leaderboard",
      "description": "**1.** alice — 12,400 xp\n**2.** bob — 9,810 xp\n**3.** carol — 7,205 xp",
      "color": 16763688,
      "footer": { "text": "Top 3 of 142" },
      "timestamp": "2026-04-29T12:00:00Z"
    }
  ]
}

Limits & Notes

  • Up to 10 embeds per message — extras are silently dropped.
  • color is a decimal integer (e.g. 0xFFC92816763688). Omit for the client's default neutral.
  • timestamp must be ISO-8601 (UTC). Pass an empty string to suppress.
  • Image/thumbnail/video/audio URLs are proxied through Echoed's media CDN — clients receive a signed proxy_url alongside your original url.
  • Auto-unfurled link embeds (when users post URLs) use the same shape — your bot can mirror that look by sending a rich embed of type: "rich" with an author block.

Reactions

Bots can add and remove their own reactions on messages. Useful for polls (seed 👍/👎), giveaways (seed 🎉), and reaction-role setup messages.

To listen for reactions added by other users, subscribe to the MESSAGE_REACTION_ADD WebSocket event.

PUT /v1/bots/{server_id}/messages/{message_id}/reactions/{emoji}
Add the bot's own reaction to a message. The {emoji} path segment must be URL-encoded. Idempotent — re-adding an existing reaction is a no-op. Requires ADD_REACTIONS in the channel.
DELETE /v1/bots/{server_id}/messages/{message_id}/reactions/{emoji}
Remove the bot's own reaction. Bots can only remove their own reactions — clearing other users' reactions is not supported.

Pinned Messages

Pin and unpin messages in a channel. Both endpoints require MANAGE_MESSAGES in the target channel.

POST /v1/bots/{server_id}/channels/{channel_id}/messages/{message_id}/pin
Pin a message. Idempotent — pinning an already-pinned message returns success.
DELETE /v1/bots/{server_id}/channels/{channel_id}/messages/{message_id}/pin
Unpin a message. Returns success if the message wasn't pinned.

Direct Messages

POST /v1/bots/dm/send
Send a direct message to a user by username. The bot and target user must share at least one server, and DMs respect block status.
Request Body
{
  "username": "target-user",
  "content": "Hey! This is a DM from my bot."
}
Response
{
  "message": "DM sent successfully.",
  "messageId": "message-id",
  "receiverId": "user-id",
  "content": "Hey! This is a DM from my bot."
}

Tasks

Manage tasks in Task-type channels. Your bot can create, update, and delete tasks for project management and automation.

GET /v1/bots/{server_id}/tasks?channelId={id}
Get all tasks from a task channel.
POST /v1/bots/{server_id}/tasks
Create a new task.
Request Body
{
  "channelId": "task-channel-id",
  "title": "Fix login bug",
  "description": "Users can't login with OAuth",
  "assigneeId": "user-id",
  "dueDate": "2026-04-01T00:00:00Z",
  "priority": "high"
}
FieldValues
prioritylow, medium, high, urgent
statusbacklog, todo, in_progress, review, done
PUT /v1/bots/{server_id}/tasks/{task_id}
Update a task's status, description, priority, or other fields.
DELETE /v1/bots/{server_id}/tasks/{task_id}
Delete a task.

Calendar Events

Manage events in Calendar-type channels. Create, update, and manage event participants.

GET /v1/bots/{server_id}/calendar?channelId={id}
Get all events from a calendar channel.
POST /v1/bots/{server_id}/calendar
Create a new calendar event.
Request Body
{
  "channelId": "calendar-channel-id",
  "title": "Game Night",
  "description": "Weekly gaming session",
  "startTime": "2026-04-01T20:00:00Z",
  "endTime": "2026-04-01T23:00:00Z",
  "location": "Voice Channel #gaming",
  "isAllDay": false
}
PUT /v1/bots/{server_id}/calendar/{event_id}
Update an event's title, description, time, or location.
DELETE /v1/bots/{server_id}/calendar/{event_id}
Delete a calendar event.

Event Participants

GET /v1/bots/{server_id}/calendar/{event_id}/participants
List all participants for an event.
POST /v1/bots/{server_id}/calendar/{event_id}/participants/add
Add a participant to an event.
Request Body
{ "userId": "user-id" }
POST /v1/bots/{server_id}/calendar/{event_id}/participants/remove
Remove a participant from an event.
Request Body
{ "userId": "user-id" }

Audit Logs

Read the server audit log to enrich a mod-log channel, build moderation dashboards, or reconcile bot actions with admin actions.

GET /v1/bots/{server_id}/audit-logs
Fetch audit log entries for a server. Requires MANAGE_SERVER.
ParameterTypeDescription
limitintegerMax entries to return (1–100, default 50)
beforeISO 8601Return entries before this timestamp
actionstringFilter by action name (e.g. kick_member, timeout_member)
Response
{
  "logs": [
    {
      "timestamp": "2026-04-28T17:20:00Z",
      "logId": "log-id",
      "actorId": "bot-id",
      "action": "timeout_member",
      "targetType": "member",
      "targetId": "user-id",
      "reason": "Spamming",
      "details": { "durationSeconds": 300 }
    }
  ],
  "count": 1,
  "serverId": "server-id",
  "botId": "bot-id"
}

WebSocket Events

Connect via Socket.IO at https://socket.echoed.gg to receive real-time events. Authenticate with your bot token after connecting; on success the bot is auto-subscribed to every server it's invited to — no separate subscribe step is required.

Connecting

Node.js (socket.io-client)
// npm install socket.io-client
import { io } from "socket.io-client";

const socket = io("https://socket.echoed.gg", {
    transports: ["websocket"],
});

socket.on("connect", () => {
    socket.emit("authenticate", {
        botToken: "zbot_your_token_here",
    });
});

socket.on("authenticated", (payload) => {
    console.log("Bot ready, sessionId:", payload.sessionId);
});
Python (python-socketio)
# pip install python-socketio[client]
import socketio

sio = socketio.Client()

@sio.on("connect")
def on_connect():
    sio.emit("authenticate", {"botToken": "zbot_your_token_here"})

sio.connect("https://socket.echoed.gg")
sio.wait()

Heartbeat

Send a heartbeat every 30 seconds to keep the connection alive. The token must be included on every heartbeat.

Heartbeat
setInterval(() => {
    socket.emit("heartbeat", { botToken: "zbot_your_token_here" });
}, 30000);

Event Types

Each Socket.IO event name is the literal event below (e.g. MESSAGE_CREATE). The payload is the event data directly — not wrapped in a {type, data} envelope. Listen for events by name:

Listening
socket.on("MESSAGE_CREATE", (data) => {
    // data.id, data.channelId, data.senderId, data.content, ...
    if (data.senderId === botUserId) return; // ignore own messages
    handleMessage(data);
});

Available Events

EventDescription
Messages
MESSAGE_CREATENew message posted in any channel the bot can see
MESSAGE_UPDATEMessage edited
MESSAGE_DELETEMessage deleted
MESSAGE_DELETE_BULKBulk delete — payload has messageIds array
MESSAGE_EMBEDS_UPDATEAsync URL unfurl completed for a message
Reactions
MESSAGE_REACTION_ADDReaction added to a message
MESSAGE_REACTION_REMOVEReaction removed from a message
MESSAGE_REACTION_REMOVE_ALLAll reactions cleared from a message
MESSAGE_REACTION_REMOVE_EMOJIAll instances of one emoji cleared
Members
SERVER_MEMBER_ADDMember joined the server — trigger welcome flows
SERVER_MEMBER_REMOVEMember left the server
SERVER_MEMBER_UPDATEMember's roles changed
SERVER_MEMBER_NICKNAME_UPDATENickname changed
SERVER_MEMBER_AVATAR_UPDATEServer-specific avatar changed
SERVER_MEMBER_KICKMember was kicked
SERVER_MEMBER_BANMember was banned
Channels & Categories
CHANNEL_CREATEChannel created
CHANNEL_UPDATEChannel edited (name, description, slowmode, etc.)
CHANNEL_DELETEChannel deleted
CHANNEL_REORDERChannels reordered within a category
CHANNEL_PINS_UPDATEPinned messages changed in a channel
CATEGORY_CREATE / CATEGORY_UPDATE / CATEGORY_DELETE / CATEGORY_REORDERCategory lifecycle
Server
SERVER_CREATEServer created
SERVER_UPDATEServer settings changed
SERVER_DELETEServer deleted
SERVER_INVITE_CREATEInvite created/refreshed
Tasks & Calendar
TASK_CREATE / TASK_UPDATE / TASK_DELETETask lifecycle
CALENDAR_EVENT_CREATE / CALENDAR_EVENT_UPDATE / CALENDAR_EVENT_DELETECalendar event lifecycle
CALENDAR_PARTICIPANT_JOIN / LEAVE / ADD / REMOVEEvent participation changes
Other
TYPING_START / TYPING_STOPTyping indicators
PRESENCE_UPDATEUser online/idle/dnd status changes
NOTIFICATION_CREATENew notification for a user
UNREAD_COUNT_UPDATEUnread count changed
PERMISSION_UPDATEPermission cache invalidated — re-fetch member permissions

MESSAGE_CREATE Payload

Example
{
  "id": "message-id",
  "channelId": "channel-id",
  "serverId": "server-id",
  "senderId": "user-id",
  "content": "Hello everyone!",
  "messageType": "user",
  "attachments": [],
  "mentions": [],
  "replyToId": "",
  "createdAt": "2026-04-28T17:00:00Z",
  "author": {
    "id": "user-id",
    "name": "Username",
    "avatarUrl": "https://...",
    "isBot": false
  }
}
Filter out the bot's own messages by checking data.senderId === botUserId — without this, every reply your bot posts triggers another MESSAGE_CREATE event and risks an infinite loop.

OAuth2

Integrate "Login with Echoed" into your application, access user data, and invite bots to servers using standard OAuth2 authorization code flow.

Authorization Flow

  1. Register an OAuth2 client in Settings → OAuth2 Apps
  2. Redirect users to the authorization URL with your client_id and requested scopes
  3. User approves — Echoed redirects back with an authorization code
  4. Exchange the code for access + refresh tokens
  5. Use the access token to call OAuth2 API endpoints
Step 1 — Authorization URL
https://go.echoed.gg/oauth2/authorize?
  response_type=code&
  client_id=your_client_id&
  redirect_uri=https://yourapp.com/callback&
  scope=openid profile email servers&
  state=random_csrf_token
Step 2 — Exchange Code for Tokens
POST https://go.echoed.gg/oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=authorization_code_from_callback&
redirect_uri=https://yourapp.com/callback&
client_id=your_client_id&
client_secret=your_client_secret
Token Response
{
  "access_token": "MKquTIm08LMU...",
  "refresh_token": "dR4nG7kP...",
  "expires_in": 3600,
  "token_type": "Bearer"
}

Scopes

Scope Access
openidBasic identity (user ID, issuer)
profileName, username, avatar, created date
emailEmail address
serversUser's servers, invite bots to servers
friendsUser's friends list
offline_accessReceive a refresh token for long-lived access

Token Lifetimes

Token Lifetime
Authorization Code10 minutes
Access Token1 hour
Refresh Token30 days

OAuth2 Endpoints

GET /oauth2/userinfo
Get the authenticated user's profile. Requires Bearer token in Authorization header.
Response (with profile + servers scopes)
{
  "sub": "user-id",
  "name": "Display Name",
  "username": "username",
  "avatar_url": "https://s3.echoed.gg/...",
  "email": "user@example.com",
  "owned_servers": [
    { "id": "server-id", "name": "My Server", "type": "public" }
  ],
  "servers_count": 5
}
GET /oauth2/api/user/servers
Get user's servers where they can invite bots. Requires servers scope.
GET /oauth2/api/user/friends
Get the user's friends list. Requires friends scope.
POST /oauth2/api/servers/{server_id}/invite-bot
Invite a bot to a server. Requires servers scope and bot management permission.
Request Body
{ "bot_id": "bot-user-id" }
POST /oauth2/token
Exchange authorization code for tokens, or refresh an existing token. Uses application/x-www-form-urlencoded.
POST /oauth2/revoke
Revoke an access or refresh token.
Request Body (form-encoded)
token=access_token_value&
token_type_hint=access_token&
client_id=your_client_id&
client_secret=your_client_secret
Tip: Request only the scopes you need. Users are more likely to approve fewer permissions. Use offline_access only if your app needs long-lived access.