API v2
A smaller Zavu API on the same engine as v1. No sender object: you send from an address. One project webhook, flat errors, and a 409 that tells you what to connect.
What v2 is
v2 is a second version of the Zavu REST API. It runs on the same engine as v1: the same API keys, the same sending limits, prices and reviews. v1 keeps working unchanged, and you can call both from the same code.
What v2 changes is the shape:
- No sender object. You send from an address, the way the recipient sees it: a phone number, an email address, a bot name.
- No
Zavu-Senderheader. Passfrom, or reply inside a thread withconversationId. - Every send says how it was routed. The
202carriesrouting: the channel, the address it left from, and why. - One webhook for the project. Every event, at one URL.
- One error shape.
{code, message, details}at the root of every error.
Coming from v1? Read Migrate from v1.
Base URL and authentication
Codehttps://api.zavu.dev/v2
Authenticate with the same API key you use on v1, as a Bearer token:
terminalcurl https://api.zavu.dev/v2/channels \ -H "Authorization: Bearer $ZAVUDEV_API_KEY"
The rate limit is per project and shared: v1 and v2 requests count against the same window. Sending limits apply to a message the same way whichever version sent it.
A test-mode key sends from the WhatsApp sandbox on v2, as it does on v1. from
and conversationId are not consulted.
TypeScriptimport Zavudev from '@zavudev/sdk'; const zavu = new Zavudev(); // reads ZAVUDEV_API_KEY for await (const entry of zavu.v2.channels.list()) { console.log(entry.address, entry.type, entry.active); }
Addresses
An address is where a message leaves from, written the way the recipient sees it.
| Type | Address |
|---|---|
sms, voice, whatsapp | E.164 number, e.g. +13125551212 |
email | The email address, lowercase |
telegram | @botname |
instagram | @username |
messenger | The Facebook Page id |
sms_oneway | The literal sms_oneway. One-way SMS leaves from shared numbers, never a number of your own. |
GET /v2/channels lists one entry per (address, type). A number that carries
WhatsApp and SMS is two entries with the same address. active: false means the
connection exists but is switched off.
JSON{ "items": [ { "address": "+13125551212", "type": "whatsapp", "active": true, "v1SenderId": "kd7..." }, { "address": "+13125551212", "type": "sms", "active": true, "v1SenderId": "kd7..." }, { "address": "noreply@yourdomain.com", "type": "email", "active": true, "v1SenderId": "kd7..." } ], "nextCursor": null }
More in Addresses and channels.
Send your first message
Connect a channel
POST /v2/channelsconnects what needs no purchase and no Meta signup. Pick one:# Needs a verified email domain. See "Email domains" below. curl -X POST https://api.zavu.dev/v2/channels \ -H "Authorization: Bearer $ZAVUDEV_API_KEY" \ -H "Content-Type: application/json" \ -d '{"type": "email", "address": "noreply@yourdomain.com", "fromName": "Acme"}'The answer lists the entries of the address you connected. Two-way SMS and voice come with a number of your own (
POST /v2/numbers). WhatsApp, Instagram and Messenger connect through Meta, from Accounts in the dashboard.Send
In a new project the first channel lands on the default, so
fromis not needed.terminalcurl -X POST https://api.zavu.dev/v2/messages \ -H "Authorization: Bearer $ZAVUDEV_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "to": "maria@example.com", "subject": "Your order shipped", "text": "Order ORD-12345 is on its way." }'202 Accepted{ "message": { "id": "jd7x2k3m4n5p6q7r8s9t0", "to": "maria@example.com", "from": "noreply@yourdomain.com", "channel": "email", "direction": "outbound", "messageType": "text", "status": "queued", "createdAt": "2026-10-14T15:04:05.000Z" }, "routing": { "channel": "email", "from": "noreply@yourdomain.com", "reason": "recipient_type", "estimatedCost": null } }Pick the sending address
With more than one address, name it in
from:terminalcurl -X POST https://api.zavu.dev/v2/messages \ -H "Authorization: Bearer $ZAVUDEV_API_KEY" \ -H "Content-Type: application/json" \ -d '{"from": "+13125551212", "to": "+56912345678", "text": "Your code is 123456."}'
Email domains
An email address can only be connected at a verified domain of the project:
POST /v2/email-domainswith{"domain": "yourdomain.com"}. The answer has the DNS records to publish.- Publish them exactly as returned.
POST /v2/email-domains/{emailDomainId}/verify.POST /v2/channelswith{"type": "email", "address": "noreply@yourdomain.com"}.
Connecting an address at a domain that is missing or not verified answers
409 email_domain_missing or 409 email_domain_not_verified, with the call
that fixes it in details.recommendation.
How a send is routed
The message leaves from conversationId if given, else from, else the project
default. The transport is the first rule that applies, reported in
routing.reason:
reason | When |
|---|---|
sandbox | A test-mode key. Always the WhatsApp sandbox. |
recipient_type | to is an email address, so email. |
message_type | messageType is WhatsApp-only (media, interactive, template), so WhatsApp. |
explicit | channel was given. |
conversation | channel omitted, and the sender came from conversationId. |
auto_order | channel omitted: the first active transport, for a phone number WhatsApp, then SMS, then one-way SMS. |
The order does not compare price or check the WhatsApp 24-hour window. A
connection that is switched off is skipped. A WhatsApp-to-SMS fallback happens
after the 202, so it never appears in routing; read attemptedChannels on
the message.
When nothing can send: 409 no_channel
Every "nothing can send this" is the same answer: no channel at all, a channel
that is not connected, an email to without email, a WhatsApp-only
messageType without WhatsApp. details.connect lists what this project can
connect to reach to, with the exact request for each.
409 Conflict{ "code": "no_channel", "message": "This project has no channel that can send to +56912345678 yet.", "details": { "recommendation": "Connect one with POST /v2/channels {\"type\":\"sms_oneway\"}, or POST /v2/numbers, or from Accounts in the dashboard. `details.connect` says what each needs.", "connect": [ { "type": "sms_oneway", "request": { "method": "POST", "path": "/v2/channels", "body": { "type": "sms_oneway" } }, "note": "One-way SMS from shared numbers; recipients cannot reply. US and Canadian (+1) numbers need an approved 10DLC campaign first: reach them by email or Telegram meanwhile.", "available": true }, { "type": "sms", "request": { "method": "POST", "path": "/v2/numbers", "body": { "phoneNumber": "<one from GET /v2/numbers/available>" } }, "note": "Two-way SMS from a number of your own. Buying one needs a paid plan.", "available": true }, { "type": "whatsapp", "request": null, "note": "Connected through Meta's signup, from Accounts in the dashboard.", "available": true, "dashboardUrl": "/accounts" } ] } }
request: null means the channel is connected from the dashboard.
available: false says why in note, for example one-way SMS before business
verification is approved.
Replying in a conversation
Pass conversationId instead of from. The reply leaves from the address the
thread was last handled from and goes to whoever the thread is with, so to can
be omitted.
terminalcurl -X POST https://api.zavu.dev/v2/messages \ -H "Authorization: Bearer $ZAVUDEV_API_KEY" \ -H "Content-Type: application/json" \ -d '{"conversationId": "js723987cyghwqxxaxcf590qd18axd95", "text": "Thanks, we are on it."}'
A to that is not the thread's contact is refused with 400, rather than
starting another conversation.
Webhook
One webhook per project, configured with PUT /v2/webhook, receives every event
you subscribe to. Payloads name addresses, never a sender id. See
Webhook.
Errors
Every error is flat:
JSON{ "code": "invalid_request", "message": "Invalid request body", "details": { "errors": [{ "path": ["to"], "message": "Required" }] } }
A known path called with the wrong method answers 405 method_not_allowed, with
details.allowed and an Allow header. See Errors.
Money
Every amount in v2 is USD, rounded to 6 decimals. routing.estimatedCost is
what an SMS took from your balance when it was accepted; it is null where the
price is not known at send time (WhatsApp, email, voice). message.charged is
what an SMS has taken so far.
Reference
Every operation, with its request and response schemas, is listed under API v2 reference in this tab.