Channels API
Messaging-platform channel bridges (Telegram, Discord, Slack, WhatsApp, Signal, Matrix, Email, Teams, Mattermost, webhook, and ~35 others) — create/edit/enable/disable/delete a channel bridge (Issue #1188).
Persistence is a SQLite table (core_channels, via the shared CoreDb) —
not config.toml. Any hand-edited [channels.*] section in
config.toml is not read; channels only exist if created through this API
(or the console's Settings › Channels screen, which calls the same routes).
A channel's identity is its operator-chosen id (e.g.
"claims-webhook") — distinct from kind (the registered adapter type,
e.g. "webhook"), which is not required to be unique. Multiple channels
of the same kind are supported (registering several webhooks to push
different workflow events is the motivating case).
Every mutation below takes effect on the live adapter set immediately —
create/update/enable/disable start, restart, or stop the
corresponding adapter as part of the same request; nothing here requires a
server restart to take effect.
Endpoints
| Method | Path | Action | Description |
|---|---|---|---|
| GET | /channels/kinds | ChannelRead | List registered adapter kinds |
| GET | /channels | ChannelRead | List configured channels |
| POST | /channels | ChannelManage | Create a new channel |
| GET | /channels/{id} | ChannelRead | Full detail for one channel |
| PUT | /channels/{id} | ChannelManage | Update a channel's config/enabled/routing |
| DELETE | /channels/{id} | ChannelManage | Permanently remove a channel |
| POST | /channels/{id}/enable | ChannelManage | Enable a channel |
| POST | /channels/{id}/disable | ChannelManage | Disable a channel |
ChannelRead is admin/operator/developer/viewer/auditor; ChannelManage is
admin/operator only. (The console surface exposes the same operations at
/settings/channels, with PUT /settings/channels/{id}/enable — body
{"enabled": bool} — in place of separate /enable//disable routes.)
GET /channels/kinds
Registered adapter kinds actually reachable via the channel factory — feeds a "New Channel" kind selector so it only offers kinds that can really be constructed.
curl -H "Api-Version: v1" -H "Authorization: Bearer hzn_sk_..." \
http://localhost:4200/channels/kinds
Response (200)
{ "kinds": ["webhook", "slack", "discord", "telegram", "mqtt", "..."] }
GET /channels
curl -H "Api-Version: v1" -H "Authorization: Bearer hzn_sk_..." \
http://localhost:4200/channels
Response (200) — ChannelDetail[]
{
"channels": [
{
"id": "claims-webhook",
"kind": "webhook",
"status": "Connected",
"enabled": true,
"default_agent": "550e8400-e29b-41d4-a716-446655440000",
"default_workflow": null,
"config": { "listen_port": 9301, "secret": "••••••••" }
},
{
"id": "slack",
"kind": "slack",
"status": "Disabled",
"enabled": false,
"default_agent": null,
"default_workflow": null,
"config": {}
}
]
}
status is one of "Connected", "Disabled", {"Error": "<message>"},
"NotConfigured". Any password/secret config field (the two adapters —
mqtt, webhook — that take a raw credential rather than an *_env
env-var-name reference) is redacted to the placeholder "••••••••" in
every response; the real value is never returned in plaintext.
GET /channels/{id}
Same ChannelDetail shape as one element of the list above.
curl -H "Api-Version: v1" -H "Authorization: Bearer hzn_sk_..." \
http://localhost:4200/channels/claims-webhook
POST /channels
Create a new channel. id must be unique; kind must be one of the values
GET /channels/kinds returns. The config is dry-run validated (constructed,
never started) before being persisted, so a channel can't be saved in a
shape nothing can actually connect with.
curl -X POST -H "Api-Version: v1" -H "Authorization: Bearer hzn_sk_..." \
-H "Content-Type: application/json" \
-d '{
"id": "claims-webhook",
"kind": "webhook",
"config": { "listen_port": 9301, "secret": "s3cr3t" },
"enabled": true,
"default_agent": "550e8400-e29b-41d4-a716-446655440000"
}' \
http://localhost:4200/channels
Response (201) — the created ChannelDetail (secret redacted)
A duplicate id returns 409; an unknown kind or a config that fails
dry-run construction returns 400.
PUT /channels/{id}
Full-replace update of config/enabled/default_agent/default_workflow.
kind is fixed at create time and can't be changed. Resubmit the exact
redaction placeholder ("••••••••") for a password/secret field to leave
its real stored value unchanged — since GET never returns it in plaintext,
that's the only way a client that fetched-then-edited can preserve it.
Submitting any other value sets it to that new value.
curl -X PUT -H "Api-Version: v1" -H "Authorization: Bearer hzn_sk_..." \
-H "Content-Type: application/json" \
-d '{
"config": { "listen_port": 9302, "secret": "••••••••" },
"enabled": true
}' \
http://localhost:4200/channels/claims-webhook
Response (200) — the updated ChannelDetail
DELETE /channels/{id}
Permanently removes the channel and stops its live adapter.
curl -X DELETE -H "Api-Version: v1" -H "Authorization: Bearer hzn_sk_..." \
http://localhost:4200/channels/claims-webhook
Response (204)
POST /channels/{id}/enable
curl -X POST -H "Api-Version: v1" -H "Authorization: Bearer hzn_sk_..." \
http://localhost:4200/channels/claims-webhook/enable
Response (200)
{"status": "enabled", "channel": "claims-webhook"}
POST /channels/{id}/disable
curl -X POST -H "Api-Version: v1" -H "Authorization: Bearer hzn_sk_..." \
http://localhost:4200/channels/claims-webhook/disable
Response (200)
{"status": "disabled", "channel": "claims-webhook"}
Known gap: per-instance inbound routing
Creating/editing/deleting a second channel of the same kind works
correctly, and outbound sending from any configured channel works
correctly. Inbound message routing for anything past the first
configured instance of a given kind does not yet reliably reach that
instance's intended agent/workflow — every adapter hardcodes its kind as
its dispatch identity, not its id. Tracked as a follow-up (#1216).
Related
- agents.md — the
default_agenta channel routes messages to - reference/cli/channel.md — the CLI equivalent of this API