Skip to main content

Channels

Connect an agent to WhatsApp, Discord, and other messaging surfaces.

Overview

The customer configures the agent; the platform runs the conversation. A channel connection is established via embedded signup / OAuth / keys, and always carries a default — what answers when nothing else matches. A project's own route table can answer differently per surface and condition, and an address — the identifier a message arrived at — can override both for one identifier. Each conversation maps 1:1 to a Session.

Two kinds are connectable today — whatsapp and discord. They share everything above: the same channel, binding and conversation resources, the same address continuity. Only the transport differs, which is why adding a kind adds no concepts, just the fields that kind needs.

See the OpenAPI spec for the full endpoint and schema reference, or browse it rendered under API Reference → Channels.

Data Model

Channel

FieldTypeDescription
idstringPublic channel connection ID.
project_idstringThe owning project.
channelstringThe channel kind (whatsapp or discord).
statusstringactive or disabled.
credential_sourcestring, nullableWhere this connection's credentials come from.
phone_number_idstring, nullableWhatsApp-specific identifier.
waba_idstring, nullableWhatsApp Business Account identifier.
application_idstring, nullableDiscord application id. One channel per application.
modesobject, nullableDiscord only — { dms, mention_threads }, the flows this channel serves. Null for other kinds.
defaultobjectWhat happens when no route matches and the address has no action of its own — { action, agent_id, text, repeat, language, config }. Required at connect.
has_credentialbooleanWhether a credential is configured — never returns the credential itself.
created_atstring (date-time)
updated_atstring (date-time)

Key Concepts

The channel default

Every channel carries a default action — agent (answer with an agent), message (deliver fixed text, no session created), or silence (answer nothing) — required on POST /v1/projects/{project_id}/channels and patchable through PATCH /v1/projects/{project_id}/channels/{channel_id}. It is what answers when nothing more specific does: no route matches, and the address has no action of its own. Connecting a channel without saying what it does is the misconfiguration, so it is caught here rather than discovered as silence in production.

The same three-field shape (action / agent_id or text / repeat) is what a route and an address carry too — one shape, learned once.

Do not bind an agent that has an output_schema

A channel delivers the assistant's reply text verbatim to a person. An agent configured with an output_schema returns its answer as JSON, and a session turn has nowhere to put a parsed object — so the JSON arrives as the message text and the human on the other end reads {"approved":true}.

Structured output is for agents you call through POST /v1/projects/{project_id}/agents/{agent_id}/generations or a board column. Leave channel-facing agents unconstrained.

Conversations

GET /v1/projects/{project_id}/channels/{channel_id}/conversations lists conversations for a channel (filterable by identifier); each maps 1:1 to a Session. Conversations are created only by the inbound path — there is no create endpoint.

Discord

Discord adds two things no other kind has: modes, which picks whether the bot serves direct messages, server threads or both, and an allowlist deciding which server members may invoke it. Both — plus the Discord-side setup they depend on, the intents and permissions each flow needs, and why a Discord bot goes quiet — are on the Discord page.

Examples

Connect a WhatsApp channel:

naturali create-channel \
--project-id proj_V1StGXR8Z5jdHi6B \
--channel whatsapp \
--default '{ "action": "agent", "agent_id": "agent_V1StGXR8Z5jdHi6B" }'

Connect a Discord app for DMs only — the default, and the one that needs no privileged intent:

naturali create-channel \
--project-id proj_V1StGXR8Z5jdHi6B \
--channel discord \
--application-id 1290000000000000009 \
--bot-token "$DISCORD_BOT_TOKEN" \
--default '{ "action": "agent", "agent_id": "agent_V1StGXR8Z5jdHi6B" }'

Turn the thread flow on later, once MESSAGE_CONTENT is enabled on the app:

naturali update-channel \
--project-id proj_V1StGXR8Z5jdHi6B \
--channel-id chan_V1StGXR8Z5jdHi6B \
--modes '{ "dms": true, "mention_threads": true }'

Set the channel default to an agent, and restrict who may invoke it in a server:

naturali update-channel \
--project-id proj_V1StGXR8Z5jdHi6B \
--channel-id chan_V1StGXR8Z5jdHi6B \
--default '{
"action": "agent",
"agent_id": "agent_V1StGXR8Z5jdHi6B",
"config": { "discord": { "allowed_role_ids": ["1290000000000000042"] } }
}'

Per-surface and per-condition answers — a public agent in server threads, a different one in DMs, a paywall message until an identifier is admitted — are the route table and addresses.