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

# Migrate from v1

> Move from Zavu API v1 to v2 at your own pace. v1 keeps working unchanged. What maps to what, what behaves differently, and what stays on v1.

## v1 keeps working

v1 is frozen, not retired. Its endpoints, payloads and webhooks keep working
exactly as they do today. There is no deadline to move.

You can migrate one call at a time. Both versions take the same API key and
share the same data: a message, conversation, contact, campaign or agent created
through one is read through the other with the same ID. Every entry of
`GET /v2/channels` carries `v1SenderId`, the sender to use as `Zavu-Sender` on
the v1 calls you have not moved yet.

## What maps to what

| v1 | v2 |
| - | - |
| `Zavu-Sender` header | `from` (an address) or `conversationId` in the body |
| Senders (`/v1/senders`) | Addresses: `GET /v2/channels`, `GET /v2/channels/{address}` |
| `POST /v1/senders`, `PATCH /v1/senders/{id}` with `enableSmsOneway` or `emailAddress`, `POST /v1/senders/{id}/telegram` | `POST /v2/channels` |
| `POST /v1/senders/{id}/channels/{channel}/activate` and `/deactivate` | `POST /v2/channels/{address}/{type}/activate` and `/deactivate` |
| `DELETE /v1/senders/{id}/telegram`, `PATCH /v1/senders/{id}` with `enableSmsOneway: false` or `enableVoice: false` | `DELETE /v2/channels/{address}/{type}` |
| `POST /v1/messages`, `GET /v1/messages`, `GET /v1/messages/{id}` | `POST /v2/messages`, `GET /v2/messages`, `GET /v2/messages/{id}` |
| `/v1/conversations`, `?senderId=` | `/v2/conversations`, `?from=` |
| `senderId` on a conversation | `from`, the address it was last handled from |
| `/v1/contacts` (create, list, get, update, delete) | `/v2/contacts` |
| Sender webhooks (`webhookUrl` on each sender) | One project webhook: `PUT /v2/webhook` |
| `/v1/broadcasts` | `/v2/campaigns` (list, create, get, delete, send, cancel, progress) |
| `POST /v1/broadcasts/{id}/contacts` | `POST /v2/campaigns/{id}/recipients` |
| `GET /v1/phone-numbers/available` | `GET /v2/numbers/available` |
| `POST /v1/phone-numbers`, then assign it to a sender | `POST /v2/numbers`: buys the number and turns SMS on in one call |
| `/v1/templates` (list, create, get, delete) | `/v2/templates` |
| `POST /v1/templates/{id}/submit` with `senderId` | `POST /v2/templates/{id}/submit` with `from` |
| `/v1/agents` (list, create, get, update, delete, test) | `/v2/agents` |
| `POST /v1/agents/{id}/senders`, `DELETE /v1/agents/{id}/senders/{senderId}` | `POST /v2/agents/{id}/channels` with `{address, type}`, `DELETE /v2/agents/{id}/channels/{address}/{type}` |
| `/v1/email-domains` | `/v2/email-domains` |
| `PATCH /v1/functions/{id}` with `httpEnabled` | `PATCH /v2/functions/{id}`, which also sets name, description, timeout, memory, architecture and pause |
| No function listing | `GET /v2/functions`, `GET /v2/functions/{id}` |
| Errors, some wrapped in `{"error": {...}}` | Every error is `{code, message, details}` at the root |

A campaign is a v1 broadcast: its `id` works as `broadcastId` on
`/v1/broadcasts/{broadcastId}`.

## Sending: from a header to an address

<CodeGroup>
  ```bash v1 theme={null}
  curl -X POST https://api.zavu.dev/v1/messages \
    -H "Authorization: Bearer $ZAVUDEV_API_KEY" \
    -H "Zavu-Sender: kd7abc123def456" \
    -H "Content-Type: application/json" \
    -d '{"to": "+56912345678", "text": "Your code is 123456."}'
  ```

  ```bash v2 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."}'
  ```
</CodeGroup>

The `202` carries `message` and `routing`. `message.from` and `message.to` name
addresses; there is no `senderId`.

To reply in a thread, pass `conversationId` and drop both `from` and `to`. See
[Replying in a conversation](/api-v2/overview#replying-in-a-conversation).

## Webhooks

Your v1 sender webhooks keep receiving events after you create the v2 webhook.
Each event then goes to both, as separate deliveries with different event ids.

1. `PUT /v2/webhook` with your new URL and events.
2. Check that your receiver verifies and handles the v2 payload. `from`, `to` and
   `type` sit at the top level; `data` is what v1 sends, without `senderId`.
3. `PUT /v2/webhook` again with `"disableSenderWebhooks": true`. That switches
   the v1 sender webhooks off and keeps their configuration, so each event
   arrives once.

The v1 project webhook (`/v1/invitations/webhook`) is separate and is not
switched off. Details in [Webhook](/api-v2/webhook).

## What behaves differently in v2

Same engine, same limits and prices. These are the places where a v2 call
answers differently from the same v1 call.

### Sending

* **Only active connections are picked.** Without `channel`, a connection that
  is switched off is skipped. If nothing active can reach the recipient, the
  answer is `409 no_channel`. Naming a switched-off transport in `channel` is
  `403 channel_inactive`.
* **One answer for "nothing can send".** No channel, a `channel` that is not
  connected, an email `to` without email, a WhatsApp-only `messageType` without
  WhatsApp: all `409 no_channel`, with `details.connect` listing what to connect
  and the request for each. v1 answers several of these with `400`.
* **Contradictions are refused.** An email `to` with a channel other than
  `email`, `channel: "email"` with a phone number, or a WhatsApp-only
  `messageType` with another `channel` is `400`, naming both fields.
* **Bare digits are not guessed.** A `to` of 8 to 15 digits without `+` is a
  chat id. When nothing in the request can reach a chat id, v2 refuses it with
  `details.suggestion` holding the number with its `+`. A number written with
  spaces, dashes or parentheses after its `+` is read as E.164.
* **Fallback checks first.** Before a failed WhatsApp message falls back to SMS,
  v2 checks that the connection it would fall back to is switched on and, for a
  +1 number, that the project has an approved 10DLC campaign. Otherwise the
  message stays `failed`, with `errorCode` `CHANNEL_INACTIVE` or
  `TEN_DLC_REQUIRED`.
* **Replies stay in their thread.** With `conversationId`, `to` can be omitted,
  and a `to` that is not the thread's contact is `400`. The `202` carries
  `message.conversationId` when the thread already exists.
* **10DLC refusals say how to fix them.** `403 ten_dlc_required` carries the
  registration as API calls, in order, and the channels that reach the same
  person without 10DLC.

### Messages

* Amounts are USD rounded to 6 decimals.
* `charged` says what an SMS or one-way SMS took from your balance. `costTotal`
  is not that number for SMS.
* `errorCode` is a Zavu code. A delivery provider's own code is translated where
  its meaning is known and kept in `errorDetails.providerCode`.
* `attemptedChannels` lists the transports tried before a fallback, and
  `riskHold` says whether the message is waiting for risk review.

### Contacts

* An identifier another contact already holds on the same channel is
  `400 duplicate_identifier`, with `details.conflicts` naming that contact. v1
  refuses it too, as a plain `400 invalid_request` with no `details`.
* A phone identifier (`sms`, `whatsapp`, `voice`) must be E.164; WhatsApp also
  takes a BSUID.
* Deleting a contact that is already gone is `404`, as on v1.

### Campaigns

* Creating a campaign checks its channel: a `from` without that channel is
  `409 no_channel`, not a campaign that fails at send.
* Adding recipients: a malformed recipient, or bare digits on a phone campaign,
  refuses the whole request with `400` and the path of the recipient. A
  well-formed recipient the campaign cannot reach is skipped and reported in
  `invalid`, with `code` `unreachable_by_channel` or `ten_dlc_required`.
* Acting on a campaign in the wrong state (cancel a finished one, add recipients
  to or delete one that is not a draft) is `409 invalid_status`. v1 answers
  `400`.

### Channels, agents, domains

* A Telegram token Telegram rejects is `400 invalid_bot_token`.
* Connecting an agent to one-way SMS is `409 channel_cannot_receive`: one-way
  SMS receives nothing, so the agent would never answer.
* `POST /v2/agents` lists every missing required field, not only the first.
* `POST /v2/email-domains` validates `domain` before anything is created, and
  adding a domain the project already has returns it with `200`. A domain whose
  DKIM records could not be generated comes back `verifiable: false` with
  `setupError`.
* Verifying an email domain answers `404` only for a domain that is not yours,
  and `503 verification_unavailable` when the DNS check could not run.

### Errors and webhooks

* Every error is flat, and every `400 invalid_request` carries `details.errors`
  with field paths. A body that is not JSON is `400 invalid_json`, as on v1.
* A known path with the wrong method is `405`, not `404`, on every v2 path
  except `/v2/numbers` and `/v2/functions`.
* The v2 webhook cannot move straight from `v2` signatures back to `v1`; go
  through `v1+v2`, as in the other direction.

## What stays on v1

These have no v2 endpoint. Call them on v1 with the same API key, using
`v1SenderId` from `GET /v2/channels` where they take a sender.

| Area | v1 endpoints |
| - | - |
| Function code | Create, delete, deploy, rollback, deployments, logs, triggers, secrets, Git link (`/v1/functions`) |
| Voice calls | `/v1/calls` |
| 10DLC registration | `/v1/10dlc/brands`, `/v1/10dlc/campaigns` |
| Regulatory information | `/v1/phone-numbers/requirements`, `/v1/addresses`, `/v1/documents` |
| Owned numbers | List, get, rename, release (`/v1/phone-numbers`) |
| Agent internals | Tools, flows and knowledge bases (`/v1/agents/{agentId}/...`); executions and stats; voices; factory agents |
| Memory | `/v1/memory` |
| Contacts, beyond CRUD | Add, update and remove a contact's channels, merge, look up by phone |
| Campaigns, beyond the basics | Update, reschedule, retry review, escalate, list and remove recipients |
| Messages, beyond send and read | Reactions, typing indicator, attachments |
| Templates | Sync from WhatsApp (`/v1/templates/sync`) |
| WhatsApp | Business profile, history and contacts sync |
| Account | `/v1/me`, `/v1/balance`, sub-accounts, partner invitations, URL verification, phone and email introspection |


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