Communication Channels
Configure SMS, WhatsApp, and generic-webhook channels, monitor channel health, and understand which channels are fully live today.
Owlat models every conversation medium — email, native chat, SMS, WhatsApp, and a catch-all webhook — as a channel. Inbound messages from all of them land in the shared Team Inbox on a unified timeline. This page covers the Channels settings screen where owners and admins enable channels, store provider credentials, and watch health status.
Email and native chat send and receive end-to-end. Owlat also sends outbound over SMS, WhatsApp, and the generic webhook three ways: as an AI-agent reply (auto-send or approved draft) when the AI agent (ai.agent) is enabled, from the per-message Reply button in the Team Inbox thread view, and from the per-contact composer on a contact's Unified Timeline. All three are owner/admin actions and all three go through the same provider dispatch. See Current limitations.
Where to find Channels
Open Settings → Channels in the dashboard (route /dashboard/admin/instance/channels). The page itself only requires being signed in, but changing anything — enabling/disabling a channel or saving credentials — is restricted to owners and admins: the underlying updateChannelConfig mutation requires the organization:manage permission (see Team & Permissions). A non-admin who reaches this screen can read the channel list but their save and toggle actions are rejected.
The screen lists one card per channel. Each card shows the channel's icon and name, an enable/disable toggle, a health badge, and a Configure panel that expands to a credential form. A sidebar summarises totals: how many channels exist, how many are enabled, and how many are healthy, degraded, or down.
What the channels are
| Channel | What it does today |
|---|---|
| Send and receive via the built-in MTA. Fully live. No credentials to enter here — email delivery is managed by the MTA. | |
| Chat | Native in-app chat powered by the built-in messaging system. Fully live. No credentials required. |
| SMS | Receive inbound texts via Twilio. Send from the Team Inbox reply button, the contact composer, or an AI-agent reply. |
| Receive inbound messages via the WhatsApp Business (Meta Cloud) API. Send from the Team Inbox reply button, the contact composer, or an AI-agent reply. | |
| Generic webhook | Receive inbound messages from any HTTP source via a shared secret. Send from the Team Inbox reply button, the contact composer, or an AI-agent reply — each is a POST to your endpoint URL. |
What "enabled" really means
The enable toggle controls whether a channel is included in the periodic health check and surfaced as active in the overview counts. For email and chat it reflects a fully working channel. For SMS, WhatsApp, and the generic webhook, the toggle only governs health monitoring and the overview — it does not gate inbound: inbound webhooks are accepted purely on a valid signature against the channel's configured secret (see Inbound channel webhooks), whether or not the channel is toggled on. It does gate manual outbound — the inbox reply button and the contact composer only offer a channel that is enabled and configured.
Configuring SMS, WhatsApp, and the generic webhook
Click Configure on a channel card to open its form. Every channel accepts an optional Display Name (shown in the UI in place of the default channel name). Beyond that, the credential fields differ per channel.
Every field is encrypted at rest (channels.outbound.encryptAndPersistConfig) and decrypted only inside the Node runtime that needs it.
Outbound dispatch decrypts them in channels.outbound.dispatchOutbound when the AI agent, the inbox reply button, or the contact composer sends on the channel. Inbound webhook verification decrypts them through internal.channels.credentials.getInboundSecret and uses them in preference to the matching deployment environment variable — see Inbound channel webhooks.
One field is stored-only and reaches no provider: WhatsApp's Business Account ID, because the Meta send call is keyed on the Phone Number ID. The form labels it as such.
Saved credentials are never read back into the form — a field that already holds one is marked stored and stays blank. A save merges what you typed over what is stored, so leave a field blank to keep its current value and fill in only the credential you are changing.
Configure SMS (Twilio)
Enter your Twilio credentials:
| Field | Example |
|---|---|
| Account SID | ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
| Auth Token | your Twilio auth token (masked in the form) |
| Phone Number | +1234567890 |
The Auth Token does double duty: Twilio signs its inbound webhooks with the same value, so entering it here is all inbound SMS verification needs. The TWILIO_AUTH_TOKEN environment variable remains as a fallback for deployments configured that way.
Configure WhatsApp (Meta)
Enter your WhatsApp Business / Meta Cloud API credentials:
| Field | Description |
|---|---|
| Business Account ID | your WhatsApp Business Account ID |
| Access Token | Meta Cloud API access token (masked in the form) |
| Phone Number ID | the phone number ID from Meta |
| App Secret | Meta app secret — verifies the signature on inbound webhooks (masked) |
| Verify Token | any string you also paste into Meta; answers its subscription challenge (masked) |
The last two are inbound-only. The META_APP_SECRET and META_VERIFY_TOKEN environment variables remain as fallbacks when the fields are left empty.
Configure the generic webhook
| Field | Description |
|---|---|
| Endpoint URL | https://example.com/webhook — the destination Owlat POSTs outbound messages to (via WebhookAdapter) |
| Secret Key | the shared secret inbound callers must echo back to /webhooks/channel (masked) |
Outbound generic POSTs are unsigned. Owlat's POST to your endpoint carries Content-Type: application/json and nothing else — no signature and no shared-secret header. The Secret Key is inbound only and is never sent outbound. Do not configure your receiving endpoint to require a signature, or every reply will be rejected and recorded as a failed message. Authenticate outbound deliveries with an unguessable path segment or query token on the Endpoint URL.
The Secret Key is what an inbound caller must present in X-Webhook-Secret or Authorization: Bearer when posting to /webhooks/channel. With neither it nor the GENERIC_WEBHOOK_SECRET environment variable set, every inbound generic webhook is rejected.
Click Save Configuration to persist. A toast confirms the save. Token fields are masked by default with a show/hide toggle.
Channel health monitoring
A background cron runs every 5 minutes and updates the health status of each enabled channel. Each card shows a coloured badge plus, when available, the time of the last check, the last successful send, and the last error.
| Status | Meaning |
|---|---|
| Healthy (green) | The channel passed its last check. |
| Degraded (amber) | The channel is reachable but reporting problems. |
| Down (red) | The channel failed its last check. |
| Disabled / Unknown | The channel is turned off, or no check has run yet. |
Email and chat are always reported as healthy by this cron (email deliverability is tracked separately by the provider-health system, see Deliverability). For SMS, WhatsApp, and the generic webhook, the periodic check now makes a live provider probe for configured channels — a configured SMS channel fetches the Twilio account and a configured WhatsApp channel fetches the Meta graph, so revoked or invalid credentials report degraded or down. An unconfigured channel still reports down with "No credentials configured". Only the generic webhook remains credential-presence only.
A separate poll channel delivery status cron runs every 5 minutes so outbound SMS, WhatsApp, and generic messages do not sit at sent forever: it polls the provider for each message accepted in the last 24 hours and advances it to delivered, read, or failed on the contact's unified timeline as the carrier reports back.
Inbound channel webhooks
This is where messages from SMS, WhatsApp, and generic sources arrive. Point your provider at the matching Owlat HTTP endpoint:
| Channel | Endpoint | Method |
|---|---|---|
| SMS (Twilio) | /webhooks/sms | POST |
| WhatsApp (Meta) | /webhooks/whatsapp | POST (inbound), GET (Meta verification challenge) |
| Generic | /webhooks/channel | POST |
Every inbound request is verified before it is accepted — the channel webhooks fail closed if no secret is configured. Each check uses the credential stored on the channel (Settings → Channels), falling back to the deployment environment variable when that field is empty:
- Twilio requests are verified with an HMAC-SHA1 signature over the canonical URL-plus-sorted-params string (
X-Twilio-Signature), using the SMS channel's Auth Token (fallbackTWILIO_AUTH_TOKEN). - Meta (WhatsApp) requests are verified with an HMAC-SHA256 signature of the raw body (
X-Hub-Signature-256), using the WhatsApp channel's App Secret (fallbackMETA_APP_SECRET). TheGETchallenge is answered with its Verify Token (fallbackMETA_VERIFY_TOKEN). - Generic requests are authenticated with a constant-time comparison of a shared secret supplied via the
X-Webhook-SecretorAuthorization: Bearerheader, against the channel's Secret Key (fallbackGENERIC_WEBHOOK_SECRET).
When both a stored credential and an environment variable exist, the stored one wins — rotating a secret in the form takes effect immediately, without redeploying.
When a verified message arrives, Owlat finds or creates a contact and a conversation thread for it and stores the message on the unified timeline, where it appears in the Team Inbox. Each channel is its own identity space: an SMS sender and an email contact with the same value are not automatically the same contact — contacts reached over SMS, WhatsApp, or generic channels may have no email address at all. See Audience Data for how identities are resolved.
For the exact request shapes, headers, verification rules, and response codes, see Inbound Channel Webhooks.
Current limitations
Be precise about what these channels do today:
- Manual outbound is owner/admin only, and only on an enabled channel. Three surfaces reach one dispatch path (
channels.outbound.dispatchOutbound, which builds the real Twilio / Meta / downstream-webhook adapter): the AI-agent reply (processInboundChannel→agent.walker→agentPipeline.sendApprovedReply, gated on theai.agentfeature flag), the Reply button on a channel message in the Team Inbox thread view, and the contact's Unified Timeline composer — the last two both throughchannels.outbound.sendChannelMessage, which requiresorganization:manage. A member without that permission sees no reply affordance, and neither do email messages: those are answered by the thread's draft composer, which carries the MTA send pipeline. - A reply needs a contact and an address. SMS and WhatsApp dispatch to the contact's phone/handle identity; a message from a contact with no such identity on file is rejected with a clear error rather than silently recorded as failed. Generic replies POST to the channel's Endpoint URL, unsigned.
- The generic channel's inbound secret is a shared secret, not a per-request HMAC. It is compared in constant time, but anyone holding it can post; keep the endpoint rate-limited and rotate the Secret Key if it leaks.
- Health checks are credential-presence only for the generic webhook; SMS and WhatsApp run a live provider
healthCheck()probe (Twilio account / Meta graph) for configured channels.
Receive and triage on these channels in the Team Inbox, and answer them there with the per-message Reply button — or from the contact's Unified Timeline when you want to start the conversation.