Send a message
Send one message. The body is the /v1 message body plus from and conversationId. There is no Zavu-Sender header.
Where it leaves from: conversationId if given, else from (an address), else the project default. See from for ambiguous addresses.
Which transport: decided by the first branch that applies, and reported in routing.reason: a test-mode key uses the WhatsApp sandbox; an email-address to uses email; a WhatsApp-only messageType uses WhatsApp; an explicit channel is used as given; otherwise the fixed order over the sender’s ACTIVE transports (for a phone number: WhatsApp, then SMS, then one-way SMS). The order ignores price and the 24-hour window. A channel that contradicts the recipient (an email to on anything but email, or channel: "email" with a phone number or chat id) or a WhatsApp-only messageType is refused with 400; /v1 overrides the first and last silently.
When nothing can send the answer is always 409 no_channel, whatever the reason: no channel at all, an explicit channel that is not connected, an email to without an email channel, a WhatsApp-only messageType without WhatsApp. details.connect lists what this project can connect to reach to, with the exact call for each (POST /v2/channels, POST /v2/numbers), and the message never names an address that carries nothing.
Fallback from WhatsApp to SMS, when enabled, happens after this call returned, once the WhatsApp send fails. It is not visible in routing; read attemptedChannels on the message or wait for the webhooks. Before falling back to SMS, a message sent through /v2 is checked for an approved 10DLC campaign (+1 numbers) and an active connection; when either is missing it stays failed with errorCode TEN_DLC_REQUIRED or CHANNEL_INACTIVE.
Limits, risk review, pricing and verification are exactly /v1’s: see POST /v1/messages.
curl --request POST \
--url https://api.zavu.dev/v2/messages \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"to": "+56912345678",
"text": "Your code is 123456"
}
'import requests
url = "https://api.zavu.dev/v2/messages"
payload = {
"to": "+56912345678",
"text": "Your code is 123456"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({to: '+56912345678', text: 'Your code is 123456'})
};
fetch('https://api.zavu.dev/v2/messages', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.zavu.dev/v2/messages",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'to' => '+56912345678',
'text' => 'Your code is 123456'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.zavu.dev/v2/messages"
payload := strings.NewReader("{\n \"to\": \"+56912345678\",\n \"text\": \"Your code is 123456\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.zavu.dev/v2/messages")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"to\": \"+56912345678\",\n \"text\": \"Your code is 123456\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.zavu.dev/v2/messages")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"to\": \"+56912345678\",\n \"text\": \"Your code is 123456\"\n}"
response = http.request(request)
puts response.read_body{
"message": {
"id": "jd7x2k3m4n5p6q7r8s9t0",
"to": "+56912345678",
"channel": "sms",
"direction": "inbound",
"messageType": "text",
"status": "queued",
"createdAt": "2023-11-07T05:31:56Z",
"from": "+13125551212",
"text": "<string>",
"content": {
"mediaUrl": "https://example.com/image.jpg",
"mediaId": "<string>",
"mimeType": "image/jpeg",
"filename": "invoice.pdf",
"latitude": 123,
"longitude": 123,
"locationName": "<string>",
"locationAddress": "<string>",
"contacts": [
{
"name": "<string>",
"phones": [
"<string>"
]
}
],
"buttons": [
{
"id": "<string>",
"title": "<string>"
}
],
"listButton": "<string>",
"sections": [
{
"title": "<string>",
"rows": [
{
"id": "<string>",
"title": "<string>",
"description": "<string>"
}
]
}
],
"ctaDisplayText": "See Dates",
"ctaUrl": "https://example.com/schedule",
"ctaHeaderType": "text",
"ctaHeaderText": "<string>",
"ctaHeaderMediaUrl": "<string>",
"footerText": "Dates subject to change.",
"emoji": "<string>",
"reactToMessageId": "<string>",
"replyToMessageId": "<string>",
"replyToProviderMessageId": "<string>",
"replyToFrom": "<string>",
"replyToText": "<string>",
"replyToMessageType": "<string>",
"templateId": "<string>",
"templateVariables": {
"1": "John",
"2": "ORD-12345"
},
"templateButtonVariables": {
"0": "abc-report-token"
},
"templateHeaderVariables": {
"1": "Jorge y Laura"
},
"referral": {
"sourceType": "ad",
"sourceId": "120210000000000000",
"sourceUrl": "<string>",
"headline": "<string>",
"body": "<string>",
"mediaType": "image",
"imageUrl": "<string>",
"videoUrl": "<string>",
"thumbnailUrl": "<string>",
"ctwaClid": "ARIzZm9vYmFyY3R3YWNsaWQ"
}
},
"conversationId": "js723987cyghwqxxaxcf590qd18axd95",
"providerMessageId": "<string>",
"errorCode": "<string>",
"errorMessage": "<string>",
"cost": 123,
"costProvider": 123,
"costTotal": 123,
"metadata": {},
"updatedAt": "2023-11-07T05:31:56Z",
"attemptedChannels": [
"sms"
],
"riskHold": "held",
"errorDetails": {
"providerCode": "3"
},
"charged": 123
},
"routing": {
"channel": "sms",
"from": "<string>",
"reason": "explicit",
"estimatedCost": 123
}
}Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Body
Request body to send a message. No Zavu-Sender header: where it leaves from comes from conversationId, then from, then the project default. to is required, except with conversationId, where it defaults to whoever the conversation is with.
Recipient phone number in E.164 format, email address, WhatsApp business-scoped user ID (BSUID, e.g. US.13491208655302741918), or numeric chat ID (for Telegram/Instagram/Messenger). A phone number written with spaces, dashes, dots or parentheses after its + (+56 9 1234 5678) is read as the E.164 number; bare digits are not (see 400 on this operation). A BSUID is routed to WhatsApp and sent via the recipient field; use it to message a contact who adopted a username and whose phone number is hidden.
Required unless conversationId is given. With conversationId it may be omitted (it is the conversation's contact: its email for channel: "email", else the identifier it is filed under); if given it must be one of that conversation's identifiers (its key, email or BSUID, in any formatting), else 400 naming to and conversationId.
"+56912345678"
auto, or a transport. auto applies the fixed order described on POST /v2/messages.
auto, sms, sms_oneway, whatsapp, telegram, email, instagram, messenger, voice Type of message. Defaults to 'text'.
text, image, video, audio, document, sticker, location, contact, buttons, list, cta_url, request_contact_info, location_request, reaction, template Text body for text messages or caption for media messages.
"Your verification code is 123456."
Additional content for non-text message types.
Show child attributes
Show child attributes
Email subject line. Required when channel is 'email' or recipient is an email address.
998"Your order confirmation"
HTML body for email messages. If provided, email will be sent as multipart with both text and HTML.
Reply-To email address for email messages.
"support@example.com"
Optional idempotency key to avoid duplicate sends.
"msg_01HZY4ZP7VQY2J3BRW7Z6G0QGE"
Arbitrary metadata to associate with the message.
Show child attributes
Show child attributes
Whether to enable automatic fallback to SMS if WhatsApp fails. Defaults to true.
Language code for voice text-to-speech (e.g., 'en-US', 'es-ES', 'pt-BR'). If omitted, language is auto-detected from recipient's country code.
"es-ES"
Email attachments. Only supported when channel is 'email'. Maximum 40MB total size.
50Show child attributes
Show child attributes
Which of your addresses sends (see GET /v2/channels): an E.164 number in any common formatting, an email address, @botname, @username, a Page id. Resolved inside your project only. Omitted: the project default, the same one /v1 uses when no Zavu-Sender header is sent. Ignored when conversationId is set, and with a test-mode key, which always sends from the WhatsApp sandbox.
An address that no entry has returns 404 not_found. An address two of your senders share (a number whose WhatsApp was connected on one sender and SMS on another) resolves when channel names the transport; otherwise it returns 409 ambiguous_from with details.candidates[{address, type, v1SenderId}], and in that case, and only then, from also accepts one of those v1SenderId values.
"+13125551212"
Reply inside an existing conversation: the message leaves from the sender that last handled it (from on the Conversation), and goes to whoever the conversation is with, so to may be omitted. A to that is not the conversation's contact is refused with 400 (details.errors on to and conversationId, details.conversationTo) instead of silently opening another conversation. Takes precedence over from. When channel is omitted, routing.reason is then conversation. A conversation of another project returns 404; one with no sender recorded returns 409 no_channel. The 202's message.conversationId is set whenever the conversation the message is filed in already exists; it is absent only for a recipient's first message, whose conversation is created after the response. A reply to a conversation that an inbound email opened reaches the right person but is filed in a separate conversation keyed by the email address.
curl --request POST \
--url https://api.zavu.dev/v2/messages \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"to": "+56912345678",
"text": "Your code is 123456"
}
'import requests
url = "https://api.zavu.dev/v2/messages"
payload = {
"to": "+56912345678",
"text": "Your code is 123456"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({to: '+56912345678', text: 'Your code is 123456'})
};
fetch('https://api.zavu.dev/v2/messages', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.zavu.dev/v2/messages",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'to' => '+56912345678',
'text' => 'Your code is 123456'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.zavu.dev/v2/messages"
payload := strings.NewReader("{\n \"to\": \"+56912345678\",\n \"text\": \"Your code is 123456\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.zavu.dev/v2/messages")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"to\": \"+56912345678\",\n \"text\": \"Your code is 123456\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.zavu.dev/v2/messages")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"to\": \"+56912345678\",\n \"text\": \"Your code is 123456\"\n}"
response = http.request(request)
puts response.read_body{
"message": {
"id": "jd7x2k3m4n5p6q7r8s9t0",
"to": "+56912345678",
"channel": "sms",
"direction": "inbound",
"messageType": "text",
"status": "queued",
"createdAt": "2023-11-07T05:31:56Z",
"from": "+13125551212",
"text": "<string>",
"content": {
"mediaUrl": "https://example.com/image.jpg",
"mediaId": "<string>",
"mimeType": "image/jpeg",
"filename": "invoice.pdf",
"latitude": 123,
"longitude": 123,
"locationName": "<string>",
"locationAddress": "<string>",
"contacts": [
{
"name": "<string>",
"phones": [
"<string>"
]
}
],
"buttons": [
{
"id": "<string>",
"title": "<string>"
}
],
"listButton": "<string>",
"sections": [
{
"title": "<string>",
"rows": [
{
"id": "<string>",
"title": "<string>",
"description": "<string>"
}
]
}
],
"ctaDisplayText": "See Dates",
"ctaUrl": "https://example.com/schedule",
"ctaHeaderType": "text",
"ctaHeaderText": "<string>",
"ctaHeaderMediaUrl": "<string>",
"footerText": "Dates subject to change.",
"emoji": "<string>",
"reactToMessageId": "<string>",
"replyToMessageId": "<string>",
"replyToProviderMessageId": "<string>",
"replyToFrom": "<string>",
"replyToText": "<string>",
"replyToMessageType": "<string>",
"templateId": "<string>",
"templateVariables": {
"1": "John",
"2": "ORD-12345"
},
"templateButtonVariables": {
"0": "abc-report-token"
},
"templateHeaderVariables": {
"1": "Jorge y Laura"
},
"referral": {
"sourceType": "ad",
"sourceId": "120210000000000000",
"sourceUrl": "<string>",
"headline": "<string>",
"body": "<string>",
"mediaType": "image",
"imageUrl": "<string>",
"videoUrl": "<string>",
"thumbnailUrl": "<string>",
"ctwaClid": "ARIzZm9vYmFyY3R3YWNsaWQ"
}
},
"conversationId": "js723987cyghwqxxaxcf590qd18axd95",
"providerMessageId": "<string>",
"errorCode": "<string>",
"errorMessage": "<string>",
"cost": 123,
"costProvider": 123,
"costTotal": 123,
"metadata": {},
"updatedAt": "2023-11-07T05:31:56Z",
"attemptedChannels": [
"sms"
],
"riskHold": "held",
"errorDetails": {
"providerCode": "3"
},
"charged": 123
},
"routing": {
"channel": "sms",
"from": "<string>",
"reason": "explicit",
"estimatedCost": 123
}
}