> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zavu.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# 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-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](/api-v2/migrate-from-v1).

## Base URL and authentication

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

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

```bash theme={null}
curl https://api.zavu.dev/v2/channels \
  -H "Authorization: Bearer $ZAVUDEV_API_KEY"
```

The [rate limit](/concepts/rate-limiting) is per project and shared: v1 and v2
requests count against the same window. [Sending limits](/concepts/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.

<Note>
  The SDKs (`@zavudev/sdk` on npm, `zavudev` on PyPI, and the others) call v1
  today. Call v2 over HTTP: `curl`, `fetch`, or any HTTP client.
</Note>

## 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 theme={null}
{
  "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](/api-v2/addresses).

## Send your first message

<Steps>
  <Step title="Connect a channel">
    `POST /v2/channels` connects what needs no purchase and no Meta signup.
    Pick one:

    <CodeGroup>
      ```bash Email theme={null}
      # 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"}'
      ```

      ```bash Telegram theme={null}
      # A bot token from @BotFather. Its address is @botname.
      curl -X POST https://api.zavu.dev/v2/channels \
        -H "Authorization: Bearer $ZAVUDEV_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{"type": "telegram", "botToken": "123456789:AA..."}'
      ```

      ```bash One-way SMS theme={null}
      # No number or credential. Recipients cannot reply. US and Canadian (+1) numbers
      # need an approved 10DLC campaign first (403 ten_dlc_required): use email or Telegram there.
      curl -X POST https://api.zavu.dev/v2/channels \
        -H "Authorization: Bearer $ZAVUDEV_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{"type": "sms_oneway"}'
      ```
    </CodeGroup>

    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.
  </Step>

  <Step title="Send">
    In a new project the first channel lands on the default, so `from` is not
    needed.

    ```bash theme={null}
    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."
      }'
    ```

    ```json 202 Accepted theme={null}
    {
      "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
      }
    }
    ```
  </Step>

  <Step title="Pick the sending address">
    With more than one address, name it in `from`:

    ```bash theme={null}
    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."}'
    ```
  </Step>
</Steps>

### 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`:

| `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.

```json 409 Conflict theme={null}
{
  "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.

```bash theme={null}
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](/api-v2/webhook).

## Errors

Every error is flat:

```json theme={null}
{ "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](/api-v2/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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.