✈️Telegram
Polling-based bot using the teloxide library. Feature flag: channel-telegram.
1. Create a bot
- Message @BotFather on Telegram
- Use the
/newbotcommand and follow the instructions - Copy the bot token
2. Get your user ID
- Message @userinfobot to get your Telegram user ID
- Or use @getidsbot
3. Configure
[channels.telegram]
enabled = true
token = "123456789:ABCdefGHIjklMNOpqrsTUVwxyz"
allowFrom = ["123456789"]
allowGroups = []
dmPolicy = "allowlist"
mentionOnly = false
stream = false
-100, e.g. -1001234567890. allowGroups entries must match the runtime ID exactly — listing "-1234567890" when the supergroup is actually "-1001234567890" will silently deny. Use @getidsbot in the group to read the canonical ID.Supported features
stream=true)
Message deletion
HTML formatting
Group chat support
Inline keyboard buttons
Reply threading
Mention-only group filtering
Sender allowlist
DM policy (pairing)
🎮Discord
WebSocket gateway using the serenity library. Feature flag: channel-discord.
1. Create a bot
- Go to discord.com/developers/applications
- Click "New Application"
- Go to the "Bot" section
- Click "Add Bot"
- Under "Token", click "Reset Token" and copy it
- Enable "Message Content Intent" under "Privileged Gateway Intents"
2. Invite bot to your server
- Go to "OAuth2" > "URL Generator"
- Select scopes:
bot,applications.commands - Select bot permissions:
Send Messages,Read Message History - Copy the generated URL, open it in browser, select your server and authorize
3. Get user IDs
- Enable Developer Mode in Discord (Settings > Advanced > Developer Mode)
- Right-click on users or channels and select "Copy ID"
4. Configure
[channels.discord]
enabled = true
token = "your-discord-bot-token"
allowFrom = ["123456789012345678"]
allowGroups = []
dmPolicy = "allowlist"
mentionOnly = false
stream = false
[[channels.discord.commands]]
name = "ask"
description = "Ask the AI assistant"
[[channels.discord.commands.options]]
name = "question"
description = "Your question"
required = true
Slash commands
The commands array defines Discord slash commands registered at startup. Default: a single /ask command. Each command has name, description, and an array of options (each with name, description, required).
Supported features
💼Slack
Socket Mode (WebSocket) via tokio-tungstenite. No public endpoint required. Feature flag: channel-slack.
1. Create a Slack app
- Go to api.slack.com/apps
- Click "Create New App" > "From scratch"
- Name your app and select your workspace
2. Enable Socket Mode
- Go to "Socket Mode" in the left sidebar
- Toggle "Enable Socket Mode" to ON
- Click "Generate Token" under "App-Level Tokens"
- Name it (e.g., "Socket Mode Token") and generate
- Copy the token (starts with
xapp-)
3. Add bot token scopes
Go to "OAuth & Permissions" > "Bot Token Scopes" and add:
| Scope | Purpose |
|---|---|
| chat:write | Send and edit messages |
| channels:history | Read messages in public channels |
| groups:history | Read messages in private channels |
| im:history | Read direct messages |
| mpim:history | Read group direct messages |
| users:read | Look up usernames from user IDs |
| files:read | Download image attachments from messages |
| files:write | Upload outbound media to channels |
| reactions:write | Add emoji reactions to acknowledge messages |
Optional but recommended:
| Scope | Purpose |
|---|---|
| users:write | Set bot presence to "active" on startup |
Scroll up and click "Install to Workspace". Copy the "Bot User OAuth Token" (starts with xoxb-).
4. Enable App Home messaging
- Go to "App Home" in the left sidebar
- Under "Show Tabs", enable the Messages Tab
- Check "Allow users to send Slash commands and messages from the messages tab"
5. Subscribe to events
- Go to "Event Subscriptions"
- Enable "Enable Events"
- Subscribe to bot events:
app_mention,message.channels,message.groups,message.im
6. Get user IDs
Click on a user's profile in Slack, click the three dots menu, select "Copy member ID".
7. Configure
[channels.slack]
enabled = true
botToken = "xoxb-1234567890-1234567890123-abcdefghijklmnopqrstuvwx"
appToken = "xapp-1-A1234567890-1234567890123-abcdefghijklmnopqrstuvwxyz1234567890"
allowFrom = ["U01234567"]
allowGroups = []
dmPolicy = "allowlist"
thinkingEmoji = "eyes"
doneEmoji = "white_check_mark"
stream = false
appToken must be a Socket Mode token (starts with xapp-), not a bot token. Socket Mode allows your app to receive events without exposing a public HTTP endpoint.Optional fields
| Field | Default | Description |
|---|---|---|
thinkingEmoji | "eyes" | Reaction added when processing a message |
doneEmoji | "white_check_mark" | Reaction added after responding (thinking emoji is removed) |
Thread participation
The bot automatically tracks threads it has participated in (24-hour TTL). In tracked threads, the bot responds without requiring an @mention, even when mentionOnly is enabled. This applies to both threads the bot started and threads where it replied.
Supported features
Linked-device mode using whatsapp-rust. Runs as a secondary device on your phone. Feature flag: channel-whatsapp.
- In groups,
@-mentions and quote-replies to you, the human cannot be distinguished from messages addressing the bot. The bot will respond. Your own typing is filtered out viais_from_me, but anything others direct at your contact wakes the bot. - The bot only acts in groups listed in
allowGroups, both inbound and outbound. A misconfigured webhook or cron target cannot reach a group that is not on the list.
1. First-time setup
- Run
oxicrab gatewaywith WhatsApp enabled in config - Scan the QR code displayed in the terminal with your phone (WhatsApp > Settings > Linked Devices > Link a Device)
- Session is automatically stored in
~/.oxicrab/whatsapp/whatsapp.db
2. Configure
[channels.whatsapp]
enabled = true
allowFrom = ["15037348571"]
allowGroups = []
dmPolicy = "allowlist"
Phone number format
Use phone numbers in international format (country code + number). No spaces, dashes, or plus signs.
Example: "15037348571" for US number +1 (503) 734-8571
Supported features
Group session routing
In group chats, sessions are keyed by the group JID (not the individual sender), so all participants in a group share the same conversation context.
Media handling
Images, documents (PDFs, ZIP, etc.), video, and audio are automatically downloaded to ~/.oxicrab/media/ with the whatsapp_ prefix. MIME types are used to infer file extensions. Image documents sent as document attachments are treated as images for vision processing. Audio messages are routed through voice transcription if configured.
📞Twilio (SMS/MMS)
Webhook-based using axum. Supports both SMS API and Conversations API. Feature flag: channel-twilio.
1. Get credentials
- Sign up at console.twilio.com
- Copy your Account SID and Auth Token from the dashboard
2. Buy a phone number
- Go to Phone Numbers > Buy a Number
- Ensure SMS capability is checked
- Note the number in E.164 format (e.g.
+15551234567)
3. Create a Conversation Service
- Go to Messaging > Conversations > Manage > Create Service
- Note the Conversation Service SID
4. Configure webhooks
- Go to Conversations > Manage > [Your Service] > Webhooks
- Set Post-Webhook URL to your server's public URL (e.g.
https://your-server.example.com/twilio/webhook) - Subscribe to events:
onMessageAdded - Method: POST
5. Add participants to conversations
Conversations need participants before messages flow:
curl -X POST "https://conversations.twilio.com/v1/Conversations/{ConversationSid}/Participants" \
-u "YOUR_ACCOUNT_SID:YOUR_AUTH_TOKEN" \
--data-urlencode "MessagingBinding.Address=+19876543210" \
--data-urlencode "MessagingBinding.ProxyAddress=+15551234567"
6. Expose your webhook
The webhook server must be reachable from the internet. Options:
- Cloudflare Tunnel (recommended):
cloudflared tunnel run— free, stable, no open ports - ngrok:
ngrok http 8080— quick for development - Reverse proxy: nginx/caddy with TLS termination
7. Configure
[channels.twilio]
enabled = true
accountSid = "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
authToken = "your-auth-token"
phoneNumber = "+15551234567"
webhookPort = 8080
webhookHost = "127.0.0.1"
webhookPath = "/twilio/webhook"
webhookUrl = "https://your-server.example.com/twilio/webhook"
allowFrom = []
allowGroups = []
dmPolicy = "allowlist"
["*"] for open access. webhookHost defaults to "127.0.0.1" (loopback only); set to "0.0.0.0" to listen on all interfaces.Supported features
Common patterns
Channel formatting hints
The system prompt automatically includes per-channel formatting guidance, so the LLM avoids broken rendering (e.g. markdown tables on Discord/Telegram, standard markdown in Slack). No configuration needed — the hints are injected based on the active channel.
Selective compilation
Each channel is a Cargo feature flag. Build only what you deploy:
# All channels (default)
cargo build --release
# Only Telegram and Slack
cargo build --release --no-default-features --features channel-telegram,channel-slack
# No channels (agent CLI only)
cargo build --release --no-default-features
DM access policy
Every channel supports a dmPolicy field that controls what happens when an unrecognized sender messages the bot. Three modes are available:
| Value | Behavior |
|---|---|
| "allowlist" | Default. Check allowFrom + pairing store. Silently drop unrecognized senders. |
| "pairing" | Check allowFrom + pairing store. Send a pairing code to unrecognized senders so they can request access. |
| "open" | Allow all senders unconditionally. Skip all access checks. |
[channels.telegram]
enabled = true
token = "123456789:ABCdefGHIjklMNOpqrsTUVwxyz"
allowFrom = ["123456789"]
dmPolicy = "pairing"
Access control lifecycle
Sender access is resolved from three sources, checked in order. The first match wins:
- Config allowlist (
allowFrom) — sender IDs hardcoded inconfig.toml. Checked first, always available. Use["*"]as a wildcard to allow everyone. Phone numbers are normalized (leading+stripped) so"+15551234567"and"15551234567"both match. - Pairing store (SQLite
pairing_allowlisttable in workspacememory.sqlite3) — sender IDs added dynamically viaoxicrab pairing approve. Read from DB on every message, so approvals take effect without restarting oxicrab. - DM policy fallback (
dmPolicy) — what happens when a sender matches neither source above. This is where the three modes diverge.
The full decision flow for every inbound message:
Inbound message from sender
|
v
dmPolicy == "open"? ──yes──> ALLOW (skip all checks)
|
no
v
Sender in config allowFrom? ──yes──> ALLOW
(normalized match, or "*" wildcard)
|
no
v
Sender in pairing store? ──yes──> ALLOW
(pairing_allowlist table in memory.sqlite3)
|
no
v
dmPolicy == "pairing"? ──yes──┐
| v
no Generate 8-char code (15-min TTL)
v Reply to sender with pairing instructions
DENY ── sender shares code with bot owner ──>
(silent drop) Owner runs: oxicrab pairing approve {code}
|
v
Sender added to pairing store
Next message from sender ──> ALLOW
Walkthrough: message lifecycle with pairing
This walkthrough applies to every channel (Telegram, Discord, Slack, WhatsApp, Twilio). The only difference is how the pairing reply is delivered.
- Configure the channel. Set
enabled = true, add your own ID toallowFrom, and choose admPolicy. For this walkthrough we usedmPolicy = "pairing". - Start oxicrab.
oxicrab gatewayconnects the channel. Your ID is inallowFrom, so your messages work immediately. - An unknown sender messages the bot. They are not in
allowFromand not in the pairing store. - The channel generates a pairing code. An 8-character code (e.g.
XK7M2NPA) is created with a 15-minute TTL and sent back to the sender:- Telegram — bot sends a reply message
- Discord — ephemeral interaction response (slash commands/buttons) or channel reply (messages)
- Slack — bot posts a message in the conversation via
chat.postMessage - Twilio — returns a TwiML
<Message>so Twilio sends an SMS reply - WhatsApp — code is logged server-side (reply not yet supported)
- The sender shares the code with you (out-of-band — in person, via another chat, etc.).
- You approve the pairing. Run
oxicrab pairing approve {code}. This validates the code and adds the sender's ID to thepairing_allowlisttable in the workspace database. - The sender is now permanently allowed. Their next message hits the pairing store check and passes. No restart needed. You can revoke later with
oxicrab pairing revoke {channel} {sender_id}.
What changes with each policy
| Scenario | "allowlist" (default) | "pairing" | "open" |
|---|---|---|---|
| Sender in allowFrom | Allowed | Allowed | Allowed |
| Sender in pairing store | Allowed | Allowed | Allowed |
| Unknown sender | Silent drop | Pairing code sent | Allowed |
| Empty allowFrom, no pairings | All denied | All get pairing codes | All allowed |
"pairing" to let trusted contacts self-onboard, then switch to "allowlist" once your user base is established. Use "open" only for public-facing bots where anyone should be able to interact.Allowlist filtering
All channels support an allowFrom array. Empty allowFrom defaults to deny-all (under the "allowlist" and "pairing" policies) — use ["*"] to allow all senders, or use DM pairing to onboard specific users. When populated, only listed IDs can interact with the bot.
Media handling
Inbound media (images, voice messages) is downloaded and saved to ~/.oxicrab/media/ with channel-specific prefixes. Voice messages are automatically transcribed if the transcription service is configured.
Auto-reconnection
All channels implement exponential backoff retry loops (5–60 seconds). Reconnection is automatic after network disconnects or backend errors.
Streaming responses
Telegram, Discord, and Slack support progressive message edits so the user sees the agent's reply build up in real time instead of all at once. Set stream = true on the channel to opt in. Off by default.
- Streams every turn that produces text — first-turn Q&A streams from the very first delta; mixed turns ("Let me check..." + tool call) stream the prelude, run tools, then update the same message with the final answer.
- Per-turn isolation: each agent run gets its own UUID
turn_id; the channel keeps aDashMap<turn_id, message_id>so late deltas can never edit a previous turn's message. The dispatcher'sbegin_emittedatomic guarantees at-most-one placeholder per run. - Edit throttle: ~1 edit/sec per message to stay well under Telegram's 1/sec, Discord's 5/sec, and Slack's 50/min ceilings.
- Buttons land on the streamed message, attached via the final edit (Telegram
editMessageReplyMarkup, DiscordEditMessage.components(), Slackchat.updatewithblocks). No follow-up button-only message. - Media sidecar: attachments (screenshots, generated files) follow as a separate
OutboundMessageafter the stream ends, since edit-message APIs cannot add attachments. - Fail-safe to non-streaming: after 3 consecutive edit-API failures the pump abandons streaming for the rest of the turn and the agent loop delivers the final content via the normal
send()path. - Cancellation: the per-session cancel token aborts in-flight LLM streams; the consumer commits its accumulated content as the final message.
- WhatsApp and Twilio have no live-edit API and are not streamable.
Telemetry counters: oxicrab_streaming_turns_total{outcome}, oxicrab_streaming_edit_failures_total{reason}, oxicrab_streaming_fallback_to_nonstream_total{reason}.
Approval channel target
The operator approval workflow can route approval requests to a dedicated channel. The channel field in [agents.defaults.approval] uses "channel_type:chat_id" format. The bot must be a member of the target channel. To find the channel ID for each platform:
- Slack — right-click the channel name, select "View channel details", copy the Channel ID from the bottom of the details panel (starts with
C). Example:"slack:C0ABC123" - Discord — enable Developer Mode (User Settings > Advanced > Developer Mode), then right-click the channel and select "Copy Channel ID". Example:
"discord:123456789012345678" - Telegram — use @getidsbot in the target group/channel, or check the URL in Telegram Web (
https://web.telegram.org/k/#-XXXXXXXXX). Example:"telegram:-1001234567890"
Leave channel empty for self-approval (buttons appear in the same conversation as the user).