Skip to content

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.

View .md

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-Sender header. Pass from, or reply inside a thread with conversationId.
  • Every send says how it was routed. The 202 carries routing: 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

Code
https://api.zavu.dev/v2

Authenticate with the same API key you use on v1, as a Bearer token:

terminal
curl 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.

TypeScript
import 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.

TypeAddress
sms, voice, whatsappE.164 number, e.g. +13125551212
emailThe email address, lowercase
telegram@botname
instagram@username
messengerThe Facebook Page id
sms_onewayThe 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

  1. Connect a channel

    POST /v2/channels connects 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.

  2. Send

    In a new project the first channel lands on the default, so from is not needed.

    terminal
    curl -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 } }
  3. Pick the sending address

    With more than one address, name it in from:

    terminal
    curl -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:

  1. POST /v2/email-domains with {"domain": "yourdomain.com"}. The answer has the DNS records to publish.
  2. Publish them exactly as returned.
  3. POST /v2/email-domains/{emailDomainId}/verify.
  4. POST /v2/channels with {"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:

reasonWhen
sandboxA test-mode key. Always the WhatsApp sandbox.
recipient_typeto is an email address, so email.
message_typemessageType is WhatsApp-only (media, interactive, template), so WhatsApp.
explicitchannel was given.
conversationchannel omitted, and the sender came from conversationId.
auto_orderchannel 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.

terminal
curl -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.