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
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."}'
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.
With the TypeScript SDK
@zavudev/sdk 0.59.0 calls v2 as client.v2. Moving a call is changing
client.x to client.v2.x and the sender to an address:
await zavu.messages.send({
to: '+56912345678',
text: 'Your code is 123456.',
'Zavu-Sender': 'kd7abc123def456',
});
On v2 every id in the path is a positional argument, in path order:
zavu.v2.channels.activate('+13125551212', 'whatsapp'). v2's types come from
@zavudev/sdk/v2, because its Message and Contact are not v1's. See
TypeScript SDK: API v2.
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.
PUT /v2/webhookwith your new URL and events.- Check that your receiver verifies and handles the v2 payload.
from,toandtypesit at the top level;datais what v1 sends, withoutsenderId. PUT /v2/webhookagain 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.
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 is409 no_channel. Naming a switched-off transport inchannelis403 channel_inactive. - One answer for "nothing can send". No channel, a
channelthat is not connected, an emailtowithout email, a WhatsApp-onlymessageTypewithout WhatsApp: all409 no_channel, withdetails.connectlisting what to connect and the request for each. v1 answers several of these with400. - Contradictions are refused. An email
towith a channel other thanemail,channel: "email"with a phone number, or a WhatsApp-onlymessageTypewith anotherchannelis400, naming both fields. - Bare digits are not guessed. A
toof 8 to 15 digits without+is a chat id. When nothing in the request can reach a chat id, v2 refuses it withdetails.suggestionholding 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, witherrorCodeCHANNEL_INACTIVEorTEN_DLC_REQUIRED. - Replies stay in their thread. With
conversationId,tocan be omitted, and atothat is not the thread's contact is400. The202carriesmessage.conversationIdwhen the thread already exists. - 10DLC refusals say how to fix them.
403 ten_dlc_requiredcarries 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.
chargedsays what an SMS or one-way SMS took from your balance.costTotalis not that number for SMS.errorCodeis a Zavu code. A delivery provider's own code is translated where its meaning is known and kept inerrorDetails.providerCode.attemptedChannelslists the transports tried before a fallback, andriskHoldsays whether the message is waiting for risk review.
Contacts
- An identifier another contact already holds on the same channel is
400 duplicate_identifier, withdetails.conflictsnaming that contact. v1 refuses it too, as a plain400 invalid_requestwith nodetails. - 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
fromwithout that channel is409 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
400and the path of the recipient. A well-formed recipient the campaign cannot reach is skipped and reported ininvalid, withcodeunreachable_by_channelorten_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 answers400.
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/agentslists every missing required field, not only the first.POST /v2/email-domainsvalidatesdomainbefore anything is created, and adding a domain the project already has returns it with200. A domain whose DKIM records could not be generated comes backverifiable: falsewithsetupError.- Verifying an email domain answers
404only for a domain that is not yours, and503 verification_unavailablewhen the DNS check could not run.
Errors and webhooks
- Every error is flat, and every
400 invalid_requestcarriesdetails.errorswith field paths. A body that is not JSON is400 invalid_json, as on v1. - A known path with the wrong method is
405, not404, on every v2 path except/v2/numbersand/v2/functions. - The v2 webhook cannot move straight from
v2signatures back tov1; go throughv1+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) |
| Business profile, history and contacts sync | |
| Account | /v1/me, /v1/balance, sub-accounts, partner invitations, URL verification, phone and email introspection |