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
- Create a bot account in your Settings → Bot Profile
- Copy your Bot API Key (starts with
zbot_) - Invite your bot to a server via the Bot Discovery page or directly
- Start making API requests with the
X-Bot-Tokenheader
Authentication
All Bot API requests require the X-Bot-Token header with your bot's API key.
X-Bot-Token: zbot_your_api_key_here Content-Type: application/json
Base URL
All Bot API endpoints are prefixed with:
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.
{
"message": "Rate limited. Please wait.",
"code": 429,
"retryAfter": 12
}
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 |
|---|---|
200 | Success |
201 | Created (new resource) |
400 | Bad Request — invalid parameters |
401 | Unauthorized — invalid or missing token |
403 | Forbidden — insufficient permissions |
404 | Not Found — resource doesn't exist |
409 | Conflict — e.g., already banned |
429 | Too Many Requests — rate limit exceeded (see Rate Limits) |
500 | Internal Server Error |
503 | Service Unavailable — database/transient |
{
"message": "Bot is not a member of this server",
"success": false
}
Bot Profile
{
"valid": true,
"bot_id": "67e537d200011f5275d2",
"message": "Bot API key is valid."
}
{
"id": "67e537d200011f5275d2",
"username": "my-awesome-bot",
"isBot": true,
"botDescription": "A helpful server bot",
"botCategory": "utility",
"isPublic": true,
"avatarUrl": "https://s3.echoed.gg/..."
}
PATCH /v1/bots/me below.{
"botDescription": "Updated bot description",
"botCategories": ["utility", "moderation"],
"botSupportServer": "https://echoed.gg/invite/...",
"botVersion": "1.2.0"
}
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.{
"name": "Panda Bot",
"avatar": "https://cdn.example.com/panda.png"
}
{
"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.
memberNicknameUpdated socket event so clients update without refetching.{
"nickname": "Panda EU"
}
{
"success": true,
"nickname": "Panda EU"
}
name in this server.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.{
"success": true,
"path": "public/servers/server-id/members/bot-id/avatar.png",
"url": "https://s3.echoed.gg/public/servers/.../avatar.png",
"timestamp": 1735531200000
}
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.
{
"nickname": "NewName"
}
{
"success": true,
"userId": "target-user-id",
"nickname": "NewName"
}
file field. Allowed formats: png, jpg, jpeg, gif, webp.{
"success": true,
"userId": "target-user-id",
"path": "public/servers/.../avatar.png",
"url": "https://s3.echoed.gg/...",
"timestamp": 1735531200000
}
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.
callId = channelId).{
"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).
Room.disconnect()); this endpoint is for bookkeeping and audit logs.Servers
{
"count": 2,
"servers": [
{
"serverId": "6894d3d90011f842607c",
"serverName": "My Server",
"serverIcon": "",
"invitedAt": "2026-01-06T13:41:44Z",
"invitedBy": "user-id"
}
]
}
{
"id": "6894d3d90011f842607c",
"name": "My Server",
"description": "A cool server",
"ownerId": "user-id",
"memberCount": 42,
"channelCount": 8,
"iconUrl": "",
"createdAt": "2025-08-07T16:27:05Z"
}
Channels
{
"channels": [
{
"id": "647a23fede03765e0348",
"name": "general",
"type": "text",
"description": "",
"createdAt": "2026-01-05T11:20:43Z"
}
],
"total": 8
}
text, video, tasks, calendar. Requires MANAGE_CHANNELS.{
"name": "bot-updates",
"type": "text",
"description": "Automated bot announcements",
"isPrivate": false,
"isNsfw": false,
"categoryId": "category-id",
"position": 0
}
MANAGE_CHANNELS.{
"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).
{
"channelId": "channel-id",
"categoryId": "category-id",
"position": 0
}
Categories
{
"name": "Bot Channels",
"position": 0
}
{
"categories": [
{ "categoryId": "id1", "position": 0 },
{ "categoryId": "id2", "position": 1 }
]
}
Members
| Parameter | Type | Description |
|---|---|---|
limit | integer | Max results (default 10) |
offset | integer | Pagination offset |
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).
{
"userId": "user-id",
"serverId": "server-id",
"permissions": [
"VIEW_CHANNELS", "SEND_MESSAGES",
"READ_MESSAGE_HISTORY", "CONNECT", "SPEAK"
]
}
{
"userId": "user-id",
"serverId": "server-id",
"channelId": "channel-id",
"permissions": [
"VIEW_CHANNELS", "SEND_MESSAGES",
"READ_MESSAGE_HISTORY", "MANAGE_MESSAGES"
]
}
Moderation
Punitive member actions: kick, ban, unban, and timeout. Each requires the matching permission on the bot. The server owner cannot be targeted.
KICK_MEMBERS.{ "reason": "Optional reason" }
BAN_MEMBERS. Returns 409 if already banned.{ "reason": "Reason for ban" }
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.
durationSeconds (capped at 28 days). Re-applying overwrites the previous timeout.{
"durationSeconds": 300,
"reason": "Spamming"
}
{
"success": true,
"userId": "user-id",
"timeoutUntil": "2026-04-28T17:25:00Z",
"durationSeconds": 300,
"reason": "Spamming"
}
active: false if no timeout is set or it has expired.{
"active": true,
"userId": "user-id",
"timeoutUntil": "2026-04-28T17:25:00Z",
"reason": "Spamming",
"setBy": "bot-id"
}
Roles
MANAGE_ROLES. permissions accepts an array of permission names or an integer bitmask.{
"name": "Moderator",
"color": "#FF5733",
"permissions": ["SEND_MESSAGES", "MANAGE_MESSAGES", "KICK_MEMBERS"],
"position": 5
}
@everyone, Server Owner) cannot be edited. Requires MANAGE_ROLES.{
"name": "Senior Moderator",
"color": "#9B59B6",
"permissions": ["MANAGE_MESSAGES", "KICK_MEMBERS", "BAN_MEMBERS"],
"position": 8,
"mentionable": true,
"hoist": true
}
{
"roles": ["role-id-1", "role-id-2"]
}
Messages
limit (max 100), before, after for pagination.content may be empty as long as embeds or attachmentIds is provided. See Rich Embeds for the embed object schema (max 10 per message).{
"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.
multipart/form-data with a file field. Max file size: 50 MB. Supported types: images, videos, audio, documents, archives, and code files.{
"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.
{
"channelId": "channel-id",
"content": "Check out this image!",
"attachmentIds": ["abc123"],
"mentions": [],
"replyToId": ""
}
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.{
"content": "Edited message content"
}
{
"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.
MANAGE_MESSAGES.MANAGE_MESSAGES.{
"messageIds": ["id1", "id2", "id3"]
}
{
"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.
{
"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
{
"name": "Author or site name",
"url": "https://…",
"icon_url": "https://…/avatar.png"
}
{
"url": "https://…/asset",
"width": 1280,
"height": 720
}
{
"name": "Heading",
"value": "Body — markdown supported.",
"inline": false
}
{
"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.
{
"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.
coloris a decimal integer (e.g.0xFFC928→16763688). Omit for the client's default neutral.timestampmust 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_urlalongside your originalurl. - 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 anauthorblock.
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.
{emoji} path segment must be URL-encoded. Idempotent — re-adding an existing reaction is a no-op. Requires ADD_REACTIONS in the channel.Pinned Messages
Pin and unpin messages in a channel. Both endpoints require MANAGE_MESSAGES in the target channel.
Direct Messages
{
"username": "target-user",
"content": "Hey! This is a DM from my bot."
}
{
"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.
{
"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"
}
| Field | Values |
|---|---|
priority | low, medium, high, urgent |
status | backlog, todo, in_progress, review, done |
Calendar Events
Manage events in Calendar-type channels. Create, update, and manage event participants.
{
"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
}
Event Participants
{ "userId": "user-id" }
{ "userId": "user-id" }
Search
| Parameter | Type | Description |
|---|---|---|
q | string | Search query (min 2 chars, required) |
channel_id | string | Filter by channel |
author_id | string | Filter by message author |
before | ISO 8601 | Messages before this date |
after | ISO 8601 | Messages after this date |
sort | string | newest (default) or oldest |
limit | integer | Max results |
offset | integer | Pagination offset |
Audit Logs
Read the server audit log to enrich a mod-log channel, build moderation dashboards, or reconcile bot actions with admin actions.
MANAGE_SERVER.| Parameter | Type | Description |
|---|---|---|
limit | integer | Max entries to return (1–100, default 50) |
before | ISO 8601 | Return entries before this timestamp |
action | string | Filter by action name (e.g. kick_member, timeout_member) |
{
"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
// 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); });
# 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.
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:
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
| Event | Description |
|---|---|
| Messages | |
MESSAGE_CREATE | New message posted in any channel the bot can see |
MESSAGE_UPDATE | Message edited |
MESSAGE_DELETE | Message deleted |
MESSAGE_DELETE_BULK | Bulk delete — payload has messageIds array |
MESSAGE_EMBEDS_UPDATE | Async URL unfurl completed for a message |
| Reactions | |
MESSAGE_REACTION_ADD | Reaction added to a message |
MESSAGE_REACTION_REMOVE | Reaction removed from a message |
MESSAGE_REACTION_REMOVE_ALL | All reactions cleared from a message |
MESSAGE_REACTION_REMOVE_EMOJI | All instances of one emoji cleared |
| Members | |
SERVER_MEMBER_ADD | Member joined the server — trigger welcome flows |
SERVER_MEMBER_REMOVE | Member left the server |
SERVER_MEMBER_UPDATE | Member's roles changed |
SERVER_MEMBER_NICKNAME_UPDATE | Nickname changed |
SERVER_MEMBER_AVATAR_UPDATE | Server-specific avatar changed |
SERVER_MEMBER_KICK | Member was kicked |
SERVER_MEMBER_BAN | Member was banned |
| Channels & Categories | |
CHANNEL_CREATE | Channel created |
CHANNEL_UPDATE | Channel edited (name, description, slowmode, etc.) |
CHANNEL_DELETE | Channel deleted |
CHANNEL_REORDER | Channels reordered within a category |
CHANNEL_PINS_UPDATE | Pinned messages changed in a channel |
CATEGORY_CREATE / CATEGORY_UPDATE / CATEGORY_DELETE / CATEGORY_REORDER | Category lifecycle |
| Server | |
SERVER_CREATE | Server created |
SERVER_UPDATE | Server settings changed |
SERVER_DELETE | Server deleted |
SERVER_INVITE_CREATE | Invite created/refreshed |
| Tasks & Calendar | |
TASK_CREATE / TASK_UPDATE / TASK_DELETE | Task lifecycle |
CALENDAR_EVENT_CREATE / CALENDAR_EVENT_UPDATE / CALENDAR_EVENT_DELETE | Calendar event lifecycle |
CALENDAR_PARTICIPANT_JOIN / LEAVE / ADD / REMOVE | Event participation changes |
| Other | |
TYPING_START / TYPING_STOP | Typing indicators |
PRESENCE_UPDATE | User online/idle/dnd status changes |
NOTIFICATION_CREATE | New notification for a user |
UNREAD_COUNT_UPDATE | Unread count changed |
PERMISSION_UPDATE | Permission cache invalidated — re-fetch member permissions |
MESSAGE_CREATE Payload
{
"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
}
}
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
- Register an OAuth2 client in Settings → OAuth2 Apps
- Redirect users to the authorization URL with your
client_idand requested scopes - User approves — Echoed redirects back with an authorization code
- Exchange the code for access + refresh tokens
- Use the access token to call OAuth2 API endpoints
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
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
{
"access_token": "MKquTIm08LMU...",
"refresh_token": "dR4nG7kP...",
"expires_in": 3600,
"token_type": "Bearer"
}
Scopes
| Scope | Access |
|---|---|
openid | Basic identity (user ID, issuer) |
profile | Name, username, avatar, created date |
email | Email address |
servers | User's servers, invite bots to servers |
friends | User's friends list |
offline_access | Receive a refresh token for long-lived access |
Token Lifetimes
| Token | Lifetime |
|---|---|
| Authorization Code | 10 minutes |
| Access Token | 1 hour |
| Refresh Token | 30 days |
OAuth2 Endpoints
Bearer token in Authorization header.{
"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
}
servers scope.friends scope.servers scope and bot management permission.{ "bot_id": "bot-user-id" }
application/x-www-form-urlencoded.token=access_token_value& token_type_hint=access_token& client_id=your_client_id& client_secret=your_client_secret
offline_access only if your app needs long-lived access.