primer docs

Channels API

Channel providers hold the credentials for a messaging platform (Slack, Telegram, or Discord). Channels are the individual conversational rooms within a provider (a Slack channel, a Telegram chat id, a Discord channel). A Workspace can carry a reply_binding that names the Channel all session gates (ask_user, tool approval, inform) from its sessions forward to. Chat config lives on the Channel itself (config.chats).

How chats, ask_user, and the channel bridge fit together.

Configure channel providers in the console.

Endpoints

Method Path Summary
GET /v1/channel_providers List channel providers
POST /v1/channel_providers Create a channel provider
POST /v1/channel_providers/find Find channel providers by predicate
GET /v1/channel_providers/{entity_id} Get a channel provider
PUT /v1/channel_providers/{entity_id} Replace a channel provider
DELETE /v1/channel_providers/{entity_id} Delete a channel provider
GET /v1/channels List channels
POST /v1/channels Create a channel
POST /v1/channels/find Find channels by predicate
GET /v1/channels/{entity_id} Get a channel
PUT /v1/channels/{entity_id} Replace a channel
DELETE /v1/channels/{entity_id} Delete a channel
PUT /v1/workspaces/{workspace_id}/reply_binding Set workspace channel association
DELETE /v1/workspaces/{workspace_id}/reply_binding Clear workspace channel association

ChannelProvider object

{
  "id": "slack-main",
  "provider": "slack",
  "config": {
    "app_token": "**redacted**",
    "bot_token": "**redacted**",
    "signing_secret": "**redacted**"
  }
}
Field Required Type Description
id no string Identifier. If omitted, the server assigns a type-prefixed id (e.g. channel-provider-3f9a1c8d). Immutable after creation
provider yes string One of "slack", "telegram", "discord"
config yes ProviderConfig Platform-specific credentials (see below)

Token fields are write-only: the API accepts them on POST/PUT but returns redacted placeholders on GET. Raw token values never appear in responses.

Slack config: requires Socket Mode enabled on the Slack app:

Field Required Description
app_token yes App-level token starting with xapp- (generated in Basic Information)
bot_token yes Bot OAuth token starting with xoxb-
signing_secret no Webhook signing secret (optional)

Token format is validated server-side: an app_token not starting xapp- or a bot_token not starting xoxb- returns 422 /errors/validation-error.

Telegram config:

Field Required Description
bot_token yes Bot token in the <id>:<hash> shape generated by @BotFather; must be at least 20 chars and contain a colon
poll_timeout_seconds no Long-poll timeout per getUpdates call (1-60, default 25)

Discord config:

Field Required Description
bot_token yes Discord bot token from the Developer Portal; do NOT include the Bot prefix (the adapter adds it)
enable_dms no Request dm_messages intent for DM channels; default true

Create a channel provider

POST /v1/channel_providers - returns 201 Created.

curl -X POST https://your-host/v1/channel_providers \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "slack-main",
    "provider": "slack",
    "config": {
      "app_token": "xapp-1-ABC123",
      "bot_token": "xoxb-1-DEF456",
      "signing_secret": "s3cr3t"
    }
  }'
import httpx
r = httpx.post(
    "https://your-host/v1/channel_providers",
    headers={"Authorization": f"Bearer {token}"},
    json={
        "id": "slack-main",
        "provider": "slack",
        "config": {
            "app_token": "xapp-1-ABC123",
            "bot_token": "xoxb-1-DEF456",
            "signing_secret": "s3cr3t",
        },
    },
)
assert r.status_code == 201
const r = await fetch("/v1/channel_providers", {
  method: "POST",
  headers: {"Authorization": `Bearer ${token}`, "Content-Type": "application/json"},
  body: JSON.stringify({
    id: "slack-main",
    provider: "slack",
    config: {
      app_token: "xapp-1-ABC123",
      bot_token: "xoxb-1-DEF456",
      signing_secret: "s3cr3t"
    }
  })
})

Errors:

  • 422 - token format validation failed (bad prefix, wrong shape, or missing required field)
  • 409 - a provider with this id already exists

To create a Telegram provider:

curl -X POST https://your-host/v1/channel_providers \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "telegram-bot",
    "provider": "telegram",
    "config": {"bot_token": "1234567890:ABCDEFGHIJ1234567890"}
  }'
