Developers
📖 Docs 💻 Developer Portal
Support
🆘 Get Help 💛 Donate 💬 Feedback 📚 Knowledge Base
⬇ Download

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

GET/api/me

Returns 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

PATCH/api/bot/me— update status and/or mood

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

StatusMeaning
onlineActive and reachable
awayConnected but idle
busyDo not disturb
offlineAppears offline to other users

Profile

POST/api/me/avatar— upload profile picture
{ "data": "<base64>", "mime": "image/png" }
DELETE/api/me/avatar
POST/api/me/banner— same format as avatar
DELETE/api/me/banner

Messaging

GET/api/conversations/{id}/messages?limit=50&before=MSG_ID
POST/api/conversations/{id}/messages
{
  "text": "Hello!",
  "bold": false,
  "italic": false,
  "mentions": ["u-id"],
  "reply_to_id": "msg-prev"
}
PATCH/api/messages/{id}— edit own messages — { "text": "updated" }
DELETE/api/messages/{id}
POST/api/messages/{id}/reactions{ "emoji": "👍" }

Conversations & Groups

GET/api/conversations
POST/api/conversations{ "peer_id": "u-xyz" }
GET/api/groups/{id}/messages
POST/api/groups/{id}/messages

Contacts

GET/api/contacts— all visible users. Filter is_bot === true for bots only.

Error Format

{ "error": "description" }
CodeMeaning
400Bad request — check your body
401Missing or invalid token
403Forbidden — missing permission
404Resource not found
409Conflict — e.g. username taken
429Rate 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_message panel_member_joined panel_member_left panel_banned panel_updated panel_channel_created panel_channel_updated panel_channel_deleted panel_deleted mention

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

GET/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

GET/api/panels/{panel_id}/channels
[{ "id": "ch-001", "panel_id": "panel-abc", "name": "general", "category_id": "cat-1" }]

Channel Messages

GET/api/panels/{panel_id}/channels/{channel_id}/messages?limit=50
POST/api/panels/{panel_id}/channels/{channel_id}/messages
{ "text": "Hello panel!", "mentions": ["u-alice"] }
POST/api/panels/{panel_id}/channels/{channel_id}/read— clear unread counter

Panel Members

GET/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 — use ctx.panel and ctx.channel directly.
  • Check message.is_system to skip join/leave announcements.
  • Use on_panel_mention to only fire on direct @-mentions.
  • Check sender.is_bot to avoid bot-to-bot reply loops.
  • Set status to away or busy via PATCH /api/bot/me when 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

  1. User opens the authorization URL.
  2. If not logged into Circul, they see a login form.
  3. They see panels they own or can manage, excluding panels the bot is already in.
  4. They pick a panel and click Authorize.
  5. 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.