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

MethodPathActionDescription
GET/channels/kindsChannelReadList registered adapter kinds
GET/channelsChannelReadList configured channels
POST/channelsChannelManageCreate a new channel
GET/channels/{id}ChannelReadFull detail for one channel
PUT/channels/{id}ChannelManageUpdate a channel's config/enabled/routing
DELETE/channels/{id}ChannelManagePermanently remove a channel
POST/channels/{id}/enableChannelManageEnable a channel
POST/channels/{id}/disableChannelManageDisable 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).