Skip to main content

Troubleshooting: CHANNEL_UNAVAILABLE and fallback chain exhaustion

CHANNEL_UNAVAILABLE surfaces in two casings. A 422 means the routing configuration itself stops the send — no sender or provider can fix it from the outside. A 503 means the platform attempted your channels and every route it tried came back unavailable — either nothing is configured to fall back to, or the ladder was walked to its end. Both casings are tenant-owned: the fix lives in your channel configuration, your fallback map, or your send pacing — not in a provider ticket. This page maps the error to the hop that failed, the reason the ladder ran out, and the control you own to clear it.

What the error means

The router resolves a send in two passes:
  1. Configuration pass (422). The channel is not connected on your account, no hop in your fallback chain is eligible, or a campaign pre-flight gate (a RED WhatsApp quality rating) aborts before any provider is contacted. Retrying unchanged replays the same refusal.
  2. Attempt pass (503). The router walked every provider on the primary channel, then every channel your fallback map lists, and each came back unavailable. The failure is transient (a circuit breaker skipped the provider for a window) or real (a genuine outage) — the response tells you which through details.transient_skip.
An empty fallback list for a primary channel means the attempt pass has nowhere to go: the first channel failure ends the chain and returns 503 to the caller.

The three operation classes that surface it

The same code surfaces on three send paths. Triage the one you hit:

Per-channel ladder

Each primary channel has its own ladder shape. The verify chain is the only one with a platform-fixed order.

SMS and WhatsApp (org fallback map)

Your org-level cross-channel fallback map (Settings → Channels) lists, per primary channel, the ordered channels to try when the primary fails. Example: a chain rcs → ["sms", "email"] sends on RCS, retries the body on SMS if RCS fails, and ends on email if both fail. The adaptive cascade may re-rank a chain like [whatsapp, rcs, sms] by segment-learned delivery outcomes before the first attempt — so the order you configured is the default, not necessarily the order executed. A cascade-armed send (POST /notify) exhausts like this: RCS fails (carrier-level), the walk falls to SMS (carrier-level failure again), then to email, and once email also fails the walk terminates with fallback.chain_exhausted on the fallback metric and 503 to the caller.

Verify factor chain

The verify chain is platform-ordered SMS → voice → email and is not editable from your fallback map. A verify send that fails on SMS automatically falls to voice, then email; when all three are unavailable the POST /verify/start call returns 503 CHANNEL_UNAVAILABLE. The same details.transient_skip flag tells you whether the router skipped the providers (a breaker window — retry safe) or genuinely failed each one.

Edit the fallback map in Settings → Channels

Fix the chain, not the symptoms:
  1. Open Settings → Channels in the dashboard.
  2. Find the Cross-channel fallback section and edit the channel that fired — add a hop (the ordered list gains a tail), remove a stale hop, or reorder the list so a reachable channel is tried earlier.
  3. Save. The per-channel cache invalidates immediately — your next send walks the new chain.
From the API, the same map lives on GET /api/v1/settings/channels-fallback and PUT /api/v1/settings/channels-fallback: send a JSON object keyed by primary channel whose values are ordered lists (e.g. { "rcs": ["sms"] }); pass an empty array to disable fallback for that primary explicitly. Self-fallback entries and duplicates are dropped server-side. A second, per-campaign shape exists: an omnichannel campaign’s channels[] array is an ordered ladder that layers on the org map with per-hop fallback_on triggers and fallback_after_seconds windows. The org map fires when no ladder is configured, or when the ladder’s own triggered hops all fail.

The details.transient_skip flag — when retry is safe

Every 503 carries a details.transient_skip boolean. Branch on it before you page anyone:
  • transient_skip: true — every provider on the channel was skipped because its per-tenant circuit breaker was open or the channel was rate-limited in the window before your send. No provider was contacted — nothing to fix, and the breaker clears on its own. Retrying the send is safe and is what the flag exists to signal: hold a short backoff (the cooldown window) and resubmit.
  • transient_skip: false — at least one provider was attempted and genuinely failed. A retry transmits the same failure unless the upstream fault cleared in the meantime. Investigate before retrying: if every chain hop on every send for a channel fails with transient_skip: false for more than a few minutes, the outage is real — open a ticket only after you’ve ruled out your own fallback map and sender configuration.
Audit rule: a circuit-breaker window produces one incident, not one per send. Repeated transient_skip: true rejections on the same channel within the same minute are one breaker event; a second distinct channel failing in the same window moves the diagnosis from your map to the provider layer.

What to capture before escalating

Work the ladder mapping first. If a send still reads as genuinely unavailable many minutes after the breaker window should have cleared — or a 422 keeps firing after you added a valid hop — open a ticket with:
  1. The error envelope — code, HTTP status, and the full details object (transient_skip, last_error, any per-hop attribution).
  2. The send path — single message via POST /messages, a notify cascade, a campaign, or a verify spawn.
  3. The primary channel and the chain as currently configured — the GET /settings/channels-fallback body verbatim.
  4. Your organization ID — Settings → Organization, or organizationId from GET /api/v1/me.

What not to do

  • Do not retry a 422. The refusal re-fires identically until the configuration changes.
  • Do not retry a transient_skip: true 503 in a tight loop. Back off within the breaker cooldown, then retry once the window likely cleared.
  • Do not treat transient_skip: false as a provider ticket before checking your own fallback map. An empty map ends the chain regardless of provider health.
  • Do not add the failing channel to its own fallback list. A hop retrying the channel that just failed re-runs the same failure; the platform drops self-fallback entries server-side regardless.

See also