Channels

✈️Telegram

Polling-based bot using the teloxide library. Feature flag: channel-telegram.

1. Create a bot

  1. Message @BotFather on Telegram
  2. Use the /newbot command and follow the instructions
  3. Copy the bot token

2. Get your user ID

  1. Message @userinfobot to get your Telegram user ID
  2. Or use @getidsbot

3. Configure

~/.oxicrab/config.toml
[channels.telegram]
enabled = true
token = "123456789:ABCdefGHIjklMNOpqrsTUVwxyz"
allowFrom = ["123456789"]
allowGroups = []
dmPolicy = "allowlist"
mentionOnly = false
stream = false
Group ID format. Telegram group chat IDs are negative integers. Supergroups (the modern default for groups larger than ~200 members, and any group converted from "basic group") have IDs that start with -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

Text messages Photo attachments Voice messages (OGG) Video attachments Audio attachments Animation (GIF) handling Typing indicators Message editing Progressive streaming (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

  1. Go to discord.com/developers/applications
  2. Click "New Application"
  3. Go to the "Bot" section
  4. Click "Add Bot"
  5. Under "Token", click "Reset Token" and copy it
  6. Enable "Message Content Intent" under "Privileged Gateway Intents"

2. Invite bot to your server

  1. Go to "OAuth2" > "URL Generator"
  2. Select scopes: bot, applications.commands
  3. Select bot permissions: Send Messages, Read Message History
  4. Copy the generated URL, open it in browser, select your server and authorize

3. Get user IDs

  1. Enable Developer Mode in Discord (Settings > Advanced > Developer Mode)
  2. Right-click on users or channels and select "Copy ID"

4. Configure

~/.oxicrab/config.toml
[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

Text messages Image attachments Audio attachments PDF/document downloads Typing indicators Message editing Message deletion Guild + DM support Interactive buttons (action rows) Mention-only group filtering Sender allowlist DM policy (pairing)

💼Slack

Socket Mode (WebSocket) via tokio-tungstenite. No public endpoint required. Feature flag: channel-slack.

1. Create a Slack app

  1. Go to api.slack.com/apps
  2. Click "Create New App" > "From scratch"
  3. Name your app and select your workspace

2. Enable Socket Mode

  1. Go to "Socket Mode" in the left sidebar
  2. Toggle "Enable Socket Mode" to ON
  3. Click "Generate Token" under "App-Level Tokens"
  4. Name it (e.g., "Socket Mode Token") and generate
  5. Copy the token (starts with xapp-)

3. Add bot token scopes

Go to "OAuth & Permissions" > "Bot Token Scopes" and add:

ScopePurpose
chat:writeSend and edit messages
channels:historyRead messages in public channels
groups:historyRead messages in private channels
im:historyRead direct messages
mpim:historyRead group direct messages
users:readLook up usernames from user IDs
files:readDownload image attachments from messages
files:writeUpload outbound media to channels
reactions:writeAdd emoji reactions to acknowledge messages

Optional but recommended:

ScopePurpose
users:writeSet 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

  1. Go to "App Home" in the left sidebar
  2. Under "Show Tabs", enable the Messages Tab
  3. Check "Allow users to send Slash commands and messages from the messages tab"
Important: Without this, users will see "Sending messages to this app has been turned off."

5. Subscribe to events

  1. Go to "Event Subscriptions"
  2. Enable "Enable Events"
  3. 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

~/.oxicrab/config.toml
[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
Note: The 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

FieldDefaultDescription
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

Text messages Image attachments Audio attachments 3-step file upload Message editing Message deletion Emoji reactions Reaction lifecycle Block Kit buttons Interactive payloads Thread participation tracking Markdown formatting Sender allowlist DM policy (pairing) Error classification & retry

📱WhatsApp

Linked-device mode using whatsapp-rust. Runs as a secondary device on your phone. Feature flag: channel-whatsapp.

Important — the bot shares your WhatsApp identity. When you scan the QR with your personal phone, the bot links to your account — its JID is your phone number's JID. Two consequences:
  • 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 via is_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.
Recommended: give the bot its own WhatsApp number — a second SIM/eSIM on a separate device, or a number registered through WhatsApp Business. Then the bot's identity is distinct from yours and addressing is unambiguous. Until you do this, treat any group you add the bot to as a place where mentioning you also wakes the bot.

1. First-time setup

  1. Run oxicrab gateway with WhatsApp enabled in config
  2. Scan the QR code displayed in the terminal with your phone (WhatsApp > Settings > Linked Devices > Link a Device)
  3. Session is automatically stored in ~/.oxicrab/whatsapp/whatsapp.db

2. Configure

~/.oxicrab/config.toml
[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

Text messages Image attachments Document attachments (PDF, etc.) Video attachments Voice/audio messages QR code auth Group conversations Persistent sessions Sender allowlist DM policy (pairing)

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

  1. Sign up at console.twilio.com
  2. Copy your Account SID and Auth Token from the dashboard

2. Buy a phone number

  1. Go to Phone Numbers > Buy a Number
  2. Ensure SMS capability is checked
  3. Note the number in E.164 format (e.g. +15551234567)

3. Create a Conversation Service

  1. Go to Messaging > Conversations > Manage > Create Service
  2. Note the Conversation Service SID

4. Configure webhooks

  1. Go to Conversations > Manage > [Your Service] > Webhooks
  2. Set Post-Webhook URL to your server's public URL (e.g. https://your-server.example.com/twilio/webhook)
  3. Subscribe to events: onMessageAdded
  4. 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:

  1. Cloudflare Tunnel (recommended): cloudflared tunnel run — free, stable, no open ports
  2. ngrok: ngrok http 8080 — quick for development
  3. Reverse proxy: nginx/caddy with TLS termination

7. Configure

~/.oxicrab/config.toml
[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"
webhookUrl is required and must match exactly what Twilio POSTs to (used for HMAC-SHA1 signature validation). The Twilio channel will not start without it. Empty allowFrom denies all senders — add phone numbers or use ["*"] 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

SMS sending/receiving MMS media support (inbound images) Conversations API HMAC-SHA1 signature validation Message chunking (1600 chars) Sender allowlist Group access control DM policy (pairing)

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:

ValueBehavior
"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.
Example: enable pairing on Telegram
[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:

  1. Config allowlist (allowFrom) — sender IDs hardcoded in config.toml. Checked first, always available. Use ["*"] as a wildcard to allow everyone. Phone numbers are normalized (leading + stripped) so "+15551234567" and "15551234567" both match.
  2. Pairing store (SQLite pairing_allowlist table in workspace memory.sqlite3) — sender IDs added dynamically via oxicrab pairing approve. Read from DB on every message, so approvals take effect without restarting oxicrab.
  3. 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.

  1. Configure the channel. Set enabled = true, add your own ID to allowFrom, and choose a dmPolicy. For this walkthrough we use dmPolicy = "pairing".
  2. Start oxicrab. oxicrab gateway connects the channel. Your ID is in allowFrom, so your messages work immediately.
  3. An unknown sender messages the bot. They are not in allowFrom and not in the pairing store.
  4. 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)
  5. The sender shares the code with you (out-of-band — in person, via another chat, etc.).
  6. You approve the pairing. Run oxicrab pairing approve {code}. This validates the code and adds the sender's ID to the pairing_allowlist table in the workspace database.
  7. 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 allowFromAllowedAllowedAllowed
Sender in pairing storeAllowedAllowedAllowed
Unknown senderSilent dropPairing code sentAllowed
Empty allowFrom, no pairingsAll deniedAll get pairing codesAll allowed
Tip: Start with "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.

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:

Leave channel empty for self-approval (buttons appear in the same conversation as the user).