/v1 endpoint for this, and there does not need to be one. The agent already owns its channels: it knows which ones it supports, what each one needs, and whether each is connected right now. Your backend reaches that with POST /v1/instances/{id}/exec and relays it to your own UI.
Paste this into your coding agent
agent37-platform/starter-kit
A working implementation of everything on this page: the Messaging tab of the white-label dashboard, with the Telegram flow, the WhatsApp QR, and a generic credentials form for every other channel.
How it works
The agent serves a messaging API on port9119 inside the instance, on the loopback interface only. Nothing on the public internet can reach it, which is the point: credentials go in over your own authenticated backend, never over a URL a user could find.
Every call has the same shape. Fetch the agent’s session token out of its own UI, then call the API with it:
curl
stdout. A sleeping instance is woken by the exec, so the first call after a sleep takes about ten seconds longer.
The channel catalog
GET /api/messaging/platforms returns every channel the agent supports, its live state, and the credentials it wants. Build your UI from this rather than from a list you maintain: a channel the agent gains in a later image appears on its own.
response
string
The live state of the channel:
disabled until you switch it on, then connected once the gateway has it running. A channel that failed to start reports startup_failed and puts the reason in error_message.boolean
Whether the channel’s required credentials are already stored on the instance. Secrets are never returned, only
is_set and a redacted_value.string
Plain-language failure, safe to show a user. The agent keeps the last one even after the channel is switched off, so read it only while
enabled is true.array
The credentials this channel wants:
prompt is the label, help and url say where to get the value, is_password marks a secret, and advanced marks a field to hide behind a disclosure. Fields with required: false can be left empty.relay, whatsapp_cloud, and other in-agent setups) report no env_vars at all. They cannot be connected from a form, so leave them off your list.
Connect a channel
PUT /api/messaging/platforms/{id} writes credentials and switches a channel on.
request
response
boolean
true when the running gateway picked the change up live. false means it did not, and the channel connects only after POST /api/gateway/restart. Restarting the messaging gateway does not touch the Agent API on the instance’s default port, so chat keeps working throughout.is_set: true with no value, so omitting it leaves what the instance already holds.
To disconnect, switch the channel off and forget its credentials in the same call:
request
Telegram
Telegram is the one channel worth building a real flow around, because it reaches the agent through a webhook. Agent37 wires that for you: creating an instance on a Hermes or OpenClaw template mints a public port for it and hands the containerTELEGRAM_WEBHOOK_URL and TELEGRAM_WEBHOOK_SECRET. Telegram delivers straight to the instance and wakes it if it is asleep, so a Telegram agent answers without your app doing anything.
Three steps, and the middle one is the one people skip.
1
Take a bot token
Your user opens @BotFather, sends
/newbot, picks a name, and pastes the token back into your app.2
Check it before you write it
Call Telegram’s The
getMe from your backend first. The agent’s messaging gateway refuses to start on a rejected token, and a token written unchecked takes every other channel on that agent down with it.curl
result.username it returns is the bot’s @handle. Show it: your user needs it in the next step.3
Learn who owns the bot
Ask the user to send any message to their new bot, then read it back with A fresh bot has no webhook, so long polling works here. Once the agent connects, Telegram switches the bot to the webhook and
getUpdates. The message.from.id in the first update is the person to allow, and they never have to look up a numeric user id.curl
getUpdates starts answering 409, which is how you can tell a bot is already wired to something.4
Write it
request
POST /api/gateway/restart. Poll the catalog until Telegram reports state: "connected", and send your user to https://t.me/<username>.1
Start a pairing
curl
response
2
Poll for the code
curl
status walks installing to starting to waiting. At waiting, qr_payload holds the string to render as a QR code for the user to scan from Settings, Linked devices, Link a device. WhatsApp rotates it every few seconds, so keep polling and keep redrawing. expired means start over.3
Save the link
A landed scan reports It answers
status: "connected" with the linked account_phone. Saving it restarts the gateway, about a minute, so tell the user the scan worked before you call this rather than after:curl
{ "ok": true } once the number is saved as the allowlist.WhatsApp holds an open connection from the agent, so an instance that sleeps takes WhatsApp offline with it. Turn auto-sleep off on instances that use it. Telegram has no such problem: its webhook wakes a sleeper.
Slack, Discord, and the rest
Every other channel is a credentials form, and the agent already told you what to put on it. Renderenv_vars, PUT the values, restart if hot_served is false, and show state and error_message back. One form connects all of them, including channels shipped in an image you build yourself.
The docs_url on each channel points at whoever issues its credentials (Slack’s app console, Discord’s developer portal), and each field’s url points at the exact page for that value. Link them rather than writing your own setup guide: they stay current and you do not.
Doing this on an instance running an agent other than Hermes? The messaging API on
9119 is Hermes’s. OpenClaw carries its own channels and its own CLI for them; anything else is whatever your image ships. The pattern is the same either way: your backend drives the agent’s own configuration through exec.