Skip to main content
POST /v1/messages reaches one person. You say who, what, and — through a route — which channels to try in which order and what counts as getting there on each. OMNI tries the first; if it can’t take the message, doesn’t deliver it in time, or the person doesn’t read it in time, the next one goes.
The answer is 202 Accepted with the message as it stands — usually its first attempt already made. What happens next arrives as webhooks, and GET /v1/messages/{message_id} tells the whole story at any time.

Routes: which channels, in what order

A route is a list of up to six steps. Each step names a channel, what counts as done on it, and how long to wait:
within is in seconds, from 30 to 7 days; 15 minutes when left out. A step can only wait for what its channel reports — asking SMS for read is refused. Save routes with POST /v1/routes and name them in a message, or give steps in the message itself. One route can be the default, used when a message names none. Without a default, a message is tried on your channels in their usual order (WhatsApp, then SMS), each done when delivered, within 15 minutes.

Calls as a step

When your account has Voice, a route can end with a call: WhatsApp, then SMS, then ring them into an IVR, done when answered.
The call is placed like one from POST /v1/calls — screened for consent the same way — and the attempt carries its call id. A message that names no route is never turned into a phone call: calls happen only when a route asks for them.

How a step hands over

A step is refused, for example, when WhatsApp’s 24-hour window is closed and no approved template was given (window_closed), when an SMS’s text fills in none of your registered DLT templates (no_dlt_match), when nothing was written for that channel (no_content), when the channel is off (channel_off), or when the provider says no (provider_refused). If a channel gets there after its step has timed out, that is recorded on its attempt — but nothing is sent again.

What to send: templates and content

A unified template is one message written once per channel, with {{values}}:
Send it with "template": "order_shipped" and its "values". A message missing a value the template needs is refused before anything is sent. You can also give content per channel instead of a template — or as well, to change one part of it:
Every attempt is screened like any message OMNI sends:
  • Someone who asked never to be contacted, or opted out of everything, is never tried on any channel: the message fails with not_permitted.
  • A channel they opted out of is skipped: that step is refused with not_permitted.
  • During their quiet hours the message waits (postponed) and is tried when they end.
  • "purpose": "marketing" applies marketing consent; transactional (the default) applies the rules for messages people expect.

How a message ends

Each attempt points at the message it sent on its channel (cm_…), whose own deliveries arrive as channel_message.* events.
Charging. Every attempt a channel actually took is a message sent on that channel and is charged like one. Refused steps cost nothing. A test key’s messages reach no channel and cost nothing.

Test keys

With an ff_test_… key, a message succeeds at once on its first step with the code test, and nothing reaches any phone — so you can build the whole flow, webhooks included, before going live.