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-profileCreate 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