Messaging API

One endpoint.Every channel.

POST /v1/messages takes a phone number in E.164, an email address, a numeric chat ID or a WhatsApp business-scoped user ID. Zavu picks the channel, records the delivery, and tells your endpoint what happened.

to · E.164+14155551234
to · emailmaria@example.com
to · chat ID123456789
to · BSUIDUS.13491208655302741918
Channels
9 behind one body
Sends
Idempotent by key
Webhooks
HMAC-signed, v2
Reads
Cursor-paginated

Request builder

Pick a channel. Read the body.

The same shape covers SMS, WhatsApp, Telegram, email and voice. Change the channel and the message type below — the request and the 202 response are what you would actually send and receive.

zavu · request builderPOST /v1/messages
channel
messageType
options
Headers
Authorization:
Bearer zv_live_...
Content-Type:
application/json
Zavu-Sender:
sender_12345
Request
{
  "to": "+14155551234",
  "channel": "whatsapp",
  "messageType": "buttons",
  "text": "How would you rate your experience?",
  "content": {
    "buttons": [
      {
        "id": "great",
        "title": "Great"
      },
      {
        "id": "okay",
        "title": "It was okay"
      },
      {
        "id": "poor",
        "title": "Not good"
      }
    ]
  },
  "idempotencyKey": "msg_01HZY4ZP7VQY2J3BRW7Z6G0QGE"
}
Response · 202 Accepted
{
  "message": {
    "id": "jd7x2k3m4n5p6q7r8s9t0",
    "to": "+14155551234",
    "from": "+13125551212",
    "senderId": "sender_12345",
    "channel": "whatsapp",
    "messageType": "buttons",
    "status": "queued",
    "text": "How would you rate your experience?",
    "conversationId": "js723987cyghwqxxaxcf590qd18axd95",
    "createdAt": "2026-02-11T09:24:18.412Z"
  }
}

whatsapp · buttons

Free-form text needs an open 24-hour window. Outside it, send a template — anything else returns 400 whatsapp_window_closed.

Three at most. The tapped `id` comes back on the inbound webhook.

A non-text message type routes to WhatsApp automatically, whatever `channel` says.

SDKs

Send it from your stack.

TypeScript, Python and curl on every operation the SDK generates. Conversations and webhook verification are shown as REST, because the SDK does not cover them yet.

send.ts
npm add @zavudev/sdk
import Zavudev from "@zavudev/sdk"

const zavu = new Zavudev({ apiKey: process.env.ZAVUDEV_API_KEY })

const { message } = await zavu.messages.send({
  to: "+14155551234",
  text: "Your order ORD-12345 has shipped.",
  // channel defaults to "auto"; omit it and Zavu routes the message
  idempotencyKey: "msg_01HZY4ZP7VQY2J3BRW7Z6G0QGE",
  "Zavu-Sender": "sender_12345",
})

console.log(message.id, message.status) // -> "queued"

Official SDKs: TypeScript, Python, Ruby, Go, PHP. Auth is a Bearer token; the sender override is a header.

Delivery

Every status is a webhook.

A message moves through queued, sent and delivered — and, on WhatsApp, read. Click a step to see the exact event your endpoint receives.

channel
outcome
Webhook payload · message.queued
{
  "id": "evt_1786113454000_a1b2c3",
  "type": "message.queued",
  "timestamp": 1786113454000,
  "senderId": "sender_12345",
  "projectId": "prj_7a1e08d4",
  "data": {
    "messageId": "jx7744x3wckc9cwy0j9c8g6v0h7z7shs",
    "to": "+14155551234",
    "channel": "whatsapp",
    "status": "queued"
  }
}

Created and queued. Nothing has left Zavu yet.

Statuses are also readable at any time: GET /v1/messages/{messageId}. The webhook is the push, not the only copy.

Signatures

Verify before you trust.

Every webhook carries X-Zavu-Signature. Which scheme a sender uses is a setting, so you can move a receiver without dropping an event.

X-Zavu-Signature: t=1786113454,v2=b4b2b61cdcdb241e4270298a08c3c13c964642df…
v1v1=HMAC_SHA256(secret, body)

The scheme used before this was configurable. Existing webhooks stay on it until you move them.

v1+v2both, sharing one t

The migration setting. A receiver reading either one works, so you can deploy and confirm the new verifier first.

v2v2=HMAC_SHA256(secret, "{t}.{body}")

The current scheme and the default for new senders. It signs the timestamp together with the body.

Moving from v1 straight to v2 returns 400. Set v1+v2 first.

Refusals

What the API refuses.

Each of these is a documented response, not a silent drop. Read the code and you know what to do next.

400whatsapp_window_closed

Free-form WhatsApp outside the 24-hour window. Send a template, or wait for the contact to write first.

403url_not_verified

An SMS or email carrying a URL nobody submitted. Pre-verify it with POST /v1/urls.

403url_shortener_blocked

bit.ly, t.co and the rest hide their destination. Use the full URL.

402insufficient_balance

SMS and voice bill per message; email bills in blocks. At zero balance the send is rejected rather than queued.

429a2p_limit_exceeded

The Free plan's shared allowance is spent. It resets on the 1st; paid plans have no message cap.

409idempotency conflict

That idempotencyKey already sent a message. Retry as loudly as you like.

Email is checked before it is sent

A send that would be a guaranteed hard bounce is failed instead of dispatched, so your bounce rate survives a bad list. The message goes to `failed` with one of these codes, visible on the message and on message.failed.

EMAIL_INVALID_RECIPIENT
The address is malformed.
EMAIL_DOMAIN_NOT_FOUND
The domain has no MX or A records.
EMAIL_RECIPIENT_SUPPRESSED
It is on your suppression list after a bounce or a complaint.

Allowances

What a send costs you.

Two different meters. WhatsApp, Telegram, Instagram and Messenger share a monthly allowance; SMS, voice and email come out of your prepaid balance.

WhatsApp · Telegram · Instagram · MessengerFree2,000 / month, shared. Resets on the 1st.PaidNo message cap.
SMS · VoiceFreeBilled per message from balance.PaidBilled per message from balance.
EmailFree$2 of starting credit · 3,000 / month · 100 / dayPaid$0.40 per 1,000 transactional · $0.80 per 1,000 marketing

Email is charged in 1,000-message blocks as your monthly count crosses each boundary. Teams on earlier plans keep their original email quotas.

/pricing

Endpoints

The surface, in full.

Sending is one call. Everything around it — reading a thread, reacting, showing that you are typing, pulling an attachment — is one more.

POST/v1/messages

Send. Returns 202 with the queued message.

GET/v1/messages

List, filtered by status, recipient or channel. Cursor-paginated.

GET/v1/messages/{messageId}

One message, with its cost and conversationId.

POST/v1/messages/{messageId}/reactions

React with an emoji. WhatsApp only.

POST/v1/messages/{messageId}/typing

Mark read and show a typing indicator for up to 25 seconds.

GET/v1/messages/{messageId}/attachments

Signed, short-lived download URLs for email attachments.

GET/v1/conversations

Inbox threads, most recently active first.

GET/v1/conversations/{id}/messages

One thread, across every channel it has carried.

POST/v1/conversations/{id}/read

Reset the thread's unread count. Your inbox only.

Send the first one.

A key, a phone number, one POST. The rest of the surface is there when you need it.

Messaging API | One endpoint for SMS, WhatsApp, email and voice | Zavu