Communication channels

Channels connect an identity to an external platform. Messages come in through a webhook or a poll, bridge to the internal message store, and go out through the adapter. Conversation state stays in Atamaia, not in the vendor.

This is not a chatbot framework.

Step-by-step setup: Channels setup.

All endpoints require authentication: send Authorization: Bearer atamaia_YOUR_KEY (an identity key) or a short-lived JWT. There is no Authorization: ApiKey scheme and the server does not read X-Api-Key. Responses are wrapped in ApiEnvelope<T>; unreadMessages reports the caller identity's unread inbox count on every response. Canonical host: https://api.atamaia.ai.

Permissions: ChannelView, ChannelCreate, ChannelEdit, ChannelDelete, ChannelSend.

Flow

Inbound: POST /api/channels/webhook/{connectorGuid} is anonymous. The service verifies the delivery's platform signature (see Authenticity), then finds the connector, finds enabled inbound bindings, checks the binding's allowed senders, parses a platform-agnostic envelope, detects mentions, creates an internal message, records a mapping, and publishes an inbound event. A delivery that fails verification or the sender allowlist is logged and dropped: no message, no mapping, no event.

Webhook deliveries are deduplicated per binding on the external message id — a platform retry of the same event cannot bridge a second internal message. (Edits and deletions reuse the original message's id and pass through.)

An external thread maps to ONE internal conversation: a reply is filed under the same internal thread as the message it answers, resolved through the mapping table. If the external thread's root was never bridged, the first bridged member anchors the conversation and later replies converge onto it.

Polled deliveries (email, feeds) pass through the same sender allowlist, the same mention detection, and publish the same inbound event as webhooks — the two inbound paths behave identically.

Slack's endpoint-registration handshake (url_verification) is answered verbatim: the challenge is echoed as the raw response body. Nothing is bridged for that delivery.

Outbound: POST /api/channels/send finds the binding, decrypts credentials, sends, records the mapping, and publishes an outbound event. Decrypt fails closed. A failed decrypt does not fall back to plaintext.

The webhook returns { "status": "processed" } or { "status": "ignored" } — a rejected delivery is "ignored", never a 401, so a vendor retry is not bounced on auth. The Warning-level log is the record of every rejection.

Wake rules

A bridged message does not automatically start a run. Each binding's triggerRuleJson is the spend gate, layered on top of the allowed-senders delivery gate:

{ "wake": "mention", "dm_always_wakes": true, "min_seconds_between_wakes": 900, "max_runs_per_day": 10 }

wake is mention (the default), all, or none. Direct messages wake by default regardless of mode — a DM is someone addressing you (dm_always_wakes turns that off). Guards: a binding wakes at most once per min_seconds_between_wakes seconds and at most max_runs_per_day times per UTC day; a suppressed wake is logged and the message still sits in the identity's inbox. Malformed JSON falls back to the defaults, never to a wider surface.

Human-originated messages bypass quiet hours by design: quiet hours govern routine schedules and check-in notifications, not someone @-mentioning the bot at 23:00.

The tenant may designate one primary bot (PUT /api/identities/{id}/bot-profile with { "isPrimary": true }). Unaddressed channel messages — no mention, not a DM — are delivered to the primary's inbox instead of the binding's own identity. One receptionist; fan-out to every bot in a channel is refused by design. Whether such a message wakes anyone still follows the binding's rule.

Replies

When a wake-triggered run finishes, its final output is delivered back through the originating binding automatically — threaded to the message that triggered the run where the platform supports threads. This is the default behaviour, not something the model has to remember to do: an agent-initiated send via channel_send still exists for proactive messages, and if the run already used it, the automatic reply is suppressed rather than doubled.

The reply is delivered only for runs the channel trigger created, so it adds no inference spend of its own. Set Agents:EventReactor:AutoReply to false to suppress it. A reply that cannot be delivered — disabled binding, adapter failure — is logged at Error and traced onto the run, never silently dropped.

Authenticity and senders

The receiver is anonymous by design — external platforms cannot hold our credentials — so authenticity comes from the delivery's own signature. Verification is fail-closed and happens before any binding is processed. Each platform's material lives in the connector's credentials:

Adapter Credential What is checked
slack signing_secret v0 HMAC over v0:{timestamp}:{body}, 5-minute replay window
discord public_key Ed25519 signature over timestamp + body (the developer-portal public key), 5-minute replay window
telegram secret_token constant-time comparison of the X-Telegram-Bot-Api-Secret-Token header
webhook secret_header + secret_value constant-time comparison of the named header

An adapter that does not implement authenticity support is refused outright. A connector that genuinely cannot verify may set "allow_unverified_webhooks": true in its connector config; that choice is logged at Warning on every delivery that uses it.

Per binding, allowedSendersJson narrows who may come through at all: a JSON array of external sender ids (falling back to sender names when the envelope has no id), matched exactly and case-insensitively. null is unrestricted. An empty array denies everyone; malformed JSON denies everyone — a typo never widens the gate. This is the DELIVERY gate; triggerRuleJson is the SPEND gate.

Connector and binding

A connector is one set of credentials for one platform. Credentials are encrypted at rest. A binding routes one external channel through that connector.

Field Meaning
connectorId Credentials
adapterTypeKey slack, discord, telegram, twitter, and so on
externalChannelId Platform channel id
identityId Which identity speaks and receives
botExternalUserId Bot user id, for mention detection
direction Inbound, Outbound, or Bidirectional
channelConfigJson Adapter-specific config
allowedSendersJson Who may bridge through this binding (delivery gate); null = unrestricted
triggerRuleJson What may wake the identity to run (spend gate); null = defaults
enabled Whether the binding is active

Call GET /api/channels/adapters before creating a binding, so the type key is one the server has registered.

Adapters

Key Platform Notes
slack Slack Send, receive, edit, delete, react, thread, mention. Events API inbound with retry dedup. DM detection
discord Discord Send, receive, edit, delete, react, thread, mention. DM detection
telegram Telegram Send, receive, edit, delete, mention. DM detection
twitter X Send and delete. Outbound only. Auto-threads past 280 characters
email SMTP/IMAP Send, receive, poll. Allowlists and mention detection apply to polled mail too
webhook Generic webhook Send, receive
homeassistant Home Assistant Send, event
n8n n8n Event, receive
feed RSS/Atom Receive, poll. Allowlists and mention detection apply to polled feeds too

Categories: messaging, automation, email, webhook, feed.

Matrix and Signal are not offered: the previous stubs advertised capabilities they did not have, so they were removed rather than left throwing. They return when a real implementation lands.

Send

POST /api/channels/send
{
  "channelBindingId": 3,
  "content": "Research complete.",
  "threadId": "optional"
}

The result is success, externalMessageId, and error. HTTP 400 when success is false.

GET /api/channels/bindings/{bindingId}/mappings correlates an Atamaia message with the external message and thread.

Bindings

GET    /api/channels/bindings
POST   /api/channels/bindings
GET    /api/channels/bindings/{id}
PUT    /api/channels/bindings/{id}
DELETE /api/channels/bindings/{id}
GET    /api/channels/adapters
POST   /api/channels/webhook/{connectorGuid}
GET    /api/channels/bindings/{bindingId}/mappings
GET    /api/identities/{id}/bot-profile
PUT    /api/identities/{id}/bot-profile

Create requires connectorId, adapterTypeKey, externalChannelId, and name. Direction defaults to bidirectional. Delete is a soft delete.

MCP tools: channel_binding_list, channel_binding_get, channel_binding_create, channel_binding_update, channel_binding_delete, channel_send, channel_adapter_list, channel_mapping_list. Channel tools are opt-in on an agent profile. They are not on by default. See Agents.

Connectors themselves are on the connector API, not this controller.

HTTP surface

Method Path Summary Responses
GET /api/channels/adapters Adapters available on this deployment — only those whose operations are implemented are listed. 200 ChannelAdapterInfo[]
GET /api/channels/bindings List channel bindings (paged). 200 PagedResultOfChannelBindingDto
POST /api/channels/bindings Route an external channel to an identity. The binding's sender allowlist is the delivery gate and its wake rule the spend gate. 200 ChannelBindingDto
GET /api/channels/bindings/{bindingId}/mappings Message mappings for a binding — the external-to-internal thread anchor table. 200 PagedResultOfChannelMessageMappingDto
DELETE /api/channels/bindings/{id} Delete a binding. Existing message mappings are kept for thread resolution. 200
GET /api/channels/bindings/{id} One channel binding. 200 ChannelBindingDto
PUT /api/channels/bindings/{id} Update a binding's routing, sender allowlist, or wake rule. 200 ChannelBindingDto
POST /api/channels/send Send a message out through a binding — decrypts credentials, records the mapping, publishes the outbound event. Decrypt fails closed. 200 ChannelSendResult
POST /api/channels/webhook/{connectorGuid} Platform delivery webhook — anonymous by design. Signature-verified fail-closed before any binding is processed; deliveries are deduplicated per binding on the external message id; a rejected delivery is "ignored", never a 401, so a vendor retry is not bounced on auth. 200

See also: Channels setup — Slack, end to end

Last updated