Skip to main content

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

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

Sending: from a header to an address

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.

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.

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.