import httpx
r = httpx.post(
    "https://your-host/v1/channel_providers",
    headers={"Authorization": f"Bearer {token}"},
    json={
        "id": "telegram-bot",
        "provider": "telegram",
        "config": {"bot_token": "1234567890:ABCDEFGHIJ1234567890"},
    },
)
assert r.status_code == 201
const r = await fetch("/v1/channel_providers", {
  method: "POST",
  headers: {"Authorization": `Bearer ${token}`, "Content-Type": "application/json"},
  body: JSON.stringify({
    id: "telegram-bot",
    provider: "telegram",
    config: {bot_token: "1234567890:ABCDEFGHIJ1234567890"}
  })
})

Get a channel provider

GET /v1/channel_providers/{entity_id} - returns 200 OK. Token fields are redacted.

curl https://your-host/v1/channel_providers/slack-main \
  -H "Authorization: Bearer $TOKEN"
import httpx
r = httpx.get("https://your-host/v1/channel_providers/slack-main",
              headers={"Authorization": f"Bearer {token}"})
got = r.json()
# got["config"]["bot_token"] is redacted; never the raw value
const r = await fetch("/v1/channel_providers/slack-main", {
  headers: {"Authorization": `Bearer ${token}`}
})
const cp = await r.json()

Errors: 404 if the id does not exist.

List channel providers

GET /v1/channel_providers - returns an offset or cursor page.

Query parameters: limit (1-200, default 20), offset (default 0), cursor, order_by.

curl "https://your-host/v1/channel_providers?limit=20" \
  -H "Authorization: Bearer $TOKEN"
import httpx
r = httpx.get("https://your-host/v1/channel_providers",
              headers={"Authorization": f"Bearer {token}"})
providers = r.json()["items"]
const r = await fetch("/v1/channel_providers?limit=20", {
  headers: {"Authorization": `Bearer ${token}`}
})
const {items} = await r.json()

Channel object

{
  "id": "ch-general",
  "provider_id": "slack-main",
  "provider": "slack",
  "external_id": "C0123ABCDEF",
  "label": "#general",
  "config": {
    "chats": {
      "enabled": false,
      "default_agent": null,
      "allow_agent_switch": false,
      "allowed_agents": [],
      "relay_mode": "final"
    }
  }
}
Field Required Type Description
id no string Identifier. If omitted, the server assigns a type-prefixed id (e.g. channel-3f9a1c8d). Immutable after creation
provider_id yes string Id of the parent ChannelProvider; must exist (FK validated at create time)
provider yes string Platform enum: "slack", "telegram", or "discord". Must match the referenced provider's platform
external_id yes string Platform-side channel id (Slack channel id, Telegram chat id, Discord channel snowflake)
label no string Human-readable label (max 200 chars, default null)
config no ChannelConfig Per-room config; provider-discriminated (see below). Defaults to an empty config for the platform

The pair (provider_id, external_id) must be unique. A duplicate returns 409 /errors/conflict. A non-existent provider_id returns 422, not 409.

Channel config is provider-discriminated. Each variant has a chats block:

Field Required Type Description
config.chats.enabled no boolean Default false. When true, incoming messages on this room start primer chats
config.chats.default_agent yes when enabled string Agent id each new chat begins with; required when chats.enabled=true
config.chats.allow_agent_switch no boolean Default false. When false, the /agent command is disabled and allowed_agents is ignored
config.chats.allowed_agents no list of strings Only applies when allow_agent_switch=true: restricts /agent to these agent ids; [] means any agent is allowed
config.chats.relay_mode no string "final" (default) or "all". Controls which assistant turns are relayed back to the platform

Create a channel

POST /v1/channels - returns 201 Created.

curl -X POST https://your-host/v1/channels \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "ch-general",
    "provider_id": "slack-main",
    "provider": "slack",
    "external_id": "C0123ABCDEF",
    "label": "#general"
  }'
import httpx
r = httpx.post(
    "https://your-host/v1/channels",
    headers={"Authorization": f"Bearer {token}"},
    json={
        "id": "ch-general",
        "provider_id": "slack-main",
        "provider": "slack",
        "external_id": "C0123ABCDEF",
        "label": "#general",
    },
)
assert r.status_code == 201
const r = await fetch("/v1/channels", {
  method: "POST",
  headers: {"Authorization": `Bearer ${token}`, "Content-Type": "application/json"},
  body: JSON.stringify({
    id: "ch-general",
    provider_id: "slack-main",
    provider: "slack",
    external_id: "C0123ABCDEF",
    label: "#general"
  })
})

To enable chats on the channel:

curl -X POST https://your-host/v1/channels \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "ch-helpdesk",
    "provider_id": "slack-main",
    "provider": "slack",
    "external_id": "C0456DEFGHI",
    "label": "#helpdesk",
    "config": {
      "chats": {
        "enabled": true,
        "default_agent": "helpdesk-agent",
        "relay_mode": "final"
      }
    }
  }'
import httpx
r = httpx.post(
    "https://your-host/v1/channels",
    headers={"Authorization": f"Bearer {token}"},
    json={
        "id": "ch-helpdesk",
        "provider_id": "slack-main",
        "provider": "slack",
        "external_id": "C0456DEFGHI",
        "label": "#helpdesk",
        "config": {
            "chats": {
                "enabled": True,
                "default_agent": "helpdesk-agent",
                "relay_mode": "final",
            }
        },
    },
)
assert r.status_code == 201
const r = await fetch("/v1/channels", {
  method: "POST",
  headers: {"Authorization": `Bearer ${token}`, "Content-Type": "application/json"},
  body: JSON.stringify({
    id: "ch-helpdesk",
    provider_id: "slack-main",
    provider: "slack",
    external_id: "C0456DEFGHI",
    label: "#helpdesk",
    config: {
      chats: {enabled: true, default_agent: "helpdesk-agent", relay_mode: "final"}
    }
  })
})

Errors:

  • 422 - provider_id does not exist, provider mismatches the provider's platform, chats.enabled=true without chats.default_agent, or required field missing
  • 409 - (provider_id, external_id) already registered

Workspace channel association

A workspace can be linked to a channel so that all session gates (ask_user, tool approval, inform) from its sessions forward to that channel. The association is a single nullable field on the workspace row, not a separate resource.

Set workspace channel association

PUT /v1/workspaces/{workspace_id}/reply_binding - returns 200 OK.

Body:

{"channel_id": "ch-general"}
curl -X PUT https://your-host/v1/workspaces/ws-prod/reply_binding \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"channel_id": "ch-general"}'
import httpx
r = httpx.put(
    "https://your-host/v1/workspaces/ws-prod/reply_binding",
    headers={"Authorization": f"Bearer {token}"},
    json={"channel_id": "ch-general"},
)
assert r.status_code == 200
await fetch("/v1/workspaces/ws-prod/reply_binding", {
  method: "PUT",
  headers: {"Authorization": `Bearer ${token}`, "Content-Type": "application/json"},
  body: JSON.stringify({channel_id: "ch-general"})
})

Errors:

  • 404 - workspace does not exist
  • 422 - the named channel does not exist
  • 409 - the workspace is terminating and cannot have its association changed

Clear workspace channel association

DELETE /v1/workspaces/{workspace_id}/reply_binding - returns 204 No Content.

curl -X DELETE https://your-host/v1/workspaces/ws-prod/reply_binding \
  -H "Authorization: Bearer $TOKEN"
import httpx
r = httpx.delete(
    "https://your-host/v1/workspaces/ws-prod/reply_binding",
    headers={"Authorization": f"Bearer {token}"},
)
assert r.status_code == 204
await fetch("/v1/workspaces/ws-prod/reply_binding", {
  method: "DELETE",
  headers: {"Authorization": `Bearer ${token}`}
})

Errors:

  • 404 - workspace does not exist

primectl

The same reply-binding and channel-trigger operations are available from the primectl channel command group:

primectl channel binding set <workspace_id> <channel_id>   # PUT reply_binding
primectl channel binding clear <workspace_id>              # DELETE reply_binding
primectl channel binding get <workspace_id>                # read the workspace reply_binding

primectl channel trigger create --provider <provider_id> [--channel <channel_id>] \
  --slug <slug> --name <name>

primectl channel sub create <trigger_id> --action <kind> --event-type <type> \
  [--command <name>] [--reply-target <target>] --set field=value ...

trigger create posts a channel TriggerConfig (provider-wide when --channel is omitted), and sub create assembles the action config, event_matcher, and reply_target so operators do not hand-write the discriminated config.

Errors note

Channel-provider and channel CRUD errors use the RFC 7807 ProblemDetails envelope with type, title, status, detail, instance, and extensions (which includes request_id and, for 422 errors, an errors array with field paths). The workspace channel-association routes (PUT / DELETE .../reply_binding) instead return a {"detail": {"error": "<code>", "message": "..."}} body for their 409 (workspace terminating) and 422 (channel not found) cases. See the REST API overview for details.