Channels and fallback API
iMessage, SMS and email next to WhatsApp, sends that fall back between them, and AI conversation summaries by email. 7 ops: connect_channel, list_channels, disconnect_channel, send_routed_message, get_message_route, list_message_routes, get_contact_channels.
iMessage, SMS and email next to WhatsApp, sends that fall back between them, and AI conversation summaries by email.
Connect iMessage, SMS or email · POST /api/v1/connect_channel · scope channels:write
Connects a channel next to WhatsApp: iMessage through a bridge (provider bridge: base_url and api_token of a Mac running Rio, Photon or any bridge implementing Wabi's bridge contract), SMS through the organization's own Twilio account (provider twilio: account_sid, auth_token and the number or messaging_service_sid), email through AgentMail (provider agentmail: give username and Wabi creates a managed inbox, or api_key and address for an inbox of your own), or a free simulated one (provider sandbox). Wabi registers its webhook with the provider when it can and returns `webhook` with the URL and, when it could not, what to set by hand. Credentials are encrypted and never returned. Then send_routed_message and reply_to_conversation use it, and inbound messages on it land in the inbox.
curl -X POST https://getwabi.dev/api/v1/connect_channel \ -H "Authorization: Bearer wabi_…" \ -H "Content-Type: application/json" \ -d '{"channel":"imessage","provider":"bridge"}'npx @meterapp/wabi connect_channel --json '{"channel":"imessage","provider":"bridge"}'{
"type": "object",
"properties": {
"channel": {
"type": "string",
"enum": [
"imessage",
"sms",
"email"
],
"description": "imessage (through a bridge on a Mac: Rio, Photon or anything implementing the bridge contract), sms (through Twilio) or email (through AgentMail)."
},
"provider": {
"type": "string",
"enum": [
"bridge",
"twilio",
"agentmail",
"sandbox"
],
"description": "bridge for iMessage, twilio for SMS, agentmail for email, or sandbox for a free simulated channel (phone recipients ending in an odd digit are not on iMessage, …9 never confirms, …0000 fails everywhere; an email local part ending in +bounce bounces)."
},
"name": {
"description": "A label for this connection.",
"type": "string",
"minLength": 1,
"maxLength": 100
},
"address": {
"description": "The sender customers see: the bridge's Apple ID handle or number (iMessage), the Twilio number to send from in E.164 (SMS), or the inbox address (email with your own api_key).",
"type": "string",
"minLength": 3,
"maxLength": 200
},
"username": {
"description": "email (managed): the local part of the inbox Wabi creates for you, e.g. acme-support → acme-support@<Wabi's mail domain>. Not needed with your own api_key.",
"type": "string",
"minLength": 1,
"maxLength": 64
},
"display_name": {
"description": "email: the sender name recipients see.",
"type": "string",
"minLength": 1,
"maxLength": 100
},
"api_key": {
"description": "email: your own AgentMail API key, to send from an inbox of yours (give its address). Without it Wabi creates and manages an inbox on its own account. Stored encrypted, never returned.",
"type": "string",
"minLength": 8,
"maxLength": 500
},
"base_url": {
"description": "bridge: the bridge's public HTTPS base URL (Tailscale Funnel or a tunnel; Wabi cannot reach a private tailnet).",
"type": "string",
"maxLength": 500,
"format": "uri"
},
"api_token": {
"description": "bridge: the bearer token Wabi sends to the bridge. Stored encrypted, never returned.",
"type": "string",
"minLength": 8,
"maxLength": 500
},
"webhook_secret": {
"description": "bridge or agentmail: the whsec_ secret the provider signs its webhooks with, when Wabi could not register its webhook itself.",
"type": "string",
"minLength": 8,
"maxLength": 500
},
"account_sid": {
"description": "twilio: the Account SID (AC…).",
"type": "string",
"pattern": "^AC[0-9a-f]{32}$"
},
"auth_token": {
"description": "twilio: the auth token; it also verifies Twilio's webhook signatures. Stored encrypted, never returned.",
"type": "string",
"minLength": 16,
"maxLength": 200
},
"messaging_service_sid": {
"description": "twilio: send through this Messaging Service instead of one number.",
"type": "string",
"pattern": "^MG[0-9a-f]{32}$"
}
},
"required": [
"channel",
"provider"
]
}list_channels
readList channels · POST /api/v1/list_channels · scope channels:read or GET …?input=
Lists the organization's iMessage, SMS and email connections (no secrets): channel, provider, sender address, webhook URL, status and when the last webhook arrived. WhatsApp numbers are list_whatsapp_accounts.
curl -X POST https://getwabi.dev/api/v1/list_channels \ -H "Authorization: Bearer wabi_…"npx @meterapp/wabi list_channels{
"type": "object",
"properties": {
"include_disconnected": {
"default": false,
"description": "Include disconnected connections.",
"type": "boolean"
}
}
}Disconnect a channel · POST /api/v1/disconnect_channel · scope channels:write
Disconnects an iMessage, SMS or email connection: its credentials are deleted, routed sends stop using it and its webhooks are refused. Messages already sent keep their history. Connect again with connect_channel.
curl -X POST https://getwabi.dev/api/v1/disconnect_channel \ -H "Authorization: Bearer wabi_…" \ -H "Content-Type: application/json" \ -d '{"channel_id":"6f1c2e0a-4b7d-4c2e-9a51-0d3b8e7f9a10"}'npx @meterapp/wabi disconnect_channel --json '{"channel_id":"6f1c2e0a-4b7d-4c2e-9a51-0d3b8e7f9a10"}'{
"type": "object",
"properties": {
"channel_id": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "The connection id from list_channels."
}
},
"required": [
"channel_id"
]
}send_routed_message
writeSend with fallback across channels · POST /api/v1/send_routed_message · scope channels:write
Sends one message to a customer over the best channel, falling back to the next when one cannot deliver: by default iMessage → WhatsApp → SMS → email, over the organization's connected channels. Address them by phone (to), email, or contact_id; channels the contact has no address for are left out. It skips channels this contact was definitively unreachable on lately, tries the channel they last wrote on first, asks the iMessage bridge whether the number is on iMessage before sending (when the bridge can tell), and falls back at once on a definitive failure (not on iMessage, not on WhatsApp, a landline, a bounce, a WhatsApp safeguard such as a closed window) or after fallback_after_seconds without a delivered receipt. Returns the route: its steps, each attempt's outcome and message, and status routing → delivered | failed | unconfirmed. Poll get_message_route or subscribe to the route.completed webhook. Use idempotency_key to make retries safe.
curl -X POST https://getwabi.dev/api/v1/send_routed_message \ -H "Authorization: Bearer wabi_…" \ -H "Content-Type: application/json" \ -d '{"text":"Hi, where is my order?"}'npx @meterapp/wabi send_routed_message --json '{"text":"Hi, where is my order?"}'{
"type": "object",
"properties": {
"to": {
"description": "The customer's phone number in international format (e.g. +15550100002). Give to, email or contact_id (any combination; the contact's other addresses are used too).",
"type": "string",
"minLength": 6,
"maxLength": 32
},
"email": {
"description": "The customer's email address, for the email channel.",
"type": "string",
"maxLength": 254,
"format": "email",
"pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
},
"contact_id": {
"description": "An existing contact; its phone and email are used.",
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
},
"text": {
"type": "string",
"minLength": 1,
"maxLength": 4096,
"description": "The message. iMessage, SMS and email send it as is; WhatsApp sends it as text inside the 24-hour window."
},
"subject": {
"description": "email: the subject of a new thread. A reply to a customer who already emailed threads under their last email instead.",
"type": "string",
"minLength": 1,
"maxLength": 200
},
"media_url": {
"description": "A public HTTPS file to attach (iMessage attachment, MMS; WhatsApp and email get the link after the text).",
"type": "string",
"maxLength": 2000,
"format": "uri"
},
"whatsapp_template": {
"description": "An approved WhatsApp template to send instead of the text when the customer's 24-hour window is closed. Without it WhatsApp is skipped outside the window.",
"type": "object",
"properties": {
"name": {
"type": "string",
"pattern": "^[a-z0-9_]{1,512}$"
},
"language": {
"anyOf": [
{
"type": "string",
"minLength": 2,
"maxLength": 15
},
{
"type": "object",
"properties": {
"code": {
"type": "string",
"minLength": 2,
"maxLength": 15
}
},
"required": [
"code"
]
}
],
"description": "Template language code, e.g. en_US."
},
"components": {
"description": "Variable values, as Meta's components array: [{ type: 'body', parameters: [{ type: 'text', text: 'Ana' }] }].",
"maxItems": 20,
"type": "array",
"items": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"header",
"body",
"button",
"carousel"
]
},
"sub_type": {
"type": "string",
"enum": [
"quick_reply",
"url",
"copy_code",
"flow",
"catalog"
]
},
"index": {
"anyOf": [
{
"type": "integer",
"minimum": 0,
"maximum": 9
},
{
"type": "string",
"pattern": "^\\d$"
}
]
},
"parameters": {
"maxItems": 100,
"type": "array",
"items": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"text",
"currency",
"date_time",
"image",
"video",
"document",
"location",
"payload",
"coupon_code"
]
},
"text": {
"type": "string",
"maxLength": 4096
},
"parameter_name": {
"type": "string",
"maxLength": 100
}
},
"required": [
"type"
],
"additionalProperties": {}
}
}
},
"required": [
"type"
],
"additionalProperties": {}
}
}
},
"required": [
"name",
"language"
]
},
"channels": {
"description": "The channels to try, in order. Default imessage → whatsapp → sms → email; channels that are not connected, or that the contact has no address for, are left out.",
"minItems": 1,
"maxItems": 4,
"type": "array",
"items": {
"type": "string",
"enum": [
"imessage",
"whatsapp",
"sms",
"email"
]
}
},
"imessage_connection_id": {
"description": "The iMessage connection to send from (list_channels); default the organization's first live one, else the sandbox.",
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
},
"sms_connection_id": {
"description": "The SMS connection to send from (list_channels); default the organization's first live one, else the sandbox.",
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
},
"email_connection_id": {
"description": "The email connection to send from (list_channels); default the organization's first live one, else the sandbox.",
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
},
"whatsapp_phone_number_id": {
"description": "The WhatsApp number to send from; default the first connected live number, else the sandbox.",
"type": "string",
"minLength": 1,
"maxLength": 64
},
"fallback_after_seconds": {
"description": "Per channel, how long to wait for a delivered receipt before trying the next channel. Default imessage 30s, whatsapp 60s, sms 120s, email 300s. 0 falls back only on a definitive failure, never on silence (no risk of the customer getting it twice).",
"type": "object",
"properties": {
"imessage": {
"type": "integer",
"minimum": 0,
"maximum": 3600
},
"whatsapp": {
"type": "integer",
"minimum": 0,
"maximum": 3600
},
"sms": {
"type": "integer",
"minimum": 0,
"maximum": 3600
},
"email": {
"type": "integer",
"minimum": 0,
"maximum": 3600
}
}
},
"prefer_last_inbound": {
"default": true,
"description": "Try the channel the customer last wrote on first.",
"type": "boolean"
},
"idempotency_key": {
"description": "Retry-safe key: a second call with the same key returns the first route instead of sending again.",
"type": "string",
"minLength": 1,
"maxLength": 200
}
},
"required": [
"text"
]
}Get a routed send · POST /api/v1/get_message_route · scope channels:read or GET …?input=
Returns one routed send (send_routed_message): status, the channel that delivered, each step with its outcome (sent, delivered, failed, timed_out, skipped) and the reason, the planning notes, and every attempt's message with its status timeline fields.
curl -X POST https://getwabi.dev/api/v1/get_message_route \ -H "Authorization: Bearer wabi_…" \ -H "Content-Type: application/json" \ -d '{"route_id":"6f1c2e0a-4b7d-4c2e-9a51-0d3b8e7f9a10"}'npx @meterapp/wabi get_message_route --json '{"route_id":"6f1c2e0a-4b7d-4c2e-9a51-0d3b8e7f9a10"}'{
"type": "object",
"properties": {
"route_id": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "The route id send_routed_message returned."
}
},
"required": [
"route_id"
]
}List routed sends · POST /api/v1/list_message_routes · scope channels:read or GET …?input=
Lists recent routed sends, newest first, optionally for one contact or status.
curl -X POST https://getwabi.dev/api/v1/list_message_routes \ -H "Authorization: Bearer wabi_…"npx @meterapp/wabi list_message_routes{
"type": "object",
"properties": {
"contact_id": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "Contact id."
},
"status": {
"type": "string",
"enum": [
"routing",
"delivered",
"failed",
"unconfirmed",
"canceled"
]
},
"limit": {
"default": 50,
"description": "Page size, at most 200.",
"type": "integer",
"minimum": 1,
"maximum": 200
},
"offset": {
"default": 0,
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}
}
}Which channels reach a contact · POST /api/v1/get_contact_channels · scope channels:read or GET …?input=
What Wabi learned about one contact per channel (WhatsApp, iMessage, SMS, email) from what actually happened: reachable (a message was delivered or they wrote on it), unreachable (a definitive failure, skipped by routing until it expires) or unknown, with the last delivered, inbound and failed times, and the channel they last wrote on. Give contact_id, phone or email.
curl -X POST https://getwabi.dev/api/v1/get_contact_channels \ -H "Authorization: Bearer wabi_…"npx @meterapp/wabi get_contact_channels{
"type": "object",
"properties": {
"contact_id": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "Contact id."
},
"phone": {
"description": "The contact's phone number in international format.",
"type": "string",
"minLength": 6,
"maxLength": 32
},
"email": {
"description": "The contact's email address.",
"type": "string",
"format": "email",
"pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
}
}
}