Skip to main content

Channels

Intern receives tasks and sends replies over a channel — a chat platform you already use. At least one channel is required during setup. Telegram is the recommended default because the bot setup is the simplest.

The captive-portal wizard sets exactly one channel (the value in the channel field of POST /api/device/setup) and only accepts telegram, slack, or discord. WhatsApp is not selectable in the wizard because it requires an interactive QR-code pairing flow with the WhatsApp mobile app — it can only be added after the device is paired, via the MQTT-backed POST /api/device/channel endpoint. See WhatsApp (post-pairing only) and Adding more channels below.

Required setup fields by channel

These are the fields validated by lib/openclaw/domain/setup.go (SetupRequest.ValidateChannel). The wizard collects them; all are mandatory when the matching channel value is selected.

channelAvailable in wizard?Required fields
telegram (default)✅ yestelegram_bot_token, telegram_user_id
slack✅ yesslack_bot_token, slack_app_token, slack_user_id
discord✅ yesdiscord_bot_token, discord_guild_id, discord_user_id
whatsapp❌ no — post-pairing onlywhatsapp_user_id (E.164 phone number, e.g. +15551234567) — added via POST /api/device/channel

Create a bot

  1. Open Telegram and message @BotFather.
  2. Send /newbot, follow the prompts, choose a name and username.
  3. BotFather replies with a bot token that looks like 123456789:ABC-DEF1234ghIkl-....
  4. Send /start to your new bot, then visit https://api.telegram.org/bot<TOKEN>/getUpdates in a browser. The JSON response contains your numeric chat.id — that's your telegram_user_id.

Enter in the wizard

FieldValue
Channeltelegram
Bot tokenthe string from BotFather
User IDyour numeric chat.id from getUpdates

intern-server writes these to /root/config/config.json:

{
"telegram_bot_token": "...",
"telegram_user_id": "..."
}

They're then merged into /root/.openclaw/openclaw.json under channels.telegram:

{
"channels": {
"telegram": {
"enabled": true,
"botToken": "...",
"dmPolicy": "allowlist",
"allowFrom": ["<your-telegram_user_id>"]
}
}
}

dmPolicy: "allowlist" with a populated allowFrom is what restricts your bot to responding only to you. Set allowFrom to ["*"] and dmPolicy: "open" if you want anyone who finds the bot to be able to message it (not recommended).

Slack

Slack requires two tokens because the gateway runs in Socket Mode:

  • Bot token (xoxb-…) — issued when you install the app to a workspace
  • App-level token (xapp-…) — issued under "Basic Information" → "App-Level Tokens" with the connections:write scope

Set up the app:

  1. Create a Slack app at api.slack.com/appsFrom scratch.
  2. Under OAuth & Permissions, grant the bot the scopes Slack requires for the features you want (Slack's docs are the source of truth for the exact scope list; check lib/openclaw/openclaw.go applySlackChannelConfig for the channel's runtime requirements).
  3. Install the app to your workspace; copy the Bot User OAuth Token (xoxb-…).
  4. Under Basic InformationApp-Level Tokens, create one with scope connections:write (xapp-…).
  5. Enable Socket Mode.
  6. Get your Slack user ID by clicking your profile → "More" → "Copy member ID" (it starts with U).

In the wizard, pick slack and paste all three. The merged channels.slack block sets mode: "socket", botToken, appToken, and the canonical channel options.

Discord

Discord requires:

  • A bot token from the Discord Developer Portal
  • A guild ID (server ID) that the bot has joined
  • A user ID (your Discord numeric ID, not your username)

Set up the app:

  1. Discord Developer PortalNew ApplicationBotReset Token.
  2. Enable Message Content Intent (and any other intents you need).
  3. Invite the bot to your server with the OAuth2 URL Generator — bot scope, plus message permissions.
  4. Enable Developer Mode in Discord (User Settings → Advanced), then right-click your server → "Copy Server ID" (guild ID), and right-click your name → "Copy User ID".

In the wizard, pick discord and paste the bot token, guild ID, and user ID.

WhatsApp (post-pairing only)

WhatsApp is not a valid value for the captive-portal wizard's channel field (SetupRequest.EffectiveChannel intentionally rejects it). It can only be attached after the device is paired and set_up_completed: true, by issuing an MQTT command to the device.

Requirements

  • The device must already be paired via Telegram, Slack, or Discord and be online on Wi-Fi.
  • A WhatsApp account on your phone — your everyday personal account is fine. The Intern uses WhatsApp's standard Linked Devices feature (the same one that powers WhatsApp Web / Desktop), so any account that can scan a QR works.
  • The Intern runs OpenClaw (the default). WhatsApp is not supported on the Hermes agent/api/device/setup rejects WhatsApp creds when active_agent=hermes. See Setup Hermes → Overview.

Required field

FieldFormatWhat it controls
whatsapp_user_idE.164 phone number, e.g. +15551234567The phone number allowed to DM the device. The simplest setup is to put your own number here and use WhatsApp's "Message Yourself" chat — you send a task into that chat, the linked Intern reads it and replies in the same chat.

Pairing flow

The device exposes WhatsApp pairing as a streaming MQTT exchange — backend sends one add_channel command, the device replies with many events until pairing succeeds, times out, or fails.

  1. Backend publishes on the device's fa_channel topic:
    { "cmd": "add_channel", "channel": "whatsapp", "config": { "user_id": "+15551234567" } }
  2. Device runs openclaw channels add --channel whatsapp, overlays dmPolicy: "allowlist" + allowFrom: ["+15551234567"] into openclaw.json, enables the WhatsApp plugin (openclaw plugins enable whatsapp), and restarts the gateway.
  3. Device streams events on fd_channel (one MQTT publish per event):
    • status: "pairing_starting"
    • status: "pairing_qr" with pairing_qr_text (Unicode-block QR), pairing_qr_seq (rotates ~every 20s; backend should discard older sequences), and pairing_expires_at
    • status: "success" once the device has saved Baileys credentials, or status: "timeout" | "failure" on error
  4. On your phone, open WhatsApp → SettingsLinked DevicesLink a Device and scan the QR rendered by the backend from pairing_qr_text.
  5. On success, the CLI prints ✅ Linked after restart; web session ready. and the device publishes status: "success". Any message from a number in allowFrom is then routed to the LLM — if you put your own number there, just open the Message Yourself chat in WhatsApp and start typing.

If a session is already saved at <openclaw_config_dir>/credentials/whatsapp/default/creds.json, the device publishes status: "success" immediately and skips the QR step — Baileys auto-reconnects. (Fresh pairings and resumed sessions now share the same terminal status; the older session_resumed status has been retired to keep the wire vocabulary minimal — see app/intern/internal/device/service.go.)

Re-pairing after session loss

If the WhatsApp session dies (you tapped Log out on the phone's Linked Devices screen, the phone's SIM was offline for 14+ days, or someone ran openclaw channels logout on the device), the backend re-runs the flow with:

{ "cmd": "whatsapp_pair" }

No config payload is needed — the device reuses the previously-stored whatsapp_user_id and streams a fresh QR. Reboots, gateway crashes, and OS restarts do not lose the session (Baileys auto-reconnects); factory reset (10s long-press) does, because it wipes /root/.openclaw.

Adding more channels

Post-setup, additional channels are configured via the MQTT-backed POST /api/device/channel endpoint, which takes the same per-channel fields plus channel: "telegram" | "slack" | "discord" | "whatsapp". WhatsApp is only addable this way — it's not a valid value for the initial setup wizard's channel field.

Channel tokens at rest

All channel tokens are stored plaintext in /root/config/config.json and in the canonical channel blocks under channels.{telegram,slack,discord} in /root/.openclaw/openclaw.json. Both files live on the device — none of these tokens are uploaded to Autonomous backend servers. Protect physical access to the device; use a dedicated bot account per channel; and rotate the tokens through the wizard / channel endpoint if you suspect they've leaked.