Skip to main content

Troubleshooting: channel and conversation pre-send refusals

A send on any messaging channel runs a short set of deterministic checks before it is ever handed to a provider. When one of those checks refuses the request, the structured 404/422 envelope says exactly why — and the codes that fire carry either a channel value or a conversation_id back, so you can tell which field to fix. This page is the routing card for the five hardest channel- and conversation-naming refusals a POST /api/v1/messages (and the POST /api/v1/notify multi-channel fan-out, which dispatches through the same send service) can throw. Definitions live in the Error Code Reference; this page only owns the triage path. All five refusals are deterministic — the same body fails the same way until you fix the named field. Never loop the retry.

Section routing — which refusal fired

CHANNEL_REQUIRED — the send did not name its channel

Cause. The request body had no channel, an empty string, or the literal "auto". After 2026-09-01 every send must name its channel explicitly — the unified send path used to guess one on null/"auto", and measured behaviour was that it picked VOICE for a text message on most input combinations (paying PSTN rates and ringing the recipient’s phone on an SMS send). Rather than guess wrong again, the gate refuses the request outright. Fix. Pass a valid channel name in the body — one of the enums on channels, e.g. sms, whatsapp, rcs, email, voice. If you genuinely want the platform to pick a channel for you, do not send channel: "auto" here — call POST /messages/smart-send instead, which returns the chosen channel and its reasoning. Envelope sample.
Never retry. The gate is deterministic — the same body without a channel always fails.

CHANNEL_COMING_SOON — a gated channel on a tenant that has not been enabled

Cause. The channel enum was valid, but it points at a regional or beta channel that has not been enabled for your organization (e.g. an APAC preview surface, or a channel your plan does not yet cover). The check is tenant-scoped — the same channel may send fine for another tenant. Fix. This is a tenant-side wait-for-enablement gate. Open a support ticket asking for the channel to be gated in, listing (1) the exact channel name, (2) your organization id (Settings → Organization, or GET /api/v1/me), and (3) the intended destination region. Do not fall back to a sibling channel unless the recipient can be reached there too — the refusal exists because a beta channel’s deliverability is not endorsed for general tenant traffic. Envelope sample.
Do not confuse with CHANNEL_NOT_CONFIGURED (the channel is enabled, but the provider credentials/route for it were never completed under Settings → Channels) or CHANNEL_UNAVAILABLE (enabled, but the fallback chain was exhausted — see channel unavailable and fallback exhausted).

CHANNEL_NOT_REPLYABLE — reply via an inbox-only label

Cause. In the Inbox / conversation-reply flow (and the continue-on-channel surface on POST /conversations/:id/resume-channel), the chosen channel string was one of the inbox-only triage labels — agent (assign to an agent) or video (video call) — which are filters over the conversation list, not outbound providers. Fix. Pick a real outbound channel — sms, whatsapp, email, voice, or another reachable one — from the reply composer. An agent or video label exists only to route the thread; it never places an outbound provider dispatch. Envelope sample.
Do not confuse with MISSING_IDENTITY — the label hits CHANNEL_NOT_REPLYABLE only when the channel is an inbox view; a real channel missing the recipient’s identity on the conversation row throws the 400 MISSING_IDENTITY instead. Both are deterministic — fix the named field.

CONVERSATION_NOT_FOUND — conversation_id resolved to nothing

Cause. POST /messages (and the POST /notify cascade, which re-uses the same send service) was asked to thread the send into an existing conversation via an optional conversation_id, but the lookup found no such conversation on the caller’s tenant — either because the id is well-formed but unknown, or because it belongs to a different tenant. This is an idempotency scope guard: a reply can never be threaded into a foreign tenant’s conversation by accident. Fix. Drop the field entirely to let the send auto-resolve the thread (the recipient + channel will open a new conversation or reuse the latest open one), or re-query to get the right conversation id via GET /conversations?contact=… before threading. Envelope sample.
Never retry. Same id, same 404 — until the thread row exists. Do not confuse with the threads page diagnosing CONVERSATION_TERMINAL — a conversation that exists but is closed, archived, or snoozed, and so cannot accept replies on any channel.

CONVERSATION_MISMATCH — conversation_id belongs to a different recipient/channel

Cause. The conversation_id did resolve — but either the conversation’s contact_phone/contact_email does not equal the send’s to recipient, or the conversation’s primary channel (and its cross-channel channels[] union) does not include the send channel. This is the anti-misroute guard: it stops a reply being threaded into another contact’s open thread (or an SMS reply stapled onto a WhatsApp thread). Fix. Give the correct conversation_id — the one whose recipient and channel actually address this send — or omit the field so the send auto-resolves the thread by recipient. Query with GET /conversations?contact=<to> to pick the right row. Envelope sample.
Never retry. In both cases the guard is deterministic — fix the recipient or channel match before re-sending.

Where each refusal routes

  1. CHANNEL_REQUIRED — POST /api/v1/messages (its explicit-channel guard runs first). Call POST /messages/smart-send to have the platform choose a channel and report it back.
  2. CHANNEL_COMING_SOON — the custom-channel gate inside the message-send bootstrap; enable the tenant over support before the channel will route.
  3. CHANNEL_NOT_REPLYABLE — the /conversations/:id/reply flow and the continue-on-channel surface; pick a real outbound channel.
  4. CONVERSATION_NOT_FOUND / CONVERSATION_MISMATCH — the anti-misroute thread guard on POST /api/v1/messages (and the POST /notify cascade that re-uses the send service). Field name is conversation_id.

When to escalate

A CHANNEL_COMING_SOON on a channel you believe is released to your plan is abnormal; so is CONVERSATION_MISMATCH on a thread id that you can see in the Inbox itself. Escalate to support with:
  1. Your organization ID — Settings → Organization, or GET /api/v1/me.
  2. The exact code and meta.request_id from the rejected envelope.
  3. For CHANNEL_COMING_SOON: the channel name and the destination region.
  4. For CONVERSATION_NOT_FOUND / CONVERSATION_MISMATCH: the conversation_id you passed, the conversation id you see in the Inbox for that thread, and the recipient to + channel of the send.

See also