Bot REST API
Build bots that send messages, react to events, and interact with panels. All requests authenticate with your bot token.
Authentication
Include your token in every request header:
Authorization: Bot YOUR_TOKEN
Obtain your token from My Apps. Tokens are shown only once on creation — reset via the portal if lost.
Base URL
https://circul.xyz
Bot Identity
/api/meReturns your bot's user object.
{
"id": "bot-abc",
"name": "WeatherBot",
"is_bot": true,
"owner_id": "u-xyz",
"status": "online",
"mood": "",
"avatar_url": "/avatars/bot-abc.png",
"banner_url": ""
}
Status & Mood
/api/bot/me— update status and/or moodBoth fields are optional. Only fields present in the body are updated.
{
"status": "online", // "online" | "away" | "busy" | "offline"
"mood": "⛅ Fetching forecasts…"
}
Returns the updated user object. Broadcasts a status_change event to all connected clients immediately. If mood is changed, a user_updated event is also broadcast.
| Status | Meaning |
|---|---|
online | Active and reachable |
away | Connected but idle |
busy | Do not disturb |
offline | Appears offline to other users |
Profile
/api/me/avatar— upload profile picture{ "data": "<base64>", "mime": "image/png" }
/api/me/avatar/api/me/banner— same format as avatar/api/me/bannerMessaging
/api/conversations/{id}/messages— ?limit=50&before=MSG_ID/api/conversations/{id}/messages{
"text": "Hello!",
"bold": false,
"italic": false,
"mentions": ["u-id"],
"reply_to_id": "msg-prev"
}
/api/messages/{id}— edit own messages — { "text": "updated" }/api/messages/{id}/api/messages/{id}/reactions— { "emoji": "👍" }Conversations & Groups
/api/conversations/api/conversations— { "peer_id": "u-xyz" }/api/groups/{id}/messages/api/groups/{id}/messagesContacts
/api/contacts— all visible users. Filter is_bot === true for bots only.Error Format
{ "error": "description" }
| Code | Meaning |
|---|---|
400 | Bad request — check your body |
401 | Missing or invalid token |
403 | Forbidden — missing permission |
404 | Resource not found |
409 | Conflict — e.g. username taken |
429 | Rate limited |
WebSocket Gateway
Subscribe once and receive all real-time events without polling. The same connection works for DMs, group chats, and panel channels.
Connection
wss://circul.xyz/ws?token=YOUR_RAW_TOKEN
Pass the raw token (without the Bot prefix). Ping is sent every 30 s — respond with a pong or the server will close the connection.
Event Envelope
{ "type": "event_name", "payload": { ... } }
Message Events
message_new
{ "type": "message_new", "payload": { "conv_id": "c-abc", "message": { ... } } }
panel_message
{ "type": "panel_message", "payload": { "panel_id": "...", "channel_id": "...", "message": { ... } } }
message_edited
{ "type": "message_edited", "payload": { "conv_id": "...", "message": { "id": "...", "text": "...", "edited_at": "..." } } }
message_deleted
{ "type": "message_deleted", "payload": { "conv_id": "...", "message_id": "..." } }
Reaction Events
reaction_added
{ "type": "reaction_added", "payload": { "message_id": "...", "emoji": "👍", "user_id": "..." } }
User Events
status_change
{ "payload": { "user_id": "...", "status": "online" } }
Fired whenever any user (including bots) changes status — including when a bot calls PATCH /api/bot/me or connects/disconnects.
user_updated
Payload is the updated user object. Fired on mood, name, bio, or avatar changes.
avatar_updated
{ "payload": { "user_id": "...", "avatar_url": "/avatars/..." } }
Panel Events
panel_member_joined / panel_member_left
{ "payload": { "panel_id": "...", "user_id": "..." } }
panel_banned
{ "payload": { "panel_id": "...", "panel_name": "...", "reason": "..." } }
panel_updated
Payload is the full updated Panel object.
panel_channel_created / panel_channel_updated
Payload is a PanelChannel object.
panel_channel_deleted
{ "payload": { "panel_id": "...", "channel_id": "..." } }
panel_deleted
{ "payload": { "panel_id": "..." } }
Mention Event
mention
Fired when the bot is @-mentioned anywhere.
{ "payload": { "message": { ... }, "conv_id": "...", "panel_id": "...", "sender_id": "..." } }
Panels API
Panels are Discord-style servers with named channels, categories, roles, and members. Add your bot to a panel via OAuth or direct invite.
Listing Panels
/api/panels[{
"id": "panel-abc",
"name": "My Server",
"creator_id": "u-alice",
"member_ids": ["u-alice", "bot-xyz"],
"roles": [{ "id": "role-1", "name": "Admin", "color": "#e05252", "position": 1 }],
"categories": [{ "id": "cat-1", "name": "Text", "channel_ids": ["ch-001"] }],
"member_roles": { "u-alice": ["role-1"] }
}]
Channels
/api/panels/{panel_id}/channels[{ "id": "ch-001", "panel_id": "panel-abc", "name": "general", "category_id": "cat-1" }]
Channel Messages
/api/panels/{panel_id}/channels/{channel_id}/messages— ?limit=50/api/panels/{panel_id}/channels/{channel_id}/messages{ "text": "Hello panel!", "mentions": ["u-alice"] }
/api/panels/{panel_id}/channels/{channel_id}/read— clear unread counterPanel Members
/api/panels/{panel_id}/members[
{ "id": "u-alice", "name": "alice", "status": "online", "is_bot": false },
{ "id": "bot-xyz", "name": "WeatherBot", "is_bot": true, "owner_id": "u-alice" }
]
Permission Model
Bots inherit all role-based permissions. Channel-level overrides (allow/deny per role) take priority over panel-wide role permissions. A bot with no roles gets the default everyone permissions. Channel-level permission denials return 403.
Real-time Events
All panel events are delivered over the WebSocket connection. See the WebSocket page for full payloads.
Python Example
import os, circul
bot = circul.Bot(os.environ["CIRCUL_SERVER"], os.environ["BOT_TOKEN"])
@bot.on_ready
def ready(me):
for p in bot.get_panels():
print(p.name, [c.name for c in bot.get_channels(p.id)])
@bot.on_panel_message
def on_msg(ctx):
# ctx.panel → Panel object
# ctx.channel → PanelChannel object
if ctx.mentioned_me:
ctx.reply("You mentioned me!", mention_sender=True)
@bot.on_member_joined
def welcome(ctx):
chs = bot.get_channels(ctx.panel_id)
g = next((c for c in chs if "general" in c.name), chs[0])
ctx.send(g.id, f"Welcome <@{ctx.user_id}>!")
@bot.command("info")
def cmd_info(ctx):
if ctx.is_panel and ctx.panel:
ctx.reply(f"{ctx.panel.name} — {ctx.panel.member_count} members")
bot.run()
Best Practices
- The Python library caches panels/channels on
on_ready— usectx.panelandctx.channeldirectly. - Check
message.is_systemto skip join/leave announcements. - Use
on_panel_mentionto only fire on direct @-mentions. - Check
sender.is_botto avoid bot-to-bot reply loops. - Set status to
awayorbusyviaPATCH /api/bot/mewhen your bot is processing a long task.
Bot OAuth — Add to Panel
Let panel owners authorize your bot without you sharing your token. One URL, zero friction.
Authorization URL
GET /oauth/authorize?bot_id=YOUR_APP_ID&redirect_uri=https://yourapp.com/callback
Flow
- User opens the authorization URL.
- If not logged into Circul, they see a login form.
- They see panels they own or can manage, excluding panels the bot is already in.
- They pick a panel and click Authorize.
- Bot is added as a member and the user is redirected to
redirect_uri.
Success Redirect
https://yourapp.com/callback?panel_id=panel-abc&panel_name=My+Server&bot=WeatherBot
Cancel / Denied
https://yourapp.com/callback?error=access_denied
Callback Example
# Flask
from flask import Flask, request
app = Flask(__name__)
@app.route('/callback')
def callback():
if request.args.get('error'): return 'Cancelled.'
return f'{request.args["bot"]} added to {request.args["panel_name"]}!'
Getting Your URL
Open My Apps → Manage → 🔐 OAuth. The URL is shown with a copy button. Enter a redirect_uri to preview the full URL with all parameters.
Security Notes
- Anyone can open the URL, but only add the bot to panels they control.
- State tokens on the consent page expire after 15 minutes.
- No invite code is required or consumed.