{"openapi":"3.0.3","info":{"title":"Zavu API v2","version":"2.0.0","description":"The second version of the Zavu API. It runs on the same engine as /v1: the same API keys, the same per-project rate limit, the same sending limits, prices and reviews. /v1 keeps working unchanged, and you can mix calls to both.\n\n**Addresses, not senders.** /v2 has no sender object. Where a message leaves from is named the way the recipient sees it: an E.164 number, an email address, `@botname` for Telegram, `@username` for Instagram, the Facebook Page id for Messenger, and the literal `sms_oneway` for one-way SMS (sent 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.\n\n**What else changes.** No `Zavu-Sender` header (`from` or `conversationId` instead); every send reports how it was routed; one project webhook carries every event; every error is `{code, message, details}` at the root.\n\n**Wrong method.** A path under `/v2/messages`, `/v2/conversations`, `/v2/contacts`, `/v2/channels`, `/v2/templates`, `/v2/campaigns`, `/v2/agents`, `/v2/webhook` or `/v2/email-domains` called with a method it does not take answers `405 method_not_allowed`, with `details.allowed` and an `Allow` header.\n\n**Money.** Every amount is USD, rounded to 6 decimals."},"servers":[{"url":"https://api.zavu.dev"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"Messages","description":"Send, list and read messages."},{"name":"Conversations","description":"Inbox threads, one per contact across channels."},{"name":"Contacts","description":"Contacts and the identifiers they are reached at."},{"name":"Channels","description":"Where the project can send from: one entry per (address, type)."},{"name":"Templates","description":"WhatsApp message templates."},{"name":"Campaigns","description":"One message to many recipients."},{"name":"Webhook","description":"The one project webhook for every event."},{"name":"Agents","description":"AI agents and the addresses they answer on."},{"name":"Numbers","description":"Search and buy phone numbers."},{"name":"Email Domains","description":"Domains to send email from."},{"name":"Functions","description":"Configure Zavu Functions."}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"schemas":{"ActivatableChannel":{"type":"string","description":"A channel whose connection has an on/off switch. `sms`, `sms_oneway` and `voice` are billed per message and per phone number, are always on, and have nothing to activate.","enum":["whatsapp","telegram","instagram","messenger","email"]},"AdReferral":{"type":"object","description":"Click-to-WhatsApp (CTWA) ad attribution: where an inbound conversation came from.\n\nWhatsApp only. Present on the **first inbound message** of a conversation opened from a Meta ad or post, and on no message after it — so store it when it arrives rather than expecting it again. Organic conversations never carry it.\n\nField names are camelCased to match the rest of this API; Meta sends them as snake_case (`ctwa_clid`, `source_id`, ...). Fields that do not apply are omitted: a `post` source has no click id, and an image ad has no `videoUrl`.","properties":{"sourceType":{"type":"string","description":"Where the click came from.","enum":["ad","post"]},"sourceId":{"type":"string","description":"Identifier of the ad or post that produced the click.","example":"120210000000000000"},"sourceUrl":{"type":"string","format":"uri","description":"Meta permalink to the ad or post."},"headline":{"type":"string","description":"Headline of the ad or post."},"body":{"type":"string","description":"Body copy of the ad or post."},"mediaType":{"type":"string","description":"Type of media on the ad, when it had any.","enum":["image","video"]},"imageUrl":{"type":"string","format":"uri","description":"Image of the ad. Present when `mediaType` is `image`."},"videoUrl":{"type":"string","format":"uri","description":"Video of the ad. Present when `mediaType` is `video`."},"thumbnailUrl":{"type":"string","format":"uri","description":"Thumbnail of the ad media."},"ctwaClid":{"type":"string","description":"Click-to-WhatsApp click identifier. This is the value Meta's Conversions API needs to credit a conversion back to the ad that produced the conversation. Present on `ad` sources; a `post` source has none.","example":"ARIzZm9vYmFyY3R3YWNsaWQ"}}},"Agent":{"type":"object","description":"An AI agent. It answers on the addresses in `channels`.","required":["id","name","enabled","provider","model","systemPrompt","createdAt","updatedAt","channels"],"properties":{"id":{"type":"string","example":"agent_abc123"},"name":{"type":"string","example":"Customer Support Agent"},"enabled":{"type":"boolean","description":"Whether the agent is active."},"provider":{"$ref":"#/components/schemas/AgentProvider"},"model":{"type":"string","description":"Model ID (e.g., gpt-4o-mini, claude-3-5-sonnet).","example":"gpt-4o-mini"},"systemPrompt":{"type":"string","description":"System prompt for the agent."},"contextWindowMessages":{"type":"integer","description":"Number of previous messages to include as context.","default":10},"includeContactMetadata":{"type":"boolean","description":"Whether to include contact metadata in context.","default":true},"maxTokens":{"type":"integer","description":"Maximum tokens for LLM response.","nullable":true},"temperature":{"type":"number","description":"LLM temperature (0-2).","nullable":true},"triggerOnChannels":{"type":"array","items":{"type":"string"},"description":"Channels that trigger the agent.","example":["sms","whatsapp"]},"triggerOnMessageTypes":{"type":"array","items":{"type":"string"},"description":"Message types that trigger the agent.","example":["text"]},"stats":{"type":"object","properties":{"totalInvocations":{"type":"integer"},"totalTokensUsed":{"type":"integer"},"totalCost":{"type":"number","description":"Total cost in USD."}}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"voice":{"$ref":"#/components/schemas/AgentVoiceConfig","description":"Voice Agent configuration. When present and enabled, the agent can answer inbound phone calls and place outbound calls with Zavu's managed voice pipeline. Requires the Voice Agents feature to be enabled for your team."},"channels":{"type":"array","description":"The (address, type) entries this agent answers on: every transport of the addresses it is connected to that its `triggerOnChannels` allows. Connect one with `POST /v2/agents/{agentId}/channels`.","items":{"type":"object","required":["address","type","active"],"properties":{"address":{"type":"string"},"type":{"$ref":"#/components/schemas/ChannelType"},"active":{"type":"boolean"}}}}}},"AgentCreateStandaloneRequest":{"type":"object","description":"Create an agent that exists on its own, disabled and connected to no channel.","required":["name","provider","model","systemPrompt"],"properties":{"name":{"type":"string","maxLength":100},"provider":{"$ref":"#/components/schemas/AgentProvider"},"apiKey":{"type":"string","description":"Your key for the model provider. Required for `openai`, `anthropic`, `google` and `mistral`, where a request without it is refused with `400`; not used with `zavu`. Stored encrypted and never returned."},"model":{"type":"string"},"systemPrompt":{"type":"string","maxLength":10000},"contextWindowMessages":{"type":"integer","minimum":1,"maximum":50,"default":10},"includeContactMetadata":{"type":"boolean","default":true},"triggerOnChannels":{"type":"array","items":{"type":"string"},"default":["*"]},"triggerOnMessageTypes":{"type":"array","items":{"type":"string"},"default":["text"]},"maxTokens":{"type":"integer","minimum":1,"maximum":4096},"temperature":{"type":"number","minimum":0,"maximum":2},"voice":{"$ref":"#/components/schemas/AgentVoiceConfig"}}},"AgentProvider":{"type":"string","description":"LLM provider for the AI agent.","enum":["openai","anthropic","google","mistral","zavu"]},"AgentTestRequest":{"type":"object","description":"Run the agent and return what it would say. Nothing is delivered to anyone and no execution is recorded.\n\nA run is **billed like a live reply**: on an agent with the `zavu` provider it is charged to your balance at the same price per token, and refused with `402 insufficient_balance` at a balance of zero. On a bring-your-own-key agent it costs your provider, not Zavu. `cost` on the response is what this run charged.","required":["message"],"properties":{"message":{"type":"string","description":"What to say to the agent.","example":"Where is order ORD-12345?"},"history":{"type":"array","description":"Prior turns, oldest first, to exercise multi-turn behaviour without persisting a thread. Trimmed to the agent's context window.","items":{"type":"object","required":["role","content"],"properties":{"role":{"type":"string","enum":["user","assistant"]},"content":{"type":"string"}}}},"useKnowledgeBase":{"type":"boolean","default":true,"description":"Set false to skip retrieval and isolate prompt behaviour from the knowledge base."},"executeTools":{"type":"boolean","default":false,"description":"Run the tools the agent calls instead of reporting the choice and stopping.\n\nOff by default because a tool handler talks to the outside world: a rehearsal that charges a card is not a rehearsal. Turn it on to exercise the loop that actually matters — the model picks a tool, the handler answers, the model replies with the result — without sending a message to anyone. What ran comes back in `executedToolCalls`."}}},"AgentTestResponse":{"type":"object","required":["cost","error","inputTokens","knowledgeChunksUsed","latencyMs","outputTokens","plannedToolCalls","success","text","warnings"],"properties":{"success":{"type":"boolean"},"text":{"type":"string","nullable":true,"description":"What the agent would reply."},"error":{"type":"string","nullable":true},"inputTokens":{"type":"integer"},"outputTokens":{"type":"integer"},"latencyMs":{"type":"integer"},"knowledgeChunksUsed":{"type":"integer","description":"Knowledge-base chunks retrieved for this message. Zero means the answer was not grounded in your documents."},"executedToolCalls":{"type":"array","description":"Tools that actually ran, in order, when the request set `executeTools`. Empty on a normal dry run, where nothing is executed. An entry with `ok: false` means the agent saw an error and answered around it, which is what a customer would have received.","items":{"type":"object","required":["name","ok"],"properties":{"name":{"type":"string"},"ok":{"type":"boolean"},"error":{"type":"string","nullable":true}}}},"warnings":{"type":"array","items":{"type":"string"},"description":"Things that are true of this agent but that a dry run cannot prove. Surfaced so a passing dry run is never mistaken for proof that the agent works live.\n\n- The agent being disabled.\n- Enabled tools that were offered to the model but not run: if the agent picks one, the reply stops at that choice (see `plannedToolCalls`). Pass `executeTools` to run them.\n- Tools that ran for real, when `executeTools` was set, and any that failed.\n- An agent whose sender has none of the channels it triggers on, which answers every dry run and no real message.\n- Contact metadata that exists on a real conversation but not here, and memory tools (`remember`, `recall`, `save_record`), which have no contact to write about on a test run."},"cost":{"type":"number","description":"USD charged to your balance for this run, rounded to 6 decimals. 0 on a bring-your-own-key agent. When the balance covers only part of a run, that part is charged and the next run is refused with `402`."},"plannedToolCalls":{"type":"array","description":"Tools the model chose to call, with the arguments it chose, on a run without `executeTools`. Nothing was executed: the reply stops at the choice. Empty when it answered without reaching for a tool, and on a run that executed tools (see `executedToolCalls`).","items":{"type":"object","required":["name","arguments"],"properties":{"name":{"type":"string"},"arguments":{"type":"string","description":"The arguments as a JSON string."}}}}}},"AgentUpdateRequest":{"type":"object","properties":{"name":{"type":"string","maxLength":100},"enabled":{"type":"boolean"},"provider":{"$ref":"#/components/schemas/AgentProvider"},"model":{"type":"string"},"systemPrompt":{"type":"string","maxLength":10000},"apiKey":{"type":"string"},"contextWindowMessages":{"type":"integer","minimum":1,"maximum":50},"includeContactMetadata":{"type":"boolean"},"maxTokens":{"type":"integer","minimum":1,"maximum":4096,"nullable":true},"temperature":{"type":"number","minimum":0,"maximum":2,"nullable":true},"triggerOnChannels":{"type":"array","items":{"type":"string"}},"triggerOnMessageTypes":{"type":"array","items":{"type":"string"}},"voice":{"$ref":"#/components/schemas/AgentVoiceConfig","description":"Voice Agent configuration. Patch this object to enable voice, change the greeting, or adjust call limits. Requires the Voice Agents feature to be enabled for your team."}}},"AgentVoiceConfig":{"type":"object","description":"Voice Agent configuration on an AI agent. Controls how the agent behaves on inbound and outbound phone calls through Zavu's managed voice pipeline (speech recognition, the agent's LLM, and speech synthesis, with real-time interruption handling). Requires the Voice Agents feature to be enabled for your team.","required":["enabled"],"properties":{"enabled":{"type":"boolean","description":"Whether the agent handles voice calls. When false, the voice agent does not answer calls and outbound calls are rejected."},"greeting":{"type":"string","description":"Opening line the agent speaks when the call connects. If omitted, the agent waits for the caller to speak first.","maxLength":1000,"example":"Hi, thanks for calling Acme. How can I help you today?"},"greetings":{"type":"object","additionalProperties":{"type":"string"},"description":"Greeting per language, keyed by language code. Used when the caller's language differs from the one `greeting` is written in.","example":{"es":"Hola, soy Atlas. Preguntame lo que quieras."}},"language":{"type":"string","description":"BCP-47 language code used for both speech recognition and speech synthesis (e.g. `en`, `es`, `pt-BR`). Auto-detected from the recipient when omitted.","example":"en"},"model":{"type":"string","description":"Model that runs the conversation, co-located in the voice network for lowest latency. Independent of the model used for text messaging. Derived from the agent's text model when omitted.","example":"openai/gpt-4o"},"sttProvider":{"type":"string","description":"Speech-recognition provider. Uses the default when omitted."},"sttModel":{"type":"string","description":"Speech-recognition model. Uses the default when omitted."},"ttsProvider":{"type":"string","description":"Speech-synthesis provider. Uses the default when omitted."},"ttsVoiceId":{"type":"string","description":"Identifier of the synthesized voice that speaks. Choose from the voices available in the dashboard. Uses a neutral default when omitted.","example":"aria"},"voiceSpeed":{"type":"number","description":"Speech rate. 1.0 is natural. Only honoured by voices that support rate control; ignored by the others.","minimum":0.5,"maximum":1.5,"default":1},"interruptible":{"type":"boolean","description":"Whether the caller can interrupt the agent while it is speaking (barge-in). When true, the agent stops talking as soon as the caller starts.","default":true},"maxCallDurationMinutes":{"type":"integer","description":"Hard limit on call length in minutes. The call ends automatically when reached.","minimum":1,"maximum":120,"default":15},"maxIdleSeconds":{"type":"integer","description":"How long the agent waits during silence before ending the call.","minimum":5,"maximum":300,"default":30},"recordCalls":{"type":"boolean","description":"Whether the call audio is recorded."},"voicemailAction":{"type":"string","description":"What the agent does when an answering machine or voicemail is detected on an outbound call.","enum":["hangup","leave_message"],"default":"hangup"},"voicemailMessage":{"type":"string","description":"Message spoken when `voicemailAction` is `leave_message`. Falls back to `greeting` when omitted.","maxLength":1000},"transferPhoneNumber":{"type":"string","description":"E.164 phone number the agent can transfer the call to. When set, the agent is given a transfer tool it can use to hand the call to a human.","example":"+14155551234"}}},"AvailablePhoneNumber":{"type":"object","required":["phoneNumber","capabilities","pricing"],"properties":{"phoneNumber":{"type":"string","example":"+15551234567"},"friendlyName":{"type":"string","example":"(555) 123-4567"},"locality":{"type":"string","example":"San Francisco"},"region":{"type":"string","example":"CA"},"capabilities":{"$ref":"#/components/schemas/PhoneNumberCapabilities"},"pricing":{"$ref":"#/components/schemas/PhoneNumberPricing"}}},"Broadcast":{"type":"object","required":["id","name","status","channel","messageType","totalContacts","createdAt"],"properties":{"id":{"type":"string","example":"brd_abc123"},"name":{"type":"string"},"status":{"$ref":"#/components/schemas/BroadcastStatus"},"channel":{"$ref":"#/components/schemas/BroadcastChannel"},"messageType":{"$ref":"#/components/schemas/BroadcastMessageType"},"text":{"type":"string"},"content":{"$ref":"#/components/schemas/BroadcastContent"},"emailSubject":{"type":"string"},"totalContacts":{"type":"integer","description":"Total number of contacts in the broadcast."},"pendingCount":{"type":"integer"},"sendingCount":{"type":"integer"},"sentCount":{"type":"integer","description":"Recipients whose message the provider accepted, without a confirmed delivery yet. Channels that never report delivery keep their recipients here."},"deliveredCount":{"type":"integer","description":"Recipients with confirmed delivery to the device."},"failedCount":{"type":"integer"},"estimatedCost":{"type":"number","nullable":true,"description":"Estimated total cost in USD."},"reservedAmount":{"type":"number","nullable":true,"description":"Amount reserved from balance in USD."},"actualCost":{"type":"number","nullable":true,"description":"Actual cost so far in USD."},"scheduledAt":{"type":"string","format":"date-time"},"startedAt":{"type":"string","format":"date-time"},"completedAt":{"type":"string","format":"date-time"},"metadata":{"type":"object","additionalProperties":{"type":"string"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"reviewResult":{"type":"object","nullable":true,"description":"AI content review result.","properties":{"score":{"type":"number","description":"Content safety score from 0.0 to 1.0, where 1.0 is completely safe."},"categories":{"type":"array","items":{"type":"string"},"description":"Policy categories violated, if any."},"reasoning":{"type":"string","description":"Explanation of the review decision."},"reviewedAt":{"type":"string","format":"date-time"},"flaggedContent":{"type":"array","items":{"type":"string"},"nullable":true,"description":"Problematic text fragments, if any."}}},"reviewAttempts":{"type":"integer","nullable":true,"description":"Number of review attempts (max 3)."},"from":{"type":"string","nullable":true,"description":"The address the campaign is sent from, for its `channel` (the sender's first address when `channel` is `smart`)."}},"description":"A campaign. The same object as a /v1 broadcast: its `id` works on `/v1/broadcasts/{broadcastId}`."},"BroadcastChannel":{"type":"string","description":"Broadcast delivery channel. Use 'smart' for per-contact intelligent routing.","enum":["smart","sms","sms_oneway","whatsapp","telegram","email"]},"BroadcastContactInput":{"type":"object","required":["recipient"],"properties":{"recipient":{"type":"string","description":"Phone number (E.164) or email address. On a `telegram` broadcast, the numeric chat ID of someone who has already written to your bot (`838360395`, or `telegram:838360395`); a phone number is rejected there, because a bot cannot start a chat.","example":"+14155551234"},"templateVariables":{"type":"object","description":"Per-contact body variables. Key them to match the template body: by position (`1`, `2`, ...) for positional templates, or by name (e.g. `customer_name`) for named templates. Zavu detects the template's format and sends the correct payload to Meta. Do not mix positional and named keys.","additionalProperties":{"type":"string"},"example":{"1":"John","2":"ORD-12345"}},"templateButtonVariables":{"type":"object","description":"Per-contact button variables for dynamic URL/OTP buttons. Keys are the button index (0, 1, 2).","additionalProperties":{"type":"string"},"example":{"0":"abc-report-token"}},"templateHeaderVariables":{"type":"object","description":"Per-contact value for a text-header variable, keyed by `1`. If omitted, Zavu resolves the header from `templateVariables` by the header placeholder's name.","additionalProperties":{"type":"string"},"example":{"1":"Jorge y Laura"}}}},"BroadcastContactsAddRequest":{"type":"object","required":["contacts"],"properties":{"contacts":{"type":"array","description":"List of contacts to add (max 1000 per request).","maxItems":1000,"items":{"$ref":"#/components/schemas/BroadcastContactInput"}}}},"BroadcastContactsAddResponse":{"type":"object","required":["added","duplicates","invalid"],"properties":{"added":{"type":"integer","description":"Number of contacts successfully added."},"duplicates":{"type":"integer","description":"Number of duplicate contacts skipped."},"invalid":{"type":"integer","description":"Number of invalid contacts rejected."},"errors":{"type":"array","description":"Details about invalid contacts.","items":{"type":"object","properties":{"recipient":{"type":"string"},"reason":{"type":"string","description":"Why it was not added, in words that are true of this recipient and this campaign."},"code":{"type":"string","enum":["unreachable_by_channel","ten_dlc_required"],"description":"Set for `unreachable_by_channel` and `ten_dlc_required`. Absent on other reasons."}}}}}},"BroadcastContent":{"type":"object","description":"Content for non-text broadcast message types.","properties":{"mediaUrl":{"type":"string","description":"URL of the media file."},"mediaId":{"type":"string","description":"Media ID if already uploaded."},"mimeType":{"type":"string","description":"MIME type of the media."},"filename":{"type":"string","description":"Filename for documents."},"templateId":{"type":"string","description":"Template ID for template messages."},"templateVariables":{"type":"object","description":"Default body variables (can be overridden per contact). Key them to match the template body: by position (`1`, `2`, ...) for positional templates, or by name (e.g. `customer_name`) for named templates. Zavu detects the template's format and sends the correct payload to Meta. Do not mix positional and named keys.","additionalProperties":{"type":"string"}},"templateButtonVariables":{"type":"object","description":"Default button variables for dynamic URL/OTP buttons. Keys are the button index (0, 1, 2). Per-contact values override these.","additionalProperties":{"type":"string"}},"templateHeaderVariables":{"type":"object","description":"Default value for a text-header variable, keyed by `1` (can be overridden per contact). If omitted, Zavu resolves the header from `templateVariables` by the header placeholder's name.","additionalProperties":{"type":"string"}}}},"BroadcastCreateRequest":{"type":"object","required":["name","channel"],"properties":{"name":{"type":"string","description":"Name of the broadcast campaign.","example":"Black Friday Sale"},"channel":{"$ref":"#/components/schemas/BroadcastChannel"},"messageType":{"$ref":"#/components/schemas/BroadcastMessageType","default":"text"},"text":{"type":"string","description":"Text content or caption. Supports template variables: {{name}}, {{1}}, etc.","example":"Hi {{name}}, check out our Black Friday deals!"},"content":{"$ref":"#/components/schemas/BroadcastContent"},"emailSubject":{"type":"string","description":"Email subject line. Required for email broadcasts."},"emailHtmlBody":{"type":"string","description":"HTML body for email broadcasts."},"scheduledAt":{"type":"string","format":"date-time","description":"Schedule the broadcast for future delivery."},"metadata":{"type":"object","additionalProperties":{"type":"string"}},"idempotencyKey":{"type":"string","description":"Idempotency key to prevent duplicate broadcasts."},"from":{"type":"string","description":"The address that sends the campaign (`GET /v2/channels`). The project default when omitted. Resolved like `from` on `POST /v2/messages`, with the campaign's `channel` as the transport (none for `smart`)."}},"description":"A campaign is a /v1 broadcast. Create it, add recipients, then send."},"BroadcastMessageType":{"type":"string","description":"Type of message for broadcast.","enum":["text","image","video","audio","document","template"]},"BroadcastProgress":{"type":"object","required":["campaignId","status","total","pending","sending","delivered","failed","skipped","percentComplete"],"properties":{"status":{"$ref":"#/components/schemas/BroadcastStatus"},"total":{"type":"integer","description":"Total contacts in broadcast."},"pending":{"type":"integer","description":"Not yet queued for sending."},"sending":{"type":"integer","description":"Currently being sent."},"sent":{"type":"integer","description":"Accepted by the provider, delivery not confirmed yet."},"delivered":{"type":"integer","description":"Confirmed delivered to the device."},"failed":{"type":"integer","description":"Failed to deliver."},"skipped":{"type":"integer","description":"Skipped (broadcast cancelled)."},"percentComplete":{"type":"number","description":"Percentage complete (0-100)."},"estimatedCost":{"type":"number","nullable":true,"description":"Estimated total cost in USD."},"reservedAmount":{"type":"number","nullable":true,"description":"Amount reserved from balance in USD."},"actualCost":{"type":"number","nullable":true,"description":"Actual cost so far in USD."},"startedAt":{"type":"string","format":"date-time"},"estimatedCompletionAt":{"type":"string","format":"date-time"},"campaignId":{"type":"string"}}},"BroadcastSendRequest":{"type":"object","properties":{"scheduledAt":{"type":"string","format":"date-time","description":"Schedule for future delivery. Omit to send immediately."}}},"BroadcastStatus":{"type":"string","description":"Current status of the campaign. `pending_review`: in the automated content review. `pending_admin_review`: the automated review passed and it waits for a person, which is where every channel but a WhatsApp campaign on an approved template goes. `rejected`: the review refused it; edit it and send again, or escalate. `scheduled` and `sending` follow approval.","enum":["draft","pending_review","pending_admin_review","approved","rejected","escalated","rejected_final","scheduled","sending","paused","completed","cancelled","failed"]},"ChannelType":{"type":"string","description":"A transport a message travels on.","enum":["sms","sms_oneway","whatsapp","telegram","email","instagram","messenger","voice"]},"ChannelTypeRequest":{"type":"string","description":"`auto`, or a transport. `auto` applies the fixed order described on `POST /v2/messages`.","enum":["auto","sms","sms_oneway","whatsapp","telegram","email","instagram","messenger","voice"]},"Contact":{"type":"object","required":["id","availableChannels","verified","metadata","createdAt"],"properties":{"id":{"type":"string"},"displayName":{"type":"string","description":"Display name for the contact.","example":"John Doe"},"phoneNumber":{"type":"string","description":"DEPRECATED: Use primaryPhone instead. Primary phone number in E.164 format.","example":"+56912345678"},"primaryPhone":{"type":"string","description":"Primary phone number in E.164 format.","example":"+56912345678"},"primaryEmail":{"type":"string","format":"email","description":"Primary email address.","example":"john@example.com"},"countryCode":{"type":"string","example":"CL"},"profileName":{"type":"string","nullable":true,"description":"Contact's WhatsApp profile name. Only available for WhatsApp contacts.","example":"John Doe"},"whatsapp":{"type":"object","description":"WhatsApp identity. Present when Zavu has a business-scoped user ID (BSUID) for the contact, and absent otherwise. A contact with a username can be reached under its BSUID, its phone number, or both; they are the same contact. Look one up by BSUID with the `bsuid` query parameter of the list endpoint.","required":["bsuid"],"properties":{"bsuid":{"type":"string","description":"Business-scoped user ID. Can be used as `to` when sending.","example":"CO.13491208655302741918"},"username":{"type":"string","description":"The WhatsApp username, when Meta reported one."}}},"availableChannels":{"type":"array","items":{"type":"string"},"description":"List of available messaging channels for this contact."},"defaultChannel":{"type":"string","enum":["sms","whatsapp","telegram","email","instagram","messenger","voice"],"description":"Preferred channel for this contact."},"verified":{"type":"boolean","description":"Whether this contact has been verified."},"channels":{"type":"array","items":{"$ref":"#/components/schemas/ContactChannel"},"description":"All communication channels for this contact."},"metadata":{"type":"object","additionalProperties":{"type":"string"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"ContactChannel":{"type":"object","description":"A communication channel for a contact.","required":["id","channel","identifier","isPrimary","verified","createdAt"],"properties":{"id":{"type":"string"},"channel":{"type":"string","enum":["sms","whatsapp","email","telegram","instagram","messenger","voice"],"description":"Channel type."},"identifier":{"type":"string","description":"Channel identifier (phone number or email address).","example":"+14155551234"},"countryCode":{"type":"string","description":"ISO country code for phone numbers.","example":"US"},"label":{"type":"string","description":"Optional label for the channel.","example":"work"},"isPrimary":{"type":"boolean","description":"Whether this is the primary channel for its type."},"verified":{"type":"boolean","description":"Whether this channel has been verified."},"metrics":{"type":"object","description":"Delivery metrics for this channel.","properties":{"successCount":{"type":"integer"},"failureCount":{"type":"integer"},"totalAttempts":{"type":"integer"},"avgDeliveryTimeMs":{"type":"number"},"lastSuccessAt":{"type":"string","format":"date-time"}}},"lastInboundAt":{"type":"string","format":"date-time","description":"Last time a message was received on this channel."},"metadata":{"type":"object","additionalProperties":{"type":"string"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"ContactChannelInput":{"type":"object","description":"Input for creating a contact channel.","required":["channel","identifier"],"properties":{"channel":{"type":"string","enum":["sms","whatsapp","email","telegram","instagram","messenger","voice"],"description":"Channel type."},"identifier":{"type":"string","description":"Channel identifier (phone number in E.164 format or email address).","example":"+14155551234"},"countryCode":{"type":"string","minLength":2,"maxLength":2,"description":"ISO country code for phone numbers.","example":"US"},"label":{"type":"string","maxLength":50,"description":"Optional label for the channel.","example":"work"},"isPrimary":{"type":"boolean","description":"Whether this should be the primary channel for its type.","default":false}}},"ContactCreateRequest":{"type":"object","description":"Request body to create a contact with channels.","required":["channels"],"properties":{"displayName":{"type":"string","maxLength":200,"description":"Display name for the contact.","example":"John Doe"},"channels":{"type":"array","items":{"$ref":"#/components/schemas/ContactChannelInput"},"minItems":1,"maxItems":20,"description":"Communication channels for the contact."},"metadata":{"type":"object","additionalProperties":{"type":"string"},"description":"Arbitrary metadata to associate with the contact."}}},"ContactUpdateRequest":{"type":"object","properties":{"displayName":{"type":"string","maxLength":200,"nullable":true,"description":"Human-readable name for this contact. Set to null to clear it and fall back to the contact's identifier. Contacts created automatically from an inbound message have no display name until you set one.","example":"John Doe"},"defaultChannel":{"type":"string","enum":["sms","whatsapp","telegram","email","instagram","messenger","voice"],"nullable":true,"description":"Preferred channel for this contact. Set to null to clear."},"metadata":{"type":"object","additionalProperties":{"type":"string"}}}},"Conversation":{"type":"object","description":"An inbox thread with one contact. A conversation groups every message exchanged with that contact across channels, so a contact who writes on WhatsApp and later by email stays in one thread.","required":["id","contactIdentifier","channels","lastMessage","messageCount","unreadCount","createdAt","updatedAt"],"properties":{"id":{"type":"string","example":"js723987cyghwqxxaxcf590qd18axd95"},"contactId":{"type":"string","description":"ID of the contact this thread belongs to. Absent on group threads and on threads whose contact has not been resolved yet."},"contactIdentifier":{"type":"string","description":"The key this thread is filed under: a phone number in E.164, a WhatsApp business-scoped user ID (BSUID), or a numeric chat ID (Telegram/Instagram/Messenger). It is not always a phone number, so do not parse it as one.","example":"+56912345678"},"email":{"type":"string","description":"Email address of the thread, when the contact was reached by email."},"channels":{"type":"array","items":{"type":"string"},"description":"Every channel this thread has carried messages on.","example":["whatsapp","sms"]},"lastMessage":{"type":"object","description":"Denormalized preview of the most recent message, so a thread list needs no extra fetch.","required":["id","text","channel","direction","at"],"properties":{"id":{"type":"string"},"text":{"type":"string","description":"Text or caption. Empty when the last message carried no text (e.g. media)."},"channel":{"$ref":"#/components/schemas/ChannelType"},"direction":{"type":"string","enum":["inbound","outbound"]},"at":{"type":"string","format":"date-time"}}},"messageCount":{"type":"integer"},"unreadCount":{"type":"integer","description":"Inbound messages not yet marked read. Reset with POST /v2/conversations/{conversationId}/read."},"whatsapp":{"type":"object","description":"WhatsApp identity, present when the contact adopted a username.","properties":{"bsuid":{"type":"string","description":"Business-scoped user ID. Can be used as `to` when sending."},"username":{"type":"string"}}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"from":{"type":"string","nullable":true,"description":"The address this thread was last handled from, on the transport of its last message. Pass the conversation's `id` as `conversationId` on `POST /v2/messages` to reply from it. A thread opened by an API send before /v2 recorded no sender; its `from` is read off its messages. `?from=` on the list matches threads by the recorded sender only, which messages sent through /v2 record."}}},"EmailAttachmentInput":{"type":"object","description":"Email attachment. Provide either `content` (base64) or `path` (URL), not both.","required":["filename"],"properties":{"filename":{"type":"string","description":"Name of the attached file.","maxLength":255,"example":"invoice.pdf"},"content":{"type":"string","description":"Content of the attached file as a Base64-encoded string."},"path":{"type":"string","format":"uri","description":"URL where the attachment file is hosted. The server will fetch the file."},"content_type":{"type":"string","description":"MIME type of the attachment. If not set, will be derived from the filename.","example":"application/pdf"},"content_id":{"type":"string","description":"Content ID for inline images. Reference in HTML as `<img src=\"cid:your_content_id\">`.","maxLength":100,"example":"logo"}}},"Error":{"type":"object","required":["code","message"],"description":"Every v2 error has this shape at the root. There is no `{error: {...}}` wrapper anywhere in v2. A missing resource is `404 not_found` with the message `<Resource> not found`; an address the project does not have is `404 not_found`, `None of this project's channels has the address <address>.`, with the address in `details` under the name the request used (`from` or `address`). A `message` is written for the caller and never carries a stack trace or a source path. A body that is not JSON is `400 invalid_json`, with `details.errors` (`path: []`, the parse error in `message`).","properties":{"code":{"type":"string","example":"invalid_request"},"message":{"type":"string"},"details":{"type":"object","additionalProperties":true,"description":"Machine-readable context. Keys that mean the same thing everywhere:\n- `errors`: present on every `400 invalid_request` for an invalid body or query parameter, `[{path, message, ...}]`, `path` naming the field (`[\"to\"]`, `[\"contacts\", 2, \"recipient\"]`, `[\"limit\"]`).\n- `recommendation`: one sentence saying what to do next.\n- `dashboardUrl`: the dashboard screen that fixes it.\n- `connect`: on `409 no_channel`, the channels this project can connect to reach the recipient, each `{type, request, note, available, dashboardUrl?}`; `request` is the exact call (`POST /v2/channels` with its body, or `POST /v2/numbers`), or null for a channel connected from the dashboard (WhatsApp, Instagram, Messenger). `available: false` says why in `note` (one-way SMS closed until business verification is approved).\n- `address` / `from`: the address the error is about, under the name the request used for it.","properties":{"errors":{"type":"array","items":{"type":"object","required":["path","message"],"properties":{"path":{"type":"array","items":{"oneOf":[{"type":"string"},{"type":"integer"}]}},"message":{"type":"string"},"code":{"type":"string"}},"additionalProperties":true}}}}}},"Message":{"type":"object","required":["id","to","direction","status","channel","messageType","createdAt"],"properties":{"id":{"type":"string","example":"jd7x2k3m4n5p6q7r8s9t0"},"to":{"type":"string","example":"+56912345678"},"from":{"type":"string","example":"+13125551212","description":"Where the message came from: for an outbound message, your address (as `GET /v2/channels` lists it for `channel`, except WhatsApp, where it is the sender's own number field); for an inbound one, the contact."},"channel":{"$ref":"#/components/schemas/ChannelType"},"direction":{"type":"string","description":"Who sent the message. Needed to render a thread: `status` cannot tell the two apart, because an inbound message is also stored as `delivered`.","enum":["inbound","outbound"]},"messageType":{"$ref":"#/components/schemas/MessageType"},"status":{"$ref":"#/components/schemas/MessageStatus"},"text":{"type":"string","description":"Text content or caption."},"content":{"$ref":"#/components/schemas/MessageContent"},"conversationId":{"type":"string","description":"ID of the conversation (inbox thread) this message belongs to. Use it to build a direct dashboard link: `https://dashboard.zavu.dev/{locale}/inbox?conv={conversationId}`. Omitted only on legacy messages created before conversation threading.","example":"js723987cyghwqxxaxcf590qd18axd95"},"providerMessageId":{"type":"string","description":"Message ID from the delivery provider."},"errorCode":{"type":"string","nullable":true,"description":"Why a failed message failed, as a Zavu code: `RISK_REJECTED`, `DAILY_LIMIT_EXCEEDED`, `TEN_DLC_REQUIRED`, `SMS_NOT_DELIVERED`, `WHATSAPP_WINDOW_CLOSED`... A delivery provider's own code is translated where its meaning is known and kept in `errorDetails.providerCode`; one with no known meaning is `SMS_DELIVERY_FAILED`, `WHATSAPP_REJECTED`, `VOICE_DELIVERY_FAILED`, `EMAIL_DELIVERY_FAILED` or `DELIVERY_FAILED`. (/v1 returns the provider's raw code here.)"},"errorMessage":{"type":"string","nullable":true},"cost":{"type":"number","nullable":true,"description":"Zavu's own per-message platform charge, in USD (6 decimals). Messaging is billed against the plan's allowance, so it is often 0."},"costProvider":{"type":"number","nullable":true,"description":"What delivering the message cost Zavu (carrier cost), in USD. Filled in when the delivery report brings it, so null until then."},"costTotal":{"type":"number","nullable":true,"description":"`cost` + `costProvider`. For SMS this is BEFORE Zavu's markup, so it is not what your balance was charged: read `charged` for that. For voice it includes the markup."},"metadata":{"type":"object","additionalProperties":{"type":"string"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"attemptedChannels":{"type":"array","items":{"$ref":"#/components/schemas/ChannelType"},"description":"Transports tried before the current `channel`, oldest first, when an automatic fallback moved the message (WhatsApp to SMS). Empty when no fallback happened. Fallback runs after the send returned, so this can change after `POST /v2/messages` answered."},"riskHold":{"type":"string","nullable":true,"enum":["held","released",null],"description":"`held` while the message waits for risk review (it stays `queued`), `released` once a reviewer let it go, null when it was never held."},"errorDetails":{"type":"object","nullable":true,"description":"Present when `errorCode` translates a delivery provider's code.","properties":{"providerCode":{"type":"string","description":"The provider's own code, unchanged.","example":"3"}}},"charged":{"type":"number","nullable":true,"description":"What this message has taken from your prepaid balance, in USD. SMS and one-way SMS only: charged per segment when the message is accepted (`routing.estimatedCost`), raised if the delivery report brings a higher real cost, and refunded (0) when the message fails AND the carrier charged nothing. A failed SMS the carrier did charge for (it attempted delivery) stays charged. Null on other channels, which are not billed per message here."}}},"MessageContent":{"type":"object","description":"Content for non-text message types (WhatsApp and Telegram).","properties":{"mediaUrl":{"type":"string","description":"URL of the media file (for image, video, audio, document, sticker).","example":"https://example.com/image.jpg"},"mediaId":{"type":"string","description":"WhatsApp media ID if already uploaded."},"mimeType":{"type":"string","description":"MIME type of the media.","example":"image/jpeg"},"filename":{"type":"string","description":"Filename for documents.","example":"invoice.pdf"},"latitude":{"type":"number","description":"Latitude for location messages."},"longitude":{"type":"number","description":"Longitude for location messages."},"locationName":{"type":"string","description":"Name of the location."},"locationAddress":{"type":"string","description":"Address of the location."},"contacts":{"type":"array","description":"Contact cards for contact messages.","items":{"type":"object","properties":{"name":{"type":"string"},"phones":{"type":"array","items":{"type":"string"}}}}},"buttons":{"type":"array","description":"Interactive buttons (max 3).","maxItems":3,"items":{"type":"object","required":["id","title"],"properties":{"id":{"type":"string","maxLength":256},"title":{"type":"string","maxLength":20}}}},"listButton":{"type":"string","description":"Button text for list messages.","maxLength":20},"sections":{"type":"array","description":"Sections for list messages.","items":{"type":"object","required":["title","rows"],"properties":{"title":{"type":"string"},"rows":{"type":"array","maxItems":10,"items":{"type":"object","required":["id","title"],"properties":{"id":{"type":"string"},"title":{"type":"string","maxLength":24},"description":{"type":"string","maxLength":72}}}}}}},"ctaDisplayText":{"type":"string","description":"Button label for cta_url messages.","maxLength":20,"example":"See Dates"},"ctaUrl":{"type":"string","format":"uri","description":"Destination URL opened in the device's default browser when the button is tapped. Used with messageType=cta_url. WhatsApp requires HTTPS in production.","example":"https://example.com/schedule"},"ctaHeaderType":{"type":"string","description":"Optional header type for cta_url messages.","enum":["text","image","video","document"]},"ctaHeaderText":{"type":"string","description":"Header text when ctaHeaderType is 'text'.","maxLength":60},"ctaHeaderMediaUrl":{"type":"string","format":"uri","description":"Public HTTPS URL of the header media when ctaHeaderType is 'image', 'video', or 'document'. WhatsApp fetches this URL — it must be publicly reachable and return the declared content type."},"footerText":{"type":"string","description":"Optional footer text for cta_url messages.","maxLength":60,"example":"Dates subject to change."},"emoji":{"type":"string","description":"Emoji for reaction messages."},"reactToMessageId":{"type":"string","description":"Message ID to react to."},"replyToMessageId":{"type":"string","description":"Zavu message ID of the quoted message this message replies to.\n\n**When sending:** set it to make the message a quoted reply to an earlier message in the same conversation, such as the `data.messageId` of a `message.inbound` webhook. Supported on WhatsApp (every message type except `reaction`) and Telegram. The quoted message must belong to this project (otherwise `404`), must have gone out on the same channel as this send, must belong to the conversation `to` addresses, must have been sent or received by the address this message goes out from (`from`), and must have a `providerMessageId`. A failed message and a Telegram button tap cannot be quoted. Any other case returns `400` instead of sending the message unquoted. The stored message then carries the rest of the `replyTo*` fields.\n\n**When receiving:** present on inbound messages that quote an earlier message. Omitted when the quoted message is not found in Zavu (e.g. an old or unknown message) — use replyToProviderMessageId in that case."},"replyToProviderMessageId":{"type":"string","description":"Provider message ID (WhatsApp WAMID, or Telegram message ID) of the quoted message. Present whenever an inbound WhatsApp message is a reply, even if the quoted message is not stored in Zavu, and on outbound quoted replies. Read-only: to quote when sending, set replyToMessageId."},"replyToFrom":{"type":"string","description":"Sender of the quoted message (phone number in E.164 format, or the Telegram chat ID). Read-only."},"replyToText":{"type":"string","description":"Truncated snippet (up to 200 characters) of the quoted message's text, for display. Empty when the quoted message has no text (e.g. media). Read-only."},"replyToMessageType":{"type":"string","description":"Type of the quoted message (text, image, video, etc.). Read-only."},"templateId":{"type":"string","description":"Template ID for template messages."},"templateVariables":{"type":"object","description":"Variables for body placeholders. Key them to match the template body: by position (`1`, `2`, ...) for positional templates, or by name (e.g. `customer_name`) for named templates. Zavu detects the template's format and sends the correct payload to Meta. Named keys also resolve a named text-header variable. Do not mix positional and named keys in the same request.","additionalProperties":{"type":"string"},"example":{"1":"John","2":"ORD-12345"}},"templateButtonVariables":{"type":"object","description":"Variables for dynamic button placeholders (URL buttons and OTP buttons). Keys are the button index (0, 1, 2) in the template's `buttons` array — not the placeholder name. Values substitute the `{{1}}` placeholder inside that button's URL.\n\n**WhatsApp constraints:**\n- URL buttons only accept `{{1}}` — positional, numeric, no whitespace, no name. Named placeholders like `{{token}}` are stored as literal URL text by Meta and cannot be substituted.\n- At most one placeholder per URL button.\n- A template may have at most three buttons.\n- Static URL buttons (no placeholder) and `quick_reply` buttons are not included here.","additionalProperties":{"type":"string"},"example":{"0":"abc-report-token"}},"templateHeaderVariables":{"type":"object","description":"Value for a text-header variable, keyed by `1` (WhatsApp text headers allow at most one variable). Optional override. If omitted, Zavu resolves the header from `templateVariables` using the header placeholder's name (e.g. `novios`). Static text headers need no value.","additionalProperties":{"type":"string"},"example":{"1":"Jorge y Laura"}},"templateHeaderMediaUrl":{"type":"string","format":"uri","maxLength":2048,"description":"HTTPS URL of the media for this send, for a WhatsApp template whose header is `image`, `video` or `document`. It replaces the file stored on the template for this message only; the template is not changed. WhatsApp fetches the URL when the message is sent, so it must be publicly reachable at that moment (a short-lived signed link works if it outlives the send). Without it, the template's stored header file is sent.\n\nA template whose header is `text`, or that has no header, is refused with `400 invalid_request`: `details.field` is `content.templateHeaderMediaUrl` and `details.errors` repeats the message, before anything is sent or charged. A URL that is not HTTPS is refused with `400 invalid_request` and a `details.errors` entry whose `path` is `[\"content\", \"templateHeaderMediaUrl\"]`. Ignored on channels other than WhatsApp.","example":"https://files.example.com/invoices/INV-1042.pdf?sig=abc123"},"templateHeaderFilename":{"type":"string","minLength":1,"maxLength":240,"description":"File name WhatsApp shows the recipient for a `document` header, e.g. `invoice-1042.pdf`. Works with `templateHeaderMediaUrl`, or on its own to name the template's stored file. Only valid when the template's header is `document`: any other header is refused with `400 invalid_request`, `details.field` set to `content.templateHeaderFilename` and `details.errors` repeating the message, before anything is sent or charged. Ignored on channels other than WhatsApp.","example":"invoice-1042.pdf"},"referral":{"$ref":"#/components/schemas/AdReferral"}}},"MessageStatus":{"type":"string","enum":["queued","sending","sent","delivered","read","failed","received","pending_url_verification"]},"MessageType":{"type":"string","description":"Type of message. Every type except `text` and `template` is sent on WhatsApp only: the send routes them there whatever `channel` says, and in v2 an explicit `channel` other than `whatsapp` is refused with `400` naming `messageType` and `channel`.\n\n`location_request` asks the recipient to share their location and is WhatsApp-only. It takes no `content` object — the prompt goes in `text` (max 1024 characters) and the button label is fixed by WhatsApp. The recipient's answer arrives as an inbound `location` message whose `content.replyToMessageId` is the ID of the request.\n\n`request_contact_info` asks the recipient to share their phone number and is WhatsApp-only. Like `location_request` it takes no `content` object — the prompt goes in `text` (max 1024 characters) and WhatsApp renders a fixed **Share Contact Info** button. The answer arrives as an inbound `contact` message. Use it to recover the phone number of a contact who adopted a WhatsApp username and is only known by their business-scoped user ID (BSUID); when they share it, Zavu automatically links the phone number to that contact.","enum":["text","image","video","audio","document","sticker","location","contact","buttons","list","cta_url","request_contact_info","location_request","reaction","template"]},"OwnedPhoneNumber":{"type":"object","required":["id","phoneNumber","capabilities","status","pricing","createdAt","regulatoryStatus"],"properties":{"id":{"type":"string"},"phoneNumber":{"type":"string","example":"+15551234567"},"name":{"type":"string","description":"Optional custom name for the phone number."},"capabilities":{"type":"array","items":{"type":"string"},"example":["sms","voice"]},"status":{"$ref":"#/components/schemas/PhoneNumberStatus"},"pricing":{"$ref":"#/components/schemas/OwnedPhoneNumberPricing"},"regulatoryStatus":{"type":"string","enum":["approved","pending_review","rejected"],"description":"Regulatory review state. Numbers that need no review are `approved` immediately. A number bought with regulatory information is owned and billed from purchase and starts `pending_review`; it cannot send messages or place calls until this is `approved`, and its `channels` entry can be absent until then. The state is re-checked every 6 hours."},"nextRenewalDate":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"OwnedPhoneNumberPricing":{"type":"object","properties":{"monthlyPrice":{"type":"number","description":"Monthly price in USD."},"upfrontCost":{"type":"number","description":"One-time purchase cost in cents."},"monthlyCost":{"type":"number","description":"Monthly cost in cents."},"isFreeNumber":{"type":"boolean","description":"Whether this is a free number."}}},"PhoneNumberCapabilities":{"type":"object","properties":{"sms":{"type":"boolean"},"voice":{"type":"boolean"},"mms":{"type":"boolean"}}},"PhoneNumberPricing":{"type":"object","properties":{"monthlyPrice":{"type":"number","description":"Monthly price in USD."},"upfrontPrice":{"type":"number","description":"One-time purchase price in USD."},"isFreeEligible":{"type":"boolean","description":"Whether this number qualifies as the plan-included number: a US or Canadian number (a +1 number) costing $20 a month or less. The benefit is one per account: it is never offered again once claimed, not even after the number is released."}}},"PhoneNumberPurchaseRequest":{"type":"object","required":["phoneNumber"],"properties":{"phoneNumber":{"type":"string","description":"Phone number in E.164 format.","example":"+15551234567"},"name":{"type":"string","maxLength":100,"description":"Optional custom name for the phone number."},"type":{"allOf":[{"$ref":"#/components/schemas/PhoneNumberType"}],"description":"Type the number was found under in `GET /v2/numbers/available`. Used to look the number up and to keep regulatory information for reuse. Required when `regulatoryRequirements` is sent. `toll_free` is accepted as an alias of `tollFree`. A number not listed under this type is refused with `400 number_unavailable` saying so."},"regulatoryRequirements":{"type":"array","maxItems":50,"description":"Regulatory information, for numbers whose requirements list is not empty. Get the list with `GET /v1/phone-numbers/requirements?phoneNumber=...` and send one entry per requirement id, except `action` requirements, which take no value. Every required id must be present, once, and no unknown id may be sent; otherwise the purchase is refused with `400 invalid_request` before anything is charged.\n\nThe information is kept for your project under the number's country and `type`. A later purchase there may omit this field if what is kept still covers that number's requirements. Omit it for numbers without requirements.","items":{"type":"object","required":["requirementType","fieldValue"],"properties":{"requirementType":{"type":"string","description":"A `requirementTypes[].id` from `GET /v1/phone-numbers/requirements`. Each id may appear only once.","example":"8c5b1a2e-0f3d-4f5b-9a61-2c7e4d9b1f10","minLength":1,"maxLength":200},"fieldValue":{"type":"string","maxLength":1000,"description":"Depends on the requirement's `type`: the text itself for `textual`; for `address`, the `id` of an address created in this project with `POST /v1/addresses`; for `document`, the `id` of a document created with `POST /v1/documents`. An address or document from another project, or one rejected in review, is refused.","example":"jd7x2k3m4n5p6q7r8s9t0abc","minLength":1}}}}}},"PhoneNumberStatus":{"type":"string","description":"Billing state of an owned number, separate from `regulatoryStatus`. `pending` is legacy and is not written to numbers today.","enum":["active","suspended","pending","releasing","released"]},"PhoneNumberType":{"type":"string","enum":["local","national","tollFree","mobile"],"description":"Type of phone number. `mobile` is stocked in countries where no geographic (`local`) or non-geographic (`national`) inventory exists, and in several markets it is the only type that can receive SMS."},"ProjectWebhookV2":{"type":"object","required":["url","events","active","signatureVersion"],"description":"The one webhook for everything in the project. Stored apart from the /v1 project webhook (`/v1/invitations/webhook`); neither API reads or writes the other's.","properties":{"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEvent"},"description":"Any event in `WebhookEvent`, including `message.*`, `conversation.new` and `call.*`."},"active":{"type":"boolean"},"signatureVersion":{"$ref":"#/components/schemas/WebhookSignatureVersion"},"secret":{"type":"string","description":"Returned only by `PUT /v2/webhook` when it creates the webhook, and by `POST /v2/webhook/secret`."}}},"ProjectWebhookV2Put":{"type":"object","required":["url","events"],"properties":{"url":{"type":"string","format":"uri","description":"HTTPS only."},"events":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/WebhookEvent"},"description":"Any event in `WebhookEvent`. An unknown value is refused with `400` and `details.allowed`."},"active":{"type":"boolean","default":true},"signatureVersion":{"$ref":"#/components/schemas/WebhookSignatureVersion"},"disableSenderWebhooks":{"type":"boolean","default":false,"description":"Switch off the /v1 sender webhooks of this project in the same call, so each event arrives once. They are only switched off (`webhookActive: false` on /v1); URL, events and secret stay, so /v1 can turn one back on. Off by default: the /v2 webhook is added next to them and both receive every event until you decide. `senderWebhooks` on the response lists what is still on."}}},"Routing":{"type":"object","required":["channel","from","reason","estimatedCost"],"description":"What the engine decided at send time. Reports only decisions the engine makes; it does not rank channels by price or by WhatsApp window state.","properties":{"channel":{"$ref":"#/components/schemas/ChannelType"},"from":{"type":"string","description":"The address the message leaves from, as `GET /v2/channels` lists it for `channel`. With a test-mode key, the WhatsApp sandbox number."},"reason":{"type":"string","enum":["explicit","auto_order","conversation","recipient_type","message_type","sandbox"],"description":"Why this transport was chosen. Closed list:\n- `explicit`: `channel` was given.\n- `auto_order`: `channel` omitted or `auto`; the first ACTIVE transport of the sender in a fixed order (for a phone number: WhatsApp, then SMS, then one-way SMS). A connected transport that is switched off is skipped, not chosen and then refused. It does not compare price and does not check the WhatsApp 24-hour window; a closed window fails later as `message.failed`.\n- `conversation`: the sender came from `conversationId` (the sender, not the transport; the transport is then chosen as for `auto_order` unless `channel` is given).\n- `recipient_type`: `to` is an email address, so email.\n- `message_type`: `messageType` is WhatsApp-only (media, interactive, template...), so WhatsApp.\n- `sandbox`: test-mode key, which always uses the WhatsApp sandbox.\nAn automatic fallback is never a send-time reason: it happens later, see `Message.attemptedChannels`."},"estimatedCost":{"type":"number","nullable":true,"description":"USD this call took from the balance when it was accepted (rounded to 6 decimals). An estimate: for SMS the delivery report can raise it to the real cost, and `message.charged` shows where it ended up. Present for `sms` and `sms_oneway`. Null otherwise — WhatsApp is billed by Meta, email in 1,000-message blocks, voice per minute — and also null while a message waits for URL verification, which is charged when it is released. Null means \"not known at send time\", not \"free\"."}}},"SendMessageRequest":{"type":"object","description":"Request body to send a message. No `Zavu-Sender` header: where it leaves from comes from `conversationId`, then `from`, then the project default. `to` is required, except with `conversationId`, where it defaults to whoever the conversation is with.","properties":{"to":{"type":"string","description":"Recipient phone number in E.164 format, email address, WhatsApp business-scoped user ID (BSUID, e.g. `US.13491208655302741918`), or numeric chat ID (for Telegram/Instagram/Messenger). A phone number written with spaces, dashes, dots or parentheses after its `+` (`+56 9 1234 5678`) is read as the E.164 number; bare digits are not (see `400` on this operation). A BSUID is routed to WhatsApp and sent via the `recipient` field; use it to message a contact who adopted a username and whose phone number is hidden.\n\nRequired unless `conversationId` is given. With `conversationId` it may be omitted (it is the conversation's contact: its email for `channel: \"email\"`, else the identifier it is filed under); if given it must be one of that conversation's identifiers (its key, email or BSUID, in any formatting), else `400` naming `to` and `conversationId`.","example":"+56912345678"},"channel":{"$ref":"#/components/schemas/ChannelTypeRequest"},"messageType":{"$ref":"#/components/schemas/MessageType","description":"Type of message. Defaults to 'text'.","default":"text"},"text":{"type":"string","description":"Text body for text messages or caption for media messages.","example":"Your verification code is 123456."},"content":{"$ref":"#/components/schemas/MessageContent","description":"Additional content for non-text message types."},"subject":{"type":"string","description":"Email subject line. Required when channel is 'email' or recipient is an email address.","maxLength":998,"example":"Your order confirmation"},"htmlBody":{"type":"string","description":"HTML body for email messages. If provided, email will be sent as multipart with both text and HTML."},"replyTo":{"type":"string","format":"email","description":"Reply-To email address for email messages.","example":"support@example.com"},"idempotencyKey":{"type":"string","description":"Optional idempotency key to avoid duplicate sends.","example":"msg_01HZY4ZP7VQY2J3BRW7Z6G0QGE"},"metadata":{"type":"object","description":"Arbitrary metadata to associate with the message.","additionalProperties":{"type":"string"}},"fallbackEnabled":{"type":"boolean","description":"Whether to enable automatic fallback to SMS if WhatsApp fails. Defaults to true.","default":true},"voiceLanguage":{"type":"string","description":"Language code for voice text-to-speech (e.g., 'en-US', 'es-ES', 'pt-BR'). If omitted, language is auto-detected from recipient's country code.","example":"es-ES"},"attachments":{"type":"array","description":"Email attachments. Only supported when channel is 'email'. Maximum 40MB total size.","maxItems":50,"items":{"$ref":"#/components/schemas/EmailAttachmentInput"}},"from":{"type":"string","description":"Which of your addresses sends (see `GET /v2/channels`): an E.164 number in any common formatting, an email address, `@botname`, `@username`, a Page id. Resolved inside your project only. Omitted: the project default, the same one /v1 uses when no `Zavu-Sender` header is sent. Ignored when `conversationId` is set, and with a test-mode key, which always sends from the WhatsApp sandbox.\n\nAn address that no entry has returns `404 not_found`. An address two of your senders share (a number whose WhatsApp was connected on one sender and SMS on another) resolves when `channel` names the transport; otherwise it returns `409 ambiguous_from` with `details.candidates[{address, type, v1SenderId}]`, and in that case, and only then, `from` also accepts one of those `v1SenderId` values.","example":"+13125551212"},"conversationId":{"type":"string","description":"Reply inside an existing conversation: the message leaves from the sender that last handled it (`from` on the Conversation), and goes to whoever the conversation is with, so `to` may be omitted. A `to` that is not the conversation's contact is refused with `400` (`details.errors` on `to` and `conversationId`, `details.conversationTo`) instead of silently opening another conversation. Takes precedence over `from`. When `channel` is omitted, `routing.reason` is then `conversation`. A conversation of another project returns `404`; one with no sender recorded returns `409 no_channel`. The 202's `message.conversationId` is set whenever the conversation the message is filed in already exists; it is absent only for a recipient's first message, whose conversation is created after the response. A reply to a conversation that an inbound email opened reaches the right person but is filed in a separate conversation keyed by the email address."}}},"SendMessageResponse":{"type":"object","required":["message","routing"],"properties":{"message":{"$ref":"#/components/schemas/Message"},"routing":{"$ref":"#/components/schemas/Routing"}}},"Template":{"type":"object","required":["id","name","language","body","category"],"properties":{"id":{"type":"string"},"name":{"type":"string","description":"Template name. For WhatsApp, must match the approved template name in Meta.","example":"order_confirmation"},"language":{"type":"string","description":"Language code.","example":"en"},"body":{"type":"string","description":"Default template body with variables: positional ({{1}}, {{2}}) or named ({{customer_name}}, {{contact.first_name}}). Templates created in Zavu are submitted to Meta as positional; templates imported from a WhatsApp Business Account keep their original format (named or positional). Used when no channel-specific body is set.","example":"Hi {{1}}, your order {{2}} has shipped."},"smsBody":{"type":"string","description":"Channel-specific body for SMS messages. Falls back to `body` if not set."},"telegramBody":{"type":"string","description":"Channel-specific body for Telegram messages. Falls back to `body` if not set."},"instagramBody":{"type":"string","description":"Channel-specific body for Instagram messages. Falls back to `body` if not set."},"category":{"allOf":[{"$ref":"#/components/schemas/WhatsAppCategory"}],"description":"The category the template is billed under. For a template linked to WhatsApp this is the category **Meta currently assigns**, not the one it was submitted with: Meta can recategorize a template at approval or at any point afterwards, and that changes what the message costs.\n\nA recategorization reaches this field through the `template.status_changed` webhook, which carries `data.category`, and through `POST /v1/templates/sync`, which reconciles it against Meta. Read one of those rather than assuming the category you sent is the one in force.\n\nFor a template that was never submitted to WhatsApp, this is the category stored locally, defaulting to `UTILITY`."},"status":{"type":"string","enum":["draft","pending","approved","rejected"],"default":"draft"},"variables":{"type":"array","items":{"type":"string"},"description":"List of variable names for documentation."},"headerType":{"type":"string","description":"Type of header (text, image, video, document)."},"headerContent":{"type":"string","description":"Header content (text or media URL)."},"footer":{"type":"string","description":"Footer text for the template."},"buttons":{"type":"array","description":"Template buttons.","items":{"type":"object","properties":{"type":{"type":"string","enum":["quick_reply","url","phone","otp","request_contact_info"]},"text":{"type":"string"},"url":{"type":"string"},"example":{"type":"string","description":"Sample value used to substitute `{{1}}` in the URL when submitting the template to Meta for review. Only present for dynamic URL buttons."},"phoneNumber":{"type":"string"},"otpType":{"type":"string","enum":["COPY_CODE","ONE_TAP"],"description":"OTP button type. Required when type is 'otp'."},"packageName":{"type":"string","description":"Android package name. Required for ONE_TAP buttons."},"signatureHash":{"type":"string","description":"Android app signature hash. Required for ONE_TAP buttons."}}}},"addSecurityRecommendation":{"type":"boolean","description":"Add 'Do not share this code' disclaimer. Only for AUTHENTICATION templates."},"codeExpirationMinutes":{"type":"integer","minimum":1,"maximum":90,"description":"Code expiration time in minutes. Only for AUTHENTICATION templates."},"whatsapp":{"type":"object","description":"WhatsApp-specific template information.","properties":{"templateName":{"type":"string","description":"WhatsApp template name."},"namespace":{"type":"string","description":"WhatsApp Business Account namespace."},"status":{"type":"string","description":"WhatsApp approval status."}}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"TemplateCreateRequest":{"type":"object","required":["name","language","body"],"properties":{"name":{"type":"string"},"language":{"type":"string","default":"en"},"body":{"type":"string","description":"Default template body. Used when no channel-specific body is set."},"smsBody":{"type":"string","description":"Channel-specific body for SMS. Falls back to `body` if not set."},"telegramBody":{"type":"string","description":"Channel-specific body for Telegram. Falls back to `body` if not set."},"instagramBody":{"type":"string","description":"Channel-specific body for Instagram. Falls back to `body` if not set."},"whatsappCategory":{"$ref":"#/components/schemas/WhatsAppCategory"},"headerType":{"type":"string","description":"Type of header for the template.","enum":["text","image","video","document"]},"headerContent":{"type":"string","description":"Header content (text string or media URL)."},"footer":{"type":"string","description":"Footer text for the template.","maxLength":60},"variables":{"type":"array","items":{"type":"string"}},"buttons":{"type":"array","description":"Template buttons (max 3).","maxItems":3,"items":{"type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["quick_reply","url","phone","otp","request_contact_info"],"description":"`request_contact_info` renders a fixed **Share Contact Info** button that asks the recipient to share their phone number — useful when a contact adopted a WhatsApp username and you only know their BSUID. It takes no other fields."},"text":{"type":"string","maxLength":25,"description":"Button label. Required for every type except `request_contact_info`, whose label is fixed by WhatsApp."},"url":{"type":"string","format":"uri","description":"Button destination. Use `{{1}}` exactly once for a dynamic URL (e.g. `https://example.com/orders/{{1}}`); WhatsApp only accepts the strict `{{1}}` form. Static URLs must not contain any `{{...}}` placeholder."},"example":{"type":"string","description":"Sample value Meta uses to review templates with a dynamic URL button. Substituted into `{{1}}` of the URL when the template is submitted to Meta. Only meaningful when `url` contains `{{1}}`; ignored for static URLs.","example":"ORD-12345"},"phoneNumber":{"type":"string"},"otpType":{"type":"string","enum":["COPY_CODE","ONE_TAP"],"description":"Required when type is 'otp'. COPY_CODE shows copy button, ONE_TAP enables Android autofill."},"packageName":{"type":"string","description":"Android package name. Required for ONE_TAP buttons."},"signatureHash":{"type":"string","description":"Android app signature hash. Required for ONE_TAP buttons."}}}},"addSecurityRecommendation":{"type":"boolean","description":"Add 'Do not share this code' disclaimer. Only for AUTHENTICATION templates."},"codeExpirationMinutes":{"type":"integer","minimum":1,"maximum":90,"description":"Code expiration time in minutes. Only for AUTHENTICATION templates."}}},"TemplateSubmitRequest":{"type":"object","required":["from"],"properties":{"from":{"type":"string","description":"The WhatsApp number whose Business Account receives the template: an address with a `whatsapp` entry in `GET /v2/channels`."},"category":{"$ref":"#/components/schemas/WhatsAppCategory"}}},"WebhookEvent":{"type":"string","description":"Type of event that triggers the webhook.\n\n**Message lifecycle events:**\n- `message.queued`: Message created and queued for sending. `data.status` = `queued`\n- `message.sent`: Message accepted by the provider. `data.status` = `sent`\n- `message.delivered`: Message delivered to recipient. `data.status` = `delivered`\n- `message.read`: Message was read by the recipient (WhatsApp only). `data.status` = `read`\n- `message.failed`: Message failed to send. `data.status` = `failed`\n\n**Inbound events:**\n- `message.inbound`: New message received from a contact. `data.conversationId`, the inbox thread id (deep-link with `https://dashboard.zavu.dev/{locale}/inbox?conv={conversationId}`), is always set: the webhook is sent once the message has been filed in its thread, so the first message of a new thread carries the same id as `conversation.new`. `data.contactId` is the Zavu contact, and unlike `data.from` it does not change when a WhatsApp contact's identifier switches from a business-scoped user ID (BSUID) to a phone number, unless the switch reveals two contacts for one person; they are then merged and `contact.identity_linked` names the one that went away in `mergedContactId`. On WhatsApp, `data.whatsapp` carries the contact's `bsuid` and `username` when known. Reactions carry none of these three. Reactions are delivered as `message.inbound` with `messageType='reaction'`. When the contact replied to (quoted) an earlier message, `data.content` carries the reply context: `replyToMessageId`, `replyToProviderMessageId`, `replyToFrom`, `replyToText`, and `replyToMessageType`. `data.providerTimestamp` is the provider's original receive time in Unix milliseconds (the moment the channel received the message from the contact — WhatsApp, Telegram, Instagram, Messenger; `null` for SMS and email). Compare it against the top-level `timestamp` (when Zavu dispatched the webhook) to detect and ignore delayed deliveries. When the conversation was opened from a Click-to-WhatsApp ad or post, `data.referral` carries the ad attribution — including `ctwaClid`, the identifier Meta's Conversions API needs to credit a conversion back to that ad. WhatsApp only, and only on the first message of the thread: it is absent from every later message, so persist it when it arrives.\n- `message.unsupported`: Received a message type that is not supported\n\n**Broadcast events:**\n- `broadcast.status_changed`: Broadcast status changed (pending_review, approved, rejected, sending, completed, cancelled)\n\n**Other events:**\n- `conversation.new`: New conversation started with a contact. `data` carries `conversationId` (the inbox thread id — deep-link with `https://dashboard.zavu.dev/{locale}/inbox?conv={conversationId}`), the `phoneNumber` or `email` key, `channel`, `firstMessageId`, `firstMessageText`, and `profileName`.\n- `contact.identity_linked`: A WhatsApp contact known by BSUID was tied to a phone number, a contact known by phone received a BSUID, or a contact changed their number. `data` carries `contactId`, `bsuid`, `phoneNumber` and `reason`: `provider_reported` (WhatsApp sent the phone and the BSUID together on an inbound message), `contact_shared` (the contact shared their phone number in reply to a `request_contact_info` message), `phone_changed` (the contact changed their number; `previousPhoneNumber` carries the old one, and `bsuid` is `null` when WhatsApp never reported one for the contact), or `bsuid_changed` (WhatsApp reported a new BSUID for the same number). `contactId` is the contact that remains if two were merged, and `mergedContactId` the one folded into it. `previousBsuid` is a BSUID you may have stored that no longer identifies the contact. Sent once per link, not on every message. `data.from` on `message.inbound` is the phone only when WhatsApp includes it, so it can still be the BSUID afterwards; key on `contactId`.\n- `template.status_changed`: WhatsApp template approval status changed. `data` carries `templateId`, `name`, `previousStatus`, `currentStatus`, `rejectionReason`, and `category` — the category Meta currently bills the template under. Meta can recategorize a template (typically `UTILITY` to `MARKETING`) at approval or long afterwards, which changes what each message costs; `category` is how that reaches you. A recategorization with no status change is delivered as this same event, so compare `category` against what you hold rather than only reacting to `currentStatus`.\n\n**Partner events:**\n- `invitation.status_changed`: A partner invitation's stored status changed: to `in_progress`, `completed`, `failed`, `cancelled`, or back to `pending` when it is resent from the dashboard. A change to the same status sends nothing, and expiry is not a stored change, so no event is sent when an invitation expires. Delivered to the project that created the invitation; a parent project does not receive its sub-accounts' events. `data` carries `invitationId`, `clientName`, `clientEmail`, `connectionType` (`whatsapp_waba` or `messenger`), `previousStatus`, and `currentStatus`. On `completed` it also carries `connectedAccount` (`channel`, `id`, `name`), the WhatsApp number or Facebook Page that was linked, and, for WhatsApp, `wabaAccountId`. On `failed` it carries `failureReason`; the invitation link stays usable, so a client can retry it.\n\n**Voice Agent events:**\nFor every voice event, `data` carries `callId`, `direction`, `from`, `to`, `status`, `durationSeconds`, `endReason`, and `transcriptAvailable`. The terminal events (`call.completed`, `call.failed`) additionally carry `cost` — what the call was billed, in USD, combining telephony and the managed voice pipeline — and `currency`. They are dispatched after the call is charged, so `cost` is populated rather than zero. It can still grow afterwards: telephony can be settling on an outbound call, and on an agent that runs a premium model the model's tokens are charged between 30 minutes and 24 hours after the call ends. `GET /v1/calls/{callId}` holds the reconciled figure.\n- `call.initiated`: An outbound call was created and is dialing, or an inbound call was received. `data.status` = `ringing`\n- `call.answered`: The call was answered and the voice agent is connected. `data.status` = `in_progress`\n- `call.completed`: The call ended after a conversation. `data.status` = `completed`; `durationSeconds` and `endReason` describe how it ended, and `transcriptAvailable` indicates whether a transcript can be fetched.\n- `call.failed`: The call could not be completed (busy, no answer, canceled, or an error). `data.status` is the terminal status and `endReason` explains the cause.\n\n**Custom domain events:**\n- `domain.verified`: A custom email domain passed verification (DKIM, and SPF/DMARC/MAIL FROM if enhanced records are enabled)\n- `domain.failed`: A custom email domain failed verification or is partially verified","enum":["message.queued","message.sent","message.delivered","message.read","message.failed","message.inbound","message.unsupported","broadcast.status_changed","conversation.new","contact.identity_linked","template.status_changed","invitation.status_changed","call.initiated","call.answered","call.completed","call.failed","domain.verified","domain.failed"]},"WebhookMessageData":{"type":"object","description":"Data payload for message webhook events (queued, sent, delivered, failed, inbound).","required":["messageId","to","channel","status"],"properties":{"messageId":{"type":"string","description":"ID of the message.","example":"jx7744x3wckc9cwy0j9c8g6v0h7z7shs"},"to":{"type":"string","description":"Recipient phone number or email. On `message.inbound` this is your own number (the message's destination).","example":"+56912345678"},"from":{"type":"string","description":"Sender of the message. Present on `message.inbound`: the contact who sent the message.","example":"+14155551234"},"channel":{"$ref":"#/components/schemas/ChannelType"},"status":{"$ref":"#/components/schemas/MessageStatus"},"messageType":{"$ref":"#/components/schemas/MessageType","description":"Type of the message. Present on `message.inbound`."},"conversationId":{"type":"string","nullable":true,"description":"ID of the conversation (inbox thread) this message belongs to. Sent on `message.inbound` on every channel, except for reactions. Use it to deep-link to the dashboard inbox: `https://dashboard.zavu.dev/{locale}/inbox?conv={conversationId}`. The webhook is sent once the message has been filed in its thread, so this is always the thread the message is in: on the first message of a new thread it is the id `conversation.new` carries, and on the WhatsApp message that ties a BSUID to a phone number it is the thread that remains once the two are joined.","example":"js723987cyghwqxxaxcf590qd18axd95"},"contactId":{"type":"string","nullable":true,"description":"ID of the Zavu contact that sent the message. Sent on `message.inbound` on every channel, except for reactions. It does not change when a WhatsApp contact's `from` switches from a business-scoped user ID (BSUID) to a phone number, so use it, not `from`, as your key for the person. `null` only when no contact could be resolved.","example":"jd7x2k3m4n5p6q7r8s9t0abc"},"whatsapp":{"type":"object","description":"WhatsApp identity of the sender. Present on WhatsApp `message.inbound` when Zavu knows the sender's business-scoped user ID (BSUID), and absent otherwise. Same shape as `whatsapp` on a contact or a conversation.","required":["bsuid"],"properties":{"bsuid":{"type":"string","description":"Business-scoped user ID. Stable for the person on your WhatsApp Business Account, whether or not `from` is a phone number.","example":"CO.13491208655302741918"},"username":{"type":"string","description":"The WhatsApp username, when the sender has one and Meta reported it."}}},"text":{"type":"string","description":"Text body or media caption. Present on `message.inbound` when the message has text."},"errorCode":{"type":"string","nullable":true,"description":"Error code if the message failed."},"errorMessage":{"type":"string","nullable":true,"description":"Error message if the message failed."},"providerTimestamp":{"type":"integer","format":"int64","nullable":true,"description":"The provider's original receive time in Unix milliseconds (when the channel received the message from the contact). Present on `message.inbound` for channels that report it (WhatsApp, Telegram, Instagram, Messenger); null for SMS and email."},"referral":{"allOf":[{"$ref":"#/components/schemas/AdReferral"}],"description":"Click-to-WhatsApp ad attribution. Present on `message.inbound` only when this is the first message of a conversation opened from a Meta ad or post — absent on every later message in that same thread, and on organic conversations. Read `referral.ctwaClid` here to attribute the conversation, and persist it on your side: this webhook is the only delivery. `GET /v2/messages/{messageId}` also returns it, under `content.referral`."}}},"WebhookPayloadV2":{"type":"object","description":"What a `POST` to your /v2 webhook carries. Signed like every Zavu webhook (`X-Zavu-Signature`, with the webhook's `signatureVersion`); the event name is also in the `X-Zavu-Event` header. No sender id appears anywhere in it.","required":["id","event","timestamp","projectId","from","to","type","data"],"properties":{"id":{"type":"string","description":"Unique event identifier. Different from the id of the same event delivered to a /v1 webhook.","example":"evt_1736850000000_abc123"},"event":{"$ref":"#/components/schemas/WebhookEvent"},"timestamp":{"type":"integer","description":"Unix milliseconds when the event was created.","example":1736850000000},"projectId":{"type":"string"},"from":{"type":"string","nullable":true,"description":"Where the event's message or call came from. Outbound message events: your address. `message.inbound`, `message.unsupported`, `conversation.new`: the contact. `call.*`: the caller. Project-level events (`invitation.*`, `domain.*`, `broadcast.*`) are not about one address: read `data`. On those, `from`, `to` and `type` can be null, or name an address of the channel group the event was recorded on."},"to":{"type":"string","nullable":true,"description":"The other end: the recipient of an outbound message, your address for an inbound one, the callee of a call."},"type":{"allOf":[{"$ref":"#/components/schemas/ChannelType"}],"nullable":true,"description":"The transport, as in `GET /v2/channels`. Null when the event names none."},"data":{"type":"object","description":"The same event data /v1 sends (see `WebhookEvent`), without `senderId`."}}},"WebhookSignatureVersion":{"type":"string","description":"Which `X-Zavu-Signature` scheme this receiver is sent. The header is `t=<unix seconds>` followed by the signatures.\n\n- `v1`: `v1=HMAC_SHA256(secret, body)`.\n- `v2`: `v2=HMAC_SHA256(secret, \"{t}.{body}\")`. The current scheme, and the default for a new /v2 webhook. It signs the timestamp together with the body.\n- `v1+v2`: both signatures, sharing one `t`. The migration setting: a receiver reading either one works.\n\nMoving straight between `v1` and `v2`, in either direction, returns `400`. Set `v1+v2` first. See https://www.zavu.dev/docs/guides/receiving-messages/signature-migration","enum":["v1","v1+v2","v2"],"example":"v2"},"WhatsAppCategory":{"type":"string","description":"WhatsApp template category.","enum":["UTILITY","MARKETING","AUTHENTICATION"]},"ChannelEntry":{"type":"object","required":["address","type","active","v1SenderId"],"description":"One place the project can send from, on one transport.","properties":{"address":{"type":"string","description":"The address the recipient sees: an E.164 number (`sms`, `voice`, `whatsapp`), an email address (lowercase), `@botname` (`telegram`), `@username` (`instagram`), the Facebook Page id (`messenger`), or the literal `sms_oneway` (one-way SMS goes out from shared system numbers, and messages on it store this same literal as `from`).","example":"+13125551212"},"type":{"$ref":"#/components/schemas/ChannelType"},"active":{"type":"boolean","description":"It can send right now. A connected transport that is switched off is listed with `active: false`; turn it on with `POST /v2/channels/{address}/{type}/activate`. `sms`, `sms_oneway` and `voice` have no switch and are always active."},"v1SenderId":{"type":"string","description":"Read-only. The /v1 sender behind this entry, usable as the `Zavu-Sender` header on /v1. Entries with the same `v1SenderId` are one channel group. On /v2 it is accepted as `from` only when two entries of different groups share an address (see `409 ambiguous_from`)."}}},"SenderWebhookV1":{"type":"object","required":["address","url","v1SenderId"],"description":"A /v1 sender webhook that is still on. Each one also receives the events it is subscribed to, as a separate delivery.","properties":{"address":{"type":"string","nullable":true,"description":"The sender's first address; null for a sender with no connected transport."},"url":{"type":"string","format":"uri"},"v1SenderId":{"type":"string","description":"Read-only; the sender to change on /v1."}}},"ChannelConnectRequest":{"description":"What to connect. Only channels that need no purchase and no third-party signup are connected here: one-way SMS, an email address at a verified domain, a Telegram bot. Two-way SMS and voice come with a number (`POST /v2/numbers`); WhatsApp, Instagram and Messenger connect through Meta, from Accounts in the dashboard. Any of those types here is a `400` saying so.","oneOf":[{"type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["sms_oneway"]}},"description":"One-way SMS from shared system numbers. No number or credential; recipients cannot reply. Sends within the SMS daily ceiling. Sending to a US or Canadian (+1) number needs an approved 10DLC campaign first, and is refused with `403 ten_dlc_required` until then. Refused with `403 kyb_required` only on an account that must verify its business before using one-way SMS."},{"type":"object","required":["type","address"],"properties":{"type":{"type":"string","enum":["email"]},"address":{"type":"string","format":"email","example":"noreply@yourdomain.com"},"fromName":{"type":"string","maxLength":100,"description":"Display name in the recipient's inbox."}},"description":"An address at an email domain of this project that is verified (`GET /v2/email-domains`)."},{"type":"object","required":["type","botToken"],"properties":{"type":{"type":"string","enum":["telegram"]},"botToken":{"type":"string","description":"Bot token from @BotFather."}},"description":"A Telegram bot. Its address is `@botname`."}],"discriminator":{"propertyName":"type"}},"EmailDomain":{"type":"object","required":["id","domain","status","dkimStatus"],"properties":{"id":{"type":"string","example":"emd_abc123"},"domain":{"type":"string","example":"example.com"},"status":{"type":"string","description":"Overall verification status.","example":"pending"},"dkimStatus":{"type":"string","example":"not_started"},"dnsRecords":{"type":"array","description":"DNS records to publish. Present when fetching a single domain or after adding one.","items":{"$ref":"#/components/schemas/EmailDomainDnsRecord"}},"verifiable":{"type":"boolean","description":"Present with `dnsRecords`. False when the DKIM records could not be generated: such a domain cannot verify whatever DNS you publish, and `setupError` says what to do."},"setupError":{"type":"object","description":"Present when `verifiable` is false. `recommendation` is the same everywhere this domain is refused (`POST .../verify`, `POST /v2/channels`): remove the domain and add it again, which sets it up from scratch; contact support if the new one has no `dkim` record either. It never says to retry verify or to publish the other records.","properties":{"code":{"type":"string","enum":["dkim_records_unavailable"]},"message":{"type":"string"},"recommendation":{"type":"string"}}}}},"EmailDomainDnsRecord":{"type":"object","required":["type","name","value","purpose","required"],"properties":{"type":{"type":"string","description":"DNS record type.","example":"CNAME"},"name":{"type":"string","description":"Record host/name to create."},"value":{"type":"string","description":"Record value."},"priority":{"type":"integer","description":"Priority (MX records only)."},"purpose":{"type":"string","description":"What the record is for.","enum":["dkim","spf","dmarc","mail_from"]},"required":{"type":"boolean","description":"Whether the record is required to verify + send (DKIM) or recommended for deliverability."}}},"Function":{"type":"object","description":"A Zavu Function: TypeScript that runs in Zavu Cloud, either reacting to events, serving HTTP, or holding the agents its code declares.","required":["architecture","createdAt","enabled","httpEnabled","id","memoryMb","name","paused","runtime","slug","status","timeoutSec","updatedAt"],"properties":{"id":{"type":"string"},"slug":{"type":"string","example":"support"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"runtime":{"type":"string","example":"nodejs24"},"status":{"type":"string","enum":["draft","deploying","active","failed","disabled"]},"enabled":{"type":"boolean","description":"True only while a version is live and not paused (`status: active`). A draft, a failed first deploy, a deploy in progress or a pause are all `false`."},"timeoutSec":{"type":"integer"},"memoryMb":{"type":"integer","enum":[128,256,512,1024]},"architecture":{"type":"string","enum":["arm64","x86_64"],"description":"arm64 is the default and the cheaper one; x86_64 bills each invocation at 1.25x units. Applied on the next deploy."},"httpEnabled":{"type":"boolean"},"publicUrl":{"type":"string","format":"uri","nullable":true,"description":"Null while `httpEnabled` is false: the URL does not serve then."},"activeDeploymentId":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"paused":{"type":"boolean","description":"The customer paused it. A deploy, a git push or a rollback does not resume it. Pausing stops event triggers; the HTTP endpoint has its own switch, `httpEnabled`, and the tools of the agents it declares keep answering."}}},"FunctionUpdateRequest":{"type":"object","description":"Configuration only, applied in one transaction: if any part is refused, nothing changes. Code, dependencies and deploys are on /v1 (`PATCH /v1/functions/{functionId}` and `POST /v1/functions/{functionId}/deploy`). Send at least one field. Every invalid field is reported in `details.errors`.","properties":{"name":{"type":"string","minLength":1,"maxLength":80},"description":{"type":"string","nullable":true,"maxLength":280,"description":"null clears it."},"timeoutSec":{"type":"integer","minimum":1,"maximum":180,"description":"Applied on the next deploy."},"memoryMb":{"type":"integer","enum":[128,256,512,1024],"description":"Applied on the next deploy."},"architecture":{"type":"string","enum":["arm64","x86_64"]},"enabled":{"type":"boolean","description":"false pauses the function, true resumes it. Refused with 409 `function_not_ready` while it is a draft, deploying, or its first deploy failed."},"httpEnabled":{"type":"boolean","description":"Switch the public endpoint on or off. Applies to the deployed function without a redeploy."}}}}},"paths":{"/v2/messages":{"post":{"operationId":"sendMessageV2","tags":["Messages"],"summary":"Send a message","description":"Send one message. The body is the /v1 message body plus `from` and `conversationId`. There is no `Zavu-Sender` header.\n\n**Where it leaves from:** `conversationId` if given, else `from` (an address), else the project default. See `from` for ambiguous addresses.\n\n**Which transport:** decided by the first branch that applies, and reported in `routing.reason`: a test-mode key uses the WhatsApp sandbox; an email-address `to` uses email; a WhatsApp-only `messageType` uses WhatsApp; an explicit `channel` is used as given; otherwise the fixed order over the sender's ACTIVE transports (for a phone number: WhatsApp, then SMS, then one-way SMS). The order ignores price and the 24-hour window. A `channel` that contradicts the recipient (an email `to` on anything but `email`, or `channel: \"email\"` with a phone number or chat id) or a WhatsApp-only `messageType` is refused with `400`; /v1 overrides the first and last silently.\n\n**When nothing can send** the answer is always `409 no_channel`, whatever the reason: no channel at all, an explicit `channel` that is not connected, an email `to` without an email channel, a WhatsApp-only `messageType` without WhatsApp. `details.connect` lists what this project can connect to reach `to`, with the exact call for each (`POST /v2/channels`, `POST /v2/numbers`), and the message never names an address that carries nothing.\n\n**Fallback** from WhatsApp to SMS, when enabled, happens after this call returned, once the WhatsApp send fails. It is not visible in `routing`; read `attemptedChannels` on the message or wait for the webhooks. Before falling back to SMS, a message sent through /v2 is checked for an approved 10DLC campaign (+1 numbers) and an active connection; when either is missing it stays `failed` with `errorCode` `TEN_DLC_REQUIRED` or `CHANNEL_INACTIVE`.\n\nLimits, risk review, pricing and verification are exactly /v1's: see `POST /v1/messages`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendMessageRequest"},"example":{"to":"+56912345678","text":"Your code is 123456"}}}},"responses":{"202":{"description":"Accepted. `message.status` is `queued`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendMessageResponse"}}}},"400":{"description":"Invalid request, always with `details.errors[{path, message}]` naming the field. Checked in this order: a `channel` that contradicts the recipient (an email `to` on anything but `email`; `channel: \"email\"` with a `to` that is not an email address, `details.errors` on `channel` and `to`) comes before any missing field; then the body (`subject` for email, `text` or `content` by `messageType`, a bad `to`); then a `channel` that contradicts a WhatsApp-only `messageType`. A `to` of 8-15 bare digits is a chat id: when nothing in the request can deliver to a chat id (the channel is a phone channel, or `auto` from an address with no Telegram/Instagram/Messenger) it is refused with `details.suggestion` holding it with a `+`. With `conversationId`, a `to` that is not the conversation's contact (`details.errors` on `to` and `conversationId`). A WhatsApp-only `messageType` on another explicit `channel` names both `messageType` and `channel`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"`insufficient_balance`: the balance cannot cover the SMS or email.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Refused before sending. `ten_dlc_required` (+1 SMS or one-way SMS without an approved 10DLC campaign in this project: `details.connect` lists the registration as calls, in order, on /v1's 10DLC endpoints — for `sms`: brand, submit brand, campaign, submit campaign, assign the number you send from; for `sms_oneway`: the same four, then buy a US number (`POST /v2/numbers`) and assign it, because one-way SMS leaves from shared numbers that cannot be assigned to your campaign. The first four are what lift the refusal for one-way SMS; the messages still leave from numbers outside the campaign, which `details.oneway` and `message` say plainly; `details.alternatives` lists WhatsApp, Telegram and email, which need no 10DLC, each with `connected` for the sending address and how to connect it; `details.dashboardUrl` is `/10dlc/campaigns`), `sending_suspended`, `kyb_required` (one-way SMS without an approved business verification, including `channel: \"sms_oneway\"` when it is not connected; `details.dashboardUrl` is `/kyb`), `destination_not_verified`, `channel_inactive` (an explicitly chosen transport is connected but switched off; `details.channel`), `recipient_opted_out`, `url_not_verified`, `url_shortener_blocked`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`from` names no address of this project, `conversationId` is not a conversation of this project, `content.templateId` not found, or `content.replyToMessageId` is not a message of this project.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`no_channel`: nothing this call could use can reach `to`. The project has no channel and no `from` was given; the conversation has no sender; the sender has no ACTIVE transport that can reach `to`; an explicit `channel` is not connected (`details.channel`); an email `to` has no email channel (a phone `to` with `channel: \"email\"` is a `400`, not this); a WhatsApp-only `messageType` has no WhatsApp. `details.recommendation` says what to do, `details.connect` lists what can be connected with the call for each (never a type the address already has; for an explicit `channel`, that channel; `details.connected` lists what it has, and `sms` vs `sms_oneway` is explained when one was asked for and the other is there), and `details.inactive` lists a transport that is connected but off (activate it instead). `ambiguous_from`: two of your senders share the `from` address; `details.candidates[{address, type, v1SenderId}]`. `conflict`: a repeated `idempotencyKey`. Nothing is sent again; the message the key first created is in `details.message`, so a retry after a timeout can read the outcome from the 409 without a second call.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"no_channel":{"value":{"code":"no_channel","message":"This project has no channel that can send to +15550100123 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"}],"dashboardUrl":"/accounts"}}},"ambiguous_from":{"value":{"code":"ambiguous_from","message":"More than one channel in this project has the address +13125551212. Pass the `v1SenderId` of the one to use as `from`.","details":{"candidates":[{"address":"+13125551212","type":"whatsapp","v1SenderId":"jx7a..."},{"address":"+13125551212","type":"sms","v1SenderId":"jx7b..."}]}}}}}}},"422":{"description":"`sms_compliance_blocked`: an SMS was refused by the content check, or the check could not run (then the message says so, nothing was charged, and a retry in a few minutes goes through). `details` carries `riskScore`, `messageType`, `complianceFailures`, `requiredFixes` and, when there is one, `suggestedCompliantVersion`. One-time codes (OTP) always pass.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`daily_limit_exceeded` (`details.limit`), `a2p_limit_exceeded` (Free-plan monthly allowance on WhatsApp/Telegram/Instagram/Messenger; a Free team that added funds keeps sending at $4 per 1,000 and gets it only when the balance cannot cover the next block; while incoming messages are on hold, `details.heldInbound` carries `count`, `max` and `nextExpiresAt`), `rate_limit_exceeded` (one SMS per recipient per minute), `email_limit_exceeded`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]},"get":{"operationId":"listMessagesV2","tags":["Messages"],"summary":"List messages","description":"List messages of this project, newest first.","parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["queued","sending","sent","delivered","failed","received"]}},{"name":"to","in":"query","schema":{"type":"string"}},{"name":"channel","in":"query","schema":{"$ref":"#/components/schemas/ChannelType"}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":100}},{"name":"cursor","in":"query","description":"Opaque `nextCursor` from the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"Messages, newest first.","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Message"}},"nextCursor":{"type":"string","nullable":true}}}}}},"400":{"description":"Invalid query parameter (`details.errors`), e.g. an unknown `status` or `channel`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/messages/{messageId}":{"get":{"operationId":"getMessageV2","tags":["Messages"],"summary":"Get a message","description":"A message, with `from` and `to` naming addresses, plus `attemptedChannels` and `riskHold`.","parameters":[{"name":"messageId","in":"path","required":true,"description":"","schema":{"type":"string"}}],"responses":{"200":{"description":"The message.","content":{"application/json":{"schema":{"type":"object","required":["message"],"properties":{"message":{"$ref":"#/components/schemas/Message"}}}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Message not found, or it belongs to another project.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/conversations":{"get":{"operationId":"listConversationsV2","tags":["Conversations"],"summary":"List conversations","description":"List inbox threads, most recently active first. Filter by `from` (one of your addresses) and `channel`. Each conversation carries `from`, the address it was last handled from.","parameters":[{"name":"channel","in":"query","description":"Only threads that have carried this transport.","schema":{"$ref":"#/components/schemas/ChannelType"}},{"name":"from","in":"query","description":"Keep only threads last handled from this address. Resolved like `from` on `POST /v2/messages`, narrowed by `channel` when given.","schema":{"type":"string"}},{"name":"search","in":"query","description":"Identity search (number, email, username). Never message bodies.","schema":{"type":"string","maxLength":120}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":100}},{"name":"cursor","in":"query","description":"Opaque `nextCursor` from the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"Threads, most recently active first (by relevance when `search` is set).","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Conversation"}},"nextCursor":{"type":"string","nullable":true}}}}}},"400":{"description":"Invalid query.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`from` names no address of this project.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/conversations/{conversationId}":{"get":{"operationId":"getConversationV2","tags":["Conversations"],"summary":"Get a conversation","parameters":[{"name":"conversationId","in":"path","required":true,"description":"","schema":{"type":"string"}}],"responses":{"200":{"description":"The conversation.","content":{"application/json":{"schema":{"type":"object","required":["conversation"],"properties":{"conversation":{"$ref":"#/components/schemas/Conversation"}}}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Conversation not found, or it belongs to another project.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/conversations/{conversationId}/messages":{"get":{"operationId":"listConversationMessagesV2","tags":["Conversations"],"summary":"List a conversation's messages","description":"Reply with `POST /v2/messages` and this `conversationId`.","parameters":[{"name":"conversationId","in":"path","required":true,"description":"","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":100}},{"name":"cursor","in":"query","description":"Opaque `nextCursor` from the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"Messages, newest first.","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Message"}},"nextCursor":{"type":"string","nullable":true}}}}}},"400":{"description":"Invalid query (`limit` outside 1–100, or a malformed `cursor`), with `details.errors`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Conversation not found, or it belongs to another project.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/conversations/{conversationId}/read":{"post":{"operationId":"markConversationReadV2","tags":["Conversations"],"summary":"Mark a conversation read","description":"Resets `unreadCount` in your inbox. Sends no read receipt to the contact.","parameters":[{"name":"conversationId","in":"path","required":true,"description":"","schema":{"type":"string"}}],"responses":{"200":{"description":"Updated conversation.","content":{"application/json":{"schema":{"type":"object","required":["conversation"],"properties":{"conversation":{"$ref":"#/components/schemas/Conversation"}}}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Conversation not found, or it belongs to another project.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/contacts":{"get":{"operationId":"listContactsV2","tags":["Contacts"],"summary":"List contacts","description":"List contacts with their channels. A repeated `tag` means all of them.","parameters":[{"name":"search","in":"query","schema":{"type":"string","maxLength":200}},{"name":"bsuid","in":"query","description":"Exact WhatsApp business-scoped user ID (e.g. `CO.13491208655302741918`). Returns the contact that holds it, or an empty list. Combines with `phoneNumber` and `tag` as a filter, and cannot be combined with `search` (400). A value that is not shaped like a BSUID returns 400.","schema":{"type":"string"},"example":"CO.13491208655302741918"},{"name":"tag","in":"query","style":"form","explode":true,"schema":{"type":"array","items":{"type":"string"}}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":100}},{"name":"cursor","in":"query","description":"Opaque `nextCursor` from the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"Contacts.","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Contact"}},"nextCursor":{"type":"string","nullable":true}}}}}},"400":{"description":"Unknown tag.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]},"post":{"operationId":"createContactV2","tags":["Contacts"],"summary":"Create a contact","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactCreateRequest"}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contact"}}}},"400":{"description":"Invalid body (`details.errors`), or `duplicate_identifier`: an identifier is already held by another contact (`details.conflicts`). A phone channel (`sms`, `whatsapp`, `voice`) whose identifier is not E.164 is refused with `details.errors[].path` `[\"channels\", i, \"identifier\"]` (WhatsApp also takes a BSUID). An `email` identifier that is not an email address is refused the same way. Email identifiers are stored lower-case, and one that differs from another contact's only in case is that contact's address: `duplicate_identifier`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}],"description":"Create a contact with one or more channels. An identifier another contact already holds on the same channel is refused (`400 duplicate_identifier`, with `details.conflicts[{channel, identifier, contactId}]`); update the existing contact with `PATCH /v2/contacts/{contactId}` instead."}},"/v2/contacts/{contactId}":{"get":{"operationId":"getContactV2","tags":["Contacts"],"summary":"Get a contact","parameters":[{"name":"contactId","in":"path","required":true,"description":"","schema":{"type":"string"}}],"responses":{"200":{"description":"The contact.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contact"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Contact not found, or it belongs to another project.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]},"patch":{"operationId":"updateContactV2","tags":["Contacts"],"summary":"Update a contact","parameters":[{"name":"contactId","in":"path","required":true,"description":"","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactUpdateRequest"}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contact"}}}},"400":{"description":"Invalid update.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Contact not found, or it belongs to another project.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]},"delete":{"operationId":"deleteContactV2","tags":["Contacts"],"summary":"Delete a contact","description":"Permanently delete a contact and its channels (right to erasure). Conversations, message history and campaign delivery logs are kept and no longer reference it.","parameters":[{"name":"contactId","in":"path","required":true,"description":"","schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted."},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Contact not found (including a second delete of the same contact), or it belongs to another project.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/channels":{"get":{"operationId":"listChannelsV2","tags":["Channels"],"summary":"List channels","description":"Every (address, type) the project can send from, and whether it can send right now. Paginated by sender: a page holds every entry of up to `limit` senders, so it can hold more than `limit` entries.","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":100}},{"name":"cursor","in":"query","description":"Opaque `nextCursor` from the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"Entries.","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ChannelEntry"}},"nextCursor":{"type":"string","nullable":true}}}}}},"400":{"description":"`limit` outside 1–100 (`details.errors`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]},"post":{"operationId":"connectChannelV2","tags":["Channels"],"summary":"Connect a channel","description":"Connect a channel without buying anything, so a new project can send its first message from /v2. Returns every entry of the connected address (`GET /v2/channels/{address}`).\n\n**Where it lands.** Channels are grouped: the entries of `GET /v2/channels` that share a `v1SenderId` are one group, and a group holds one connection per transport. The new channel joins the project's default group when that group does not carry the transport yet; otherwise, or when the project has none, it starts a new group (the first group of a project becomes its default). So in a fresh project every channel connected here lands on the default and `POST /v2/messages` needs no `from`; a second email address or a second bot starts its own group and is used by passing its address as `from`.\n\n**Idempotent** for `sms_oneway` and `email`: an address already connected answers `200` with its entries and changes nothing.\n\n**Active or not.** One-way SMS is active at once. An email or Telegram connection the plan includes is switched on by itself; one that would cost money is listed with `active: false` and is switched on with `POST /v2/channels/{address}/{type}/activate` (same price and limits as on /v1).\n\nA Telegram token that Telegram refuses leaves nothing behind.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelConnectRequest"},"examples":{"sms_oneway":{"summary":"One-way SMS","value":{"type":"sms_oneway"}},"email":{"summary":"Email","value":{"type":"email","address":"noreply@yourdomain.com","fromName":"Acme"}},"telegram":{"summary":"Telegram bot","value":{"type":"telegram","botToken":"123456:ABC-DEF"}}}}}},"responses":{"200":{"description":"Already connected; nothing changed.","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ChannelEntry"}}}}}}},"201":{"description":"Connected. `items` are the entries of the connected address.","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ChannelEntry"}}}}}}},"400":{"description":"Invalid body (`details.errors`), a `type` that is connected elsewhere (the message says where), or a Telegram token Telegram refused (`details.errors[0].path` is `[\"botToken\"]`). `invalid_bot_token`: Telegram rejected the bot token (`details.telegramSaid` has its words); no bot and no sender are left behind.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`kyb_required`: one-way SMS is closed to this team until its business verification is approved. Only on an account required to verify its business first; one-way SMS needs no verification by default. `details.dashboardUrl` is `/kyb`; `details.recommendation` names the channels that are open.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"kyb_required","message":"One-way SMS requires an approved business verification. Complete it at /kyb, or send on a channel that does not need it.","details":{"type":"sms_oneway","dashboardUrl":"/kyb"}}}}},"409":{"description":"`email_domain_missing`: the address's domain is not an email domain of this project (`details.domain`; add it with `POST /v2/email-domains`). `email_domain_not_verified`: it is, but not verified yet (`details.emailDomainId`, `details.status`; verify with `POST /v2/email-domains/{emailDomainId}/verify`). `domain_setup_failed`: the domain has no DKIM records and will never verify, so publishing or verifying cannot help (`details.setupError`, the same text `GET /v2/email-domains/{emailDomainId}` shows).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"email_domain_not_verified","message":"acme.com is not verified yet (status: pending), so noreply@acme.com cannot send yet.","details":{"domain":"acme.com","emailDomainId":"emd_abc123","status":"pending","recommendation":"Publish the DNS records from GET /v2/email-domains/emd_abc123, then POST /v2/email-domains/emd_abc123/verify and retry."}}}}}},"security":[{"bearerAuth":[]}]}},"/v2/templates":{"get":{"operationId":"listTemplatesV2","tags":["Templates"],"summary":"List templates","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":100}},{"name":"cursor","in":"query","description":"Opaque `nextCursor` from the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"Templates.","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Template"}},"nextCursor":{"type":"string","nullable":true}}}}}},"400":{"description":"Invalid query (`limit` above 100, or a malformed `cursor`), with `details.errors`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]},"post":{"operationId":"createTemplateV2","tags":["Templates"],"summary":"Create a template","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateCreateRequest"}}}},"responses":{"201":{"description":"Created as `draft`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Template"}}}},"400":{"description":"Invalid template.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`conflict`: this project already has a template with that name.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/templates/{templateId}":{"get":{"operationId":"getTemplateV2","tags":["Templates"],"summary":"Get a template","parameters":[{"name":"templateId","in":"path","required":true,"description":"","schema":{"type":"string"}}],"responses":{"200":{"description":"The template.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Template"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Template not found, or it belongs to another project.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]},"delete":{"operationId":"deleteTemplateV2","tags":["Templates"],"summary":"Delete a template","description":"As `DELETE /v1/templates/{templateId}`.","parameters":[{"name":"templateId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted."},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Template not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/templates/{templateId}/submit":{"post":{"operationId":"submitTemplateV2","tags":["Templates"],"summary":"Submit a template to WhatsApp","description":"Submit a draft template to Meta for approval, naming the WhatsApp number by its address (`from`).","parameters":[{"name":"templateId","in":"path","required":true,"description":"","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateSubmitRequest"}}}},"responses":{"202":{"description":"Submitted; status becomes `pending`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Template"}}}},"400":{"description":"Not a draft, or `no_whatsapp`: the `from` address exists but has no WhatsApp Business Account (`details.types` lists what it has).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Template not found, or no channel of this project has the `from` address.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`ambiguous_from`: two of your senders have a `whatsapp` entry with this address.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/campaigns":{"get":{"operationId":"listCampaignsV2","tags":["Campaigns"],"summary":"List campaigns","parameters":[{"name":"status","in":"query","required":false,"schema":{"$ref":"#/components/schemas/BroadcastStatus"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"cursor","in":"query","required":false,"description":"Opaque, from a previous `nextCursor`.","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of campaigns.","content":{"application/json":{"schema":{"type":"object","required":["items","nextCursor"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Broadcast"}},"nextCursor":{"type":"string","nullable":true}}}}}},"400":{"description":"Invalid query parameter (`details.errors`, e.g. `path: [\"limit\"]`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}],"description":"Campaigns of this project, newest first, as `GET /v1/broadcasts`, each with `from` instead of `senderId`. Cursor pagination: pass `nextCursor` back as `cursor`."},"post":{"operationId":"createCampaignV2","tags":["Campaigns"],"summary":"Create a campaign","description":"A campaign is a /v1 broadcast; the id works on `/v1/broadcasts/{broadcastId}`. Runs `POST /v1/broadcasts` with the sender resolved from `from`. A repeated `idempotencyKey` answers `409 conflict` with the original in `details.campaign`. A `channel` the resolved sender does not carry (`smart`: no channel at all) is refused at creation with `409 no_channel`, as on `POST /v2/messages`, rather than created and failed at send. `from` with a `channel` it does not carry (a `sms` campaign `from: \"sms_oneway\"`) is that same `409 no_channel`, in the words `POST /v2/messages` uses (`details.connected`, and `sms` vs `sms_oneway` told apart); `channel` only picks between senders that share an address, it never makes an existing address `404`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BroadcastCreateRequest"}}}},"responses":{"201":{"description":"Created as `draft`.","content":{"application/json":{"schema":{"type":"object","required":["campaign"],"properties":{"campaign":{"$ref":"#/components/schemas/Broadcast"}}}}}},"400":{"description":"Invalid body (`details.errors`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`kyb_required`: `channel: \"sms_oneway\"` on an account required to verify its business before using one-way SMS, until that verification is approved. One-way SMS needs no verification by default.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`from` names no address of this project (`details.from`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`no_channel`: no `from` and no default, or the sender has no `channel` transport (`details.channel`, `details.connect`, `details.connected`, the same answer as `POST /v2/messages` with that `channel`). `ambiguous_from`, or `conflict` (repeated `idempotencyKey`; the original is in `details.campaign`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/campaigns/{campaignId}":{"get":{"operationId":"getCampaignV2","tags":["Campaigns"],"summary":"Get a campaign","parameters":[{"name":"campaignId","in":"path","required":true,"description":"Campaign ID. The same ID works as `broadcastId` on /v1.","schema":{"type":"string"}}],"responses":{"200":{"description":"The campaign.","content":{"application/json":{"schema":{"type":"object","required":["campaign"],"properties":{"campaign":{"$ref":"#/components/schemas/Broadcast"}}}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Campaign not found, or it belongs to another project.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]},"delete":{"operationId":"deleteCampaignV2","tags":["Campaigns"],"summary":"Delete a draft campaign","parameters":[{"name":"campaignId","in":"path","required":true,"description":"Campaign ID. The same ID works as `broadcastId` on /v1.","schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted."},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Campaign not found, or it belongs to another project.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`invalid_status`: not a draft (`details.status`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}],"description":"Deletes a campaign that is still a `draft`, as `DELETE /v1/broadcasts/{broadcastId}`. Any other status is refused with `409 invalid_status` (`details.status`): a campaign that was sent or cancelled is kept as the record of what went out, and one still in review, scheduled or sending is cancelled with `POST /v2/campaigns/{campaignId}/cancel` (`details.recommendation`). /v1 refuses the same with a `400`."}},"/v2/campaigns/{campaignId}/recipients":{"post":{"operationId":"addCampaignRecipientsV2","tags":["Campaigns"],"summary":"Add recipients","description":"Up to 1,000 per call. Recipients are held to the rules `POST /v2/messages` holds `to` to. Validation happens in two steps with different outcomes:\n\n1. **The request as a whole.** A phone number written with spaces, dashes, dots or parentheses after its `+` is read as the E.164 number first. Then a recipient that is not a phone number, email address, BSUID or chat id makes the whole request `400`, with `details.errors[].path` naming it (`[\"contacts\", 2, \"recipient\"]`); so do 8-15 bare digits on an `sms`, `sms_oneway`, `whatsapp`, `voice` or `smart` campaign, with `details.errors[].suggestion` (`+<digits>`), as a send refuses them. Nothing is added.\n2. **Each recipient against the campaign.** A well-formed recipient the campaign cannot reach is skipped and reported in `invalid` and `errors[{recipient, reason, code?}]`: `code: \"unreachable_by_channel\"` for an email address on a phone campaign or a phone number on an email campaign; `code: \"ten_dlc_required\"` for a US/Canadian (+1) number on an `sms` or `sms_oneway` campaign while this project has no approved 10DLC campaign (the send would refuse it, as `POST /v2/messages` answers `403 ten_dlc_required`). Repeats of a recipient already in the campaign or in the same call are skipped as `duplicates`. The others are added, and the call answers `201`.\n\nSo `201` can mean some were not added: check `invalid`. A campaign that is not a draft answers `409 invalid_status`.","parameters":[{"name":"campaignId","in":"path","required":true,"description":"Campaign ID. The same ID works as `broadcastId` on /v1.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BroadcastContactsAddRequest"}}}},"responses":{"201":{"description":"Processed. `added` + `duplicates` + `invalid` equals the number sent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BroadcastContactsAddResponse"}}}},"400":{"description":"A malformed recipient, or bare digits on a phone campaign (`details.errors`, with `suggestion`); nothing added.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Campaign not found, or it belongs to another project.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`invalid_status`: the campaign is not a draft (`details.status`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/campaigns/{campaignId}/send":{"post":{"operationId":"sendCampaignV2","tags":["Campaigns"],"summary":"Send or schedule a campaign","description":"Same review rules as `POST /v1/broadcasts/{broadcastId}/send`: a WhatsApp campaign on an approved template starts directly, email starts once automated review passes, other channels wait for a person. Omit `scheduledAt` to send now.","parameters":[{"name":"campaignId","in":"path","required":true,"description":"Campaign ID. The same ID works as `broadcastId` on /v1.","schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BroadcastSendRequest"}}}},"responses":{"202":{"description":"Submitted for review, scheduled, or started.","content":{"application/json":{"schema":{"type":"object","required":["campaign"],"properties":{"campaign":{"$ref":"#/components/schemas/Broadcast"}}}}}},"400":{"description":"No recipients, invalid status, `template_not_approved`, or `sender_not_found` when the campaign's sender was deleted after it was created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"`insufficient_balance`, with `details.availableBalance`; or `paid_plan_required`: an email campaign on the Free plan (`details.dashboardUrl`). Email campaigns need a paid plan; drafts can still be created and edited.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Campaign not found, or it belongs to another project.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/campaigns/{campaignId}/progress":{"get":{"operationId":"getCampaignProgressV2","tags":["Campaigns"],"summary":"Get campaign progress","parameters":[{"name":"campaignId","in":"path","required":true,"description":"Campaign ID. The same ID works as `broadcastId` on /v1.","schema":{"type":"string"}}],"responses":{"200":{"description":"Progress.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BroadcastProgress"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Campaign not found, or it belongs to another project.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/webhook":{"get":{"operationId":"getWebhookV2","tags":["Webhook"],"summary":"Get the project webhook","description":"One webhook for the whole project and every event, `message.*`, `conversation.new` and `call.*` included. Payloads name addresses (`from`, `to`) and the transport (`type`), never a sender id; see `WebhookPayloadV2`.\n\n**Alongside /v1 webhooks.** Configuring this webhook is the opt-in. From then on each event is delivered here *in addition to* any /v1 sender webhook or /v1 project webhook subscribed to it, as independent deliveries with different event ids. Nothing is deduplicated. `senderWebhooks` lists the /v1 sender webhooks still on; switch them off with `disableSenderWebhooks` on `PUT` once this one is verified.\n\nIt does not share storage with `/v1/invitations/webhook`.","responses":{"200":{"description":"The webhook (null when none), and the /v1 sender webhooks still on.","content":{"application/json":{"schema":{"type":"object","required":["webhook","senderWebhooks"],"properties":{"webhook":{"allOf":[{"$ref":"#/components/schemas/ProjectWebhookV2"}],"nullable":true},"senderWebhooks":{"type":"array","items":{"$ref":"#/components/schemas/SenderWebhookV1"}}}}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]},"put":{"operationId":"putWebhookV2","tags":["Webhook"],"summary":"Create or replace the project webhook","description":"Create or replace the webhook. `url` must be HTTPS. The signature scheme defaults to `v2`. A webhook cannot move straight from `v1` to `v2`, or from `v2` to `v1` (`400`): a receiver verifying the current scheme would break on the next delivery, so both directions go through `v1+v2`. Replacing keeps the secret.\n\nPass `disableSenderWebhooks: true` to switch off the /v1 sender webhooks of this project in the same call, so each event arrives once.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectWebhookV2Put"}}}},"responses":{"200":{"description":"Replaced. `secret` is not returned.","content":{"application/json":{"schema":{"type":"object","required":["webhook","senderWebhooks","disabledSenderWebhooks"],"properties":{"webhook":{"$ref":"#/components/schemas/ProjectWebhookV2"},"senderWebhooks":{"type":"array","items":{"$ref":"#/components/schemas/SenderWebhookV1"}},"disabledSenderWebhooks":{"type":"integer","description":"How many /v1 sender webhooks this call switched off."}}}}}},"201":{"description":"Created. `webhook.secret` is returned once.","content":{"application/json":{"schema":{"type":"object","required":["webhook","senderWebhooks","disabledSenderWebhooks"],"properties":{"webhook":{"$ref":"#/components/schemas/ProjectWebhookV2"},"senderWebhooks":{"type":"array","items":{"$ref":"#/components/schemas/SenderWebhookV1"}},"disabledSenderWebhooks":{"type":"integer","description":"How many /v1 sender webhooks this call switched off."}}}}}},"400":{"description":"Invalid URL, an event that does not exist (`details.unknown`, `details.allowed`, and `details.errors` naming each by index), or `v1` to `v2` / `v2` to `v1` directly (go through `v1+v2`; `details.errors[0].path` is `[\"signatureVersion\"]`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]},"delete":{"operationId":"deleteWebhookV2","tags":["Webhook"],"summary":"Remove the webhook","description":"Remove the /v2 webhook and its secret. /v1 sender webhooks are not touched, including any `disableSenderWebhooks` switched off: they stay off.","responses":{"204":{"description":"Removed."},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No /v2 webhook is configured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/webhook/secret":{"post":{"operationId":"rotateWebhookSecretV2","tags":["Webhook"],"summary":"Rotate the webhook secret","responses":{"200":{"description":"New secret. The old one stops working immediately.","content":{"application/json":{"schema":{"type":"object","required":["secret"],"properties":{"secret":{"type":"string"}}}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No /v2 webhook is configured. Create one with `PUT /v2/webhook`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/agents":{"get":{"operationId":"listAgentsV2","tags":["Agents"],"summary":"List agents","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":100}},{"name":"cursor","in":"query","description":"Opaque `nextCursor` from the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"Agents, newest first.","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Agent"}},"nextCursor":{"type":"string","nullable":true}}}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]},"post":{"operationId":"createAgentV2","tags":["Agents"],"summary":"Create an agent","description":"Connect it to an address with `POST /v2/agents/{agentId}/channels`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentCreateStandaloneRequest"}}}},"responses":{"201":{"description":"Created, disabled and connected to no channel.","content":{"application/json":{"schema":{"type":"object","required":["agent"],"properties":{"agent":{"$ref":"#/components/schemas/Agent"}}}}}},"400":{"description":"A required field is missing or invalid. Every missing required field is listed, not the first (`details.errors`, e.g. `[{\"path\":[\"provider\"],\"message\":\"provider is required\"},{\"path\":[\"model\"],\"message\":\"model is required\"}]`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/agents/{agentId}":{"get":{"operationId":"getAgentV2","tags":["Agents"],"summary":"Get an agent","parameters":[{"name":"agentId","in":"path","required":true,"description":"","schema":{"type":"string"}}],"responses":{"200":{"description":"The agent.","content":{"application/json":{"schema":{"type":"object","required":["agent"],"properties":{"agent":{"$ref":"#/components/schemas/Agent"}}}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Agent not found, or it belongs to another project.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]},"patch":{"operationId":"updateAgentV2","tags":["Agents"],"summary":"Update an agent","parameters":[{"name":"agentId","in":"path","required":true,"description":"","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentUpdateRequest"}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"type":"object","required":["agent"],"properties":{"agent":{"$ref":"#/components/schemas/Agent"}}}}}},"400":{"description":"Invalid request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Agent not found, or it belongs to another project.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]},"delete":{"operationId":"deleteAgentV2","tags":["Agents"],"summary":"Delete an agent","description":"As `DELETE /v1/agents/{agentId}`.","parameters":[{"name":"agentId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted."},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Agent not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/agents/{agentId}/test":{"post":{"operationId":"testAgentV2","tags":["Agents"],"summary":"Test an agent","description":"Run the agent's prompt, model and knowledge base against a message and return the reply instead of delivering it. Nothing is delivered and no execution is recorded.\n\n**Billed like a live reply.** On an agent with the `zavu` provider the run is charged to your balance at the same price per token, and refused with `402 insufficient_balance` at a balance of zero; on a bring-your-own-key agent it costs your provider. `cost` on the response is what it charged. Runs are rate limited per project (`429`).\n\n**Tools.** By default the model is offered the agent's enabled tools but none is run: if it picks one, the reply stops there and `plannedToolCalls` says which, with what arguments. Set `executeTools` to run them. That reaches your real handlers, with whatever they cost and change; read `executedToolCalls`.","parameters":[{"name":"agentId","in":"path","required":true,"description":"Agent ID.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentTestRequest"}}}},"responses":{"200":{"description":"The agent's reply. Nothing was delivered.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentTestResponse"}}}},"400":{"description":"`message` is required, or the message and history exceed 64 KB.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"The agent runs on a Zavu model and the balance is at or below zero.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Agent not found, or it belongs to another project.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many test runs for this project. Retry after the number of seconds in the message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/channels/{address}":{"get":{"operationId":"getChannelV2","tags":["Channels"],"summary":"Get the entries of one address","parameters":[{"name":"address","in":"path","required":true,"description":"An address as `GET /v2/channels` lists it, URL-encoded (`+` as `%2B`). Numbers match in any common formatting.","schema":{"type":"string"},"example":"%2B13125551212"}],"responses":{"200":{"description":"The entries.","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ChannelEntry"}}}}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No entry of this project has the address (`details.address`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}],"description":"Every transport the address carries, one entry each."}},"/v2/channels/{address}/{type}/activate":{"post":{"operationId":"activateChannelV2","tags":["Channels"],"summary":"Activate a transport","description":"The same switch as **Activate** on Accounts in the dashboard, with the same price and limits: on a paid plan a month is charged at once, stated in `chargedCents` and `monthlyCents`. `activated: false` means it was already on and nothing was charged.","parameters":[{"name":"address","in":"path","required":true,"description":"An address as `GET /v2/channels` lists it, URL-encoded (`+` as `%2B`). Numbers match in any common formatting.","schema":{"type":"string"},"example":"%2B13125551212"},{"name":"type","in":"path","required":true,"description":"Transport to switch.","schema":{"$ref":"#/components/schemas/ActivatableChannel"}}],"responses":{"200":{"description":"The transport is active.","content":{"application/json":{"schema":{"type":"object","required":["address","type","activated","chargedCents","monthlyCents","channels"],"properties":{"address":{"type":"string"},"type":{"$ref":"#/components/schemas/ActivatableChannel"},"channels":{"type":"array","description":"Every entry of the address after the change.","items":{"$ref":"#/components/schemas/ChannelEntry"}},"activated":{"type":"boolean"},"chargedCents":{"type":"integer"},"monthlyCents":{"type":"integer"}}}}}},"400":{"description":"The transport has no switch (`sms`, `sms_oneway`, `voice`), or the key is a test-mode key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"`insufficient_balance`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`plan_limit_reached`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No entry of this project has the address (`details.address`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`channel_not_connected` (the address has no such transport), `connection_not_ready`, or `ambiguous_from` (two channel groups share the address and neither carries this transport).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/channels/{address}/{type}/deactivate":{"post":{"operationId":"deactivateChannelV2","tags":["Channels"],"summary":"Deactivate a transport","description":"Switch the connection off. It stays connected, so activating it again needs no reconnection. The month already paid is not refunded.","parameters":[{"name":"address","in":"path","required":true,"description":"An address as `GET /v2/channels` lists it, URL-encoded (`+` as `%2B`). Numbers match in any common formatting.","schema":{"type":"string"},"example":"%2B13125551212"},{"name":"type","in":"path","required":true,"description":"Transport to switch.","schema":{"$ref":"#/components/schemas/ActivatableChannel"}}],"responses":{"200":{"description":"The transport is inactive.","content":{"application/json":{"schema":{"type":"object","required":["address","type","deactivated","channels"],"properties":{"address":{"type":"string"},"type":{"$ref":"#/components/schemas/ActivatableChannel"},"channels":{"type":"array","description":"Every entry of the address after the change.","items":{"$ref":"#/components/schemas/ChannelEntry"}},"deactivated":{"type":"boolean"}}}}}},"400":{"description":"The transport has no switch (`sms`, `sms_oneway`, `voice`), or the key is a test-mode key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No entry of this project has the address (`details.address`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`channel_not_connected` (the address has no such transport), `connection_not_ready`, or `ambiguous_from` (two channel groups share the address and neither carries this transport).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/numbers/available":{"get":{"operationId":"searchNumbersV2","tags":["Numbers"],"summary":"Search numbers to buy","description":"Search numbers to buy, by country and type.","parameters":[{"name":"countryCode","in":"query","required":true,"schema":{"type":"string","minLength":2,"maxLength":2}},{"name":"type","in":"query","schema":{"$ref":"#/components/schemas/PhoneNumberType"}},{"name":"contains","in":"query","schema":{"type":"string","maxLength":20}},{"name":"capabilities","in":"query","schema":{"type":"string","example":"voice,sms"}},{"name":"limit","in":"query","schema":{"type":"integer","default":10,"maximum":50}}],"responses":{"200":{"description":"Numbers.","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AvailablePhoneNumber"}}}}}}},"400":{"description":"Invalid query, or `country_restricted`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`country_unavailable`: no stock in this country.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/numbers":{"post":{"operationId":"purchaseNumberV2","tags":["Numbers"],"summary":"Buy a number","description":"Buy a number and make it an SMS channel in the same call. The purchase follows the same rules as `POST /v1/phone-numbers`, including regulatory requirements, and the number comes back with SMS on. There is no separate assign step. Where it lands: when the project's default channel group has no number yet, the number joins that group (and becomes the project's first channel and default when there is none); otherwise it gets a channel group of its own.\n\nWhile the number is in regulatory review (`number.regulatoryStatus: pending_review`) it is attached but cannot send, and `channels` can be empty until it is approved. If the purchase succeeded but turning SMS on failed, the number is still bought and billed; the response says so in `setupError` rather than failing the call.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhoneNumberPurchaseRequest"}}}},"responses":{"201":{"description":"Bought.","content":{"application/json":{"schema":{"type":"object","required":["number","channels"],"properties":{"number":{"$ref":"#/components/schemas/OwnedPhoneNumber"},"channels":{"type":"array","description":"The entries of the new number (`sms`, once routed).","items":{"$ref":"#/components/schemas/ChannelEntry"}},"setupError":{"allOf":[{"$ref":"#/components/schemas/Error"}],"description":"Present only when the purchase succeeded and turning SMS on did not. `details.recommendation` says how to retry."}}}}}},"400":{"description":"`number_unavailable`, `regulatory_compliance_required`, `invalid_request`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"`insufficient_balance` or `paid_plan_required`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"`requirements_unavailable`; nothing charged.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/agents/{agentId}/channels":{"post":{"operationId":"connectAgentChannelV2","tags":["Agents"],"summary":"Connect an address","description":"Make the agent answer on an address. Connecting `{address, type}` connects the whole channel group that entry belongs to (the entries of `GET /v2/channels` that share its `v1SenderId`): the agent then answers on each transport of that group that its `triggerOnChannels` allows, which `channels` on the agent lists. `type` is what picks the group when two groups share the address.\n\nAn address answers with one agent at a time; connecting one already answered by another agent returns `400` naming that agent.","parameters":[{"name":"agentId","in":"path","required":true,"description":"","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["address","type"],"properties":{"address":{"type":"string","example":"+13125551212"},"type":{"$ref":"#/components/schemas/ChannelType"}}}}}},"responses":{"201":{"description":"Connected.","content":{"application/json":{"schema":{"type":"object","required":["agent"],"properties":{"agent":{"$ref":"#/components/schemas/Agent"}}}}}},"400":{"description":"Missing `address`/`type`, or the address is already answered by another agent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Agent not found, or no entry of this project has the address (`details.address`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`ambiguous_from`. `channel_cannot_receive`: `type` is `sms_oneway`, which receives nothing, so an agent there would never answer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/agents/{agentId}/channels/{address}/{type}":{"delete":{"operationId":"disconnectAgentChannelV2","tags":["Agents"],"summary":"Disconnect an address","description":"Stop the agent answering on the address. This disconnects the channel group behind `{address, type}`, so every transport of that group stops reaching the agent. An agent created on /v1 for a sender keeps that sender: its addresses cannot be disconnected here, and that answers `400`.","parameters":[{"name":"agentId","in":"path","required":true,"description":"","schema":{"type":"string"}},{"name":"address","in":"path","required":true,"description":"An address as `GET /v2/channels` lists it, URL-encoded (`+` as `%2B`). Numbers match in any common formatting.","schema":{"type":"string"},"example":"%2B13125551212"},{"name":"type","in":"path","required":true,"schema":{"$ref":"#/components/schemas/ChannelType"}}],"responses":{"204":{"description":"Disconnected."},"400":{"description":"The agent does not answer on the address, or the address belongs to the sender the agent was created for on /v1.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Agent not found, or no entry of this project has the address (`details.address`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`ambiguous_from`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/email-domains":{"get":{"operationId":"listEmailDomainsV2","tags":["Email Domains"],"summary":"List email domains","description":"Email domains of this project.","responses":{"200":{"description":"Email domains.","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/EmailDomain"}}}}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]},"post":{"operationId":"addEmailDomainV2","tags":["Email Domains"],"summary":"Add an email domain","description":"Add a domain to send email from. `domain` is validated first: a bare domain (a leading `https://` and a trailing path are dropped); anything else, spaces included, is `400` with `details.errors[0].path` `[\"domain\"]`. Adding a domain the project already has returns it with `200` (nothing created), like `POST /v2/channels`.\n\nReturns the DNS records to publish; the DKIM CNAMEs are required. If they could not be generated, the domain is still created (`201`) but comes back `verifiable: false` with `setupError`: remove it and add it again, or contact support. Verify cannot fix it.\n\nPublish the records exactly as returned, verify, then connect an address with `POST /v2/channels {\"type\":\"email\"}`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["domain"],"properties":{"domain":{"type":"string","example":"example.com"}}}}}},"responses":{"200":{"description":"Already a domain of this project; returned as it is, nothing created.","content":{"application/json":{"schema":{"type":"object","required":["domain"],"properties":{"domain":{"$ref":"#/components/schemas/EmailDomain"}}}}}},"201":{"description":"Domain added.","content":{"application/json":{"schema":{"type":"object","required":["domain"],"properties":{"domain":{"$ref":"#/components/schemas/EmailDomain"}}}}}},"400":{"description":"Missing or invalid `domain` (`details.errors`, `path: [\"domain\"]`), or the domain could not be set up (the message says what to do).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/email-domains/{emailDomainId}":{"get":{"operationId":"getEmailDomainV2","tags":["Email Domains"],"summary":"Get an email domain","description":"The domain with its DNS records and status.","parameters":[{"name":"emailDomainId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Domain.","content":{"application/json":{"schema":{"type":"object","required":["domain"],"properties":{"domain":{"$ref":"#/components/schemas/EmailDomain"}}}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Email domain not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]},"delete":{"operationId":"deleteEmailDomainV2","tags":["Email Domains"],"summary":"Remove an email domain","parameters":[{"name":"emailDomainId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Removed."},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Email domain not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/email-domains/{emailDomainId}/verify":{"post":{"operationId":"verifyEmailDomainV2","tags":["Email Domains"],"summary":"Verify an email domain","description":"Re-check the domain's DNS records and refresh its status. `404` only for a domain that is not this project's; `503 verification_unavailable` when the check could not run (nothing changed, retry). A domain with no DKIM records (`verifiable: false`) cannot verify whatever DNS says: verify runs the check (it may recover the records), and when the domain still has none it answers `409 domain_setup_failed` with the same `setupError` the domain carries.","parameters":[{"name":"emailDomainId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Re-checked.","content":{"application/json":{"schema":{"type":"object","required":["domain"],"properties":{"domain":{"$ref":"#/components/schemas/EmailDomain"}}}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`Email domain not found`: not a domain of this project.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`domain_setup_failed`: the domain has no DKIM records, so it cannot verify (`details.setupError`, `details.recommendation`: remove it and add it again, or contact support).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`verification_unavailable`: the DNS check could not run. Nothing changed; retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/channels/{address}/{type}":{"delete":{"operationId":"disconnectChannelV2","tags":["Channels"],"summary":"Disconnect a channel","description":"Disconnect one (address, type). `sms_oneway`, `telegram` and `voice` can be disconnected here. The others answer `409 disconnect_unavailable` saying what to do instead: `whatsapp`, `instagram`, `messenger` and `email` can be switched off with `.../deactivate` (they stay connected); two-way `sms` comes with the address's number.","parameters":[{"name":"address","in":"path","required":true,"schema":{"type":"string"}},{"name":"type","in":"path","required":true,"schema":{"$ref":"#/components/schemas/ChannelType"}}],"responses":{"204":{"description":"Disconnected."},"400":{"description":"Unknown `type`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No channel of this project has the address, or it has no channel of this `type` (`details.types` lists what it has).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`disconnect_unavailable`, or `ambiguous_from` when two channel groups share the address for this type.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/campaigns/{campaignId}/cancel":{"post":{"operationId":"cancelCampaignV2","tags":["Campaigns"],"summary":"Cancel a campaign","description":"As `POST /v1/broadcasts/{broadcastId}/cancel`. Recipients not yet sent are skipped; messages already queued may still be delivered.","parameters":[{"name":"campaignId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Cancelled.","content":{"application/json":{"schema":{"type":"object","required":["campaign"],"properties":{"campaign":{"$ref":"#/components/schemas/Broadcast"}}}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Campaign not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`invalid_status`: the campaign has finished and cannot be cancelled (`details.status`). /v1 answers this `400`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/functions":{"get":{"operationId":"listFunctionsV2","tags":["Functions"],"summary":"List functions","description":"The project's functions, newest first.","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","default":50,"minimum":1,"maximum":100}},{"name":"cursor","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of functions.","content":{"application/json":{"schema":{"type":"object","required":["items","nextCursor"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Function"}},"nextCursor":{"type":"string","nullable":true}}}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v2/functions/{functionId}":{"get":{"operationId":"getFunctionV2","tags":["Functions"],"summary":"Get a function","parameters":[{"name":"functionId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The function.","content":{"application/json":{"schema":{"type":"object","required":["function"],"properties":{"function":{"$ref":"#/components/schemas/Function"}}}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Function not found, or it belongs to another project.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]},"patch":{"operationId":"updateFunctionV2","tags":["Functions"],"summary":"Configure a function","description":"Name, description, timeout, memory, architecture, pause and the HTTP endpoint, with the same rules as the dashboard's Settings tab.","parameters":[{"name":"functionId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FunctionUpdateRequest"}}}},"responses":{"200":{"description":"The function after the change.","content":{"application/json":{"schema":{"type":"object","required":["function"],"properties":{"function":{"$ref":"#/components/schemas/Function"}}}}}},"400":{"description":"Nothing to change, or invalid values (each in `details.errors`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Function not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`function_not_ready`: pausing or resuming a function that is a draft, deploying, or whose last deploy failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}}}}