---
title: "Send a message"
method: POST
path: "/v1/messages"
---

# Send a message

`POST /v1/messages`

Send a message to a recipient via SMS or WhatsApp.

**Channel selection:**
- If `channel` is omitted and `messageType` is `text`, defaults to SMS
- If `messageType` is anything other than `text`, WhatsApp is used automatically

**WhatsApp 24-hour window:**
- Free-form messages (non-template) require an open 24h window
- Window opens when the user messages you first
- Use template messages to initiate conversations outside the window

**Plan allowances and email billing:**
- WhatsApp, Telegram, Instagram and Messenger share an allowance of 2,000 messages per month on Free. Over it, sends return 429 with code `a2p_limit_exceeded` and upgrade details; the counter resets on the 1st of each month. Paid plans have no message caps
- Email is billed from your prepaid balance in 1,000-message blocks: $0.40 per 1,000 transactional emails, $0.80 per 1,000 marketing (broadcast) emails. A block is charged when your monthly count crosses each 1,000 boundary, and at zero balance email sends return 402 with code `insufficient_balance`. Free teams start with $2 of credit and additionally cap at 3,000 emails/month and 100/day. Teams on earlier plans keep their original email quotas instead
- SMS and voice are billed per message from your balance on every plan

**Email recipient pre-flight:**
Email messages are validated automatically before dispatch. Sends that would be a guaranteed hard bounce are failed instead of sent, protecting your bounce rate: the message transitions to `failed` (visible via `GET /v1/messages/{messageId}` and the `message.failed` webhook) with `errorCode` set to `EMAIL_INVALID_RECIPIENT` (malformed address), `EMAIL_DOMAIN_NOT_FOUND` (recipient domain has no MX or A records), or `EMAIL_RECIPIENT_SUPPRESSED` (address is on your suppression list after a previous bounce or complaint). Advisory signals (role addresses, disposable domains) do not block sends — check them beforehand with `POST /v1/introspect/email`.

## Headers

- `Zavu-Sender` string

## Request body

- MessageRequest — Request body to send a message.
  - `to` string, required — 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 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.
  - `channel` 'auto' | 'sms' | 'sms_oneway' | 'whatsapp' | 'telegram' | 'email' | 'instagram' | 'messenger' | 'voice' — Delivery channel. Use 'auto' for intelligent routing.
  - `messageType` 'text' | 'image' | 'video' | 'audio' | 'document' | 'sticker' | 'location' | 'contact' | 'buttons' | 'list' | 'cta_url' | 'request_contact_info' | 'location_request' | 'reaction' | 'template' — Type of message. Non-text types are supported by WhatsApp and Telegram (varies by type). `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. `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.
  - `text` string — Text body for text messages or caption for media messages.
  - `content` MessageContent — Content for non-text message types (WhatsApp and Telegram).
    - `mediaUrl` string — URL of the media file (for image, video, audio, document, sticker).
    - `mediaId` string — WhatsApp media ID if already uploaded.
    - `mimeType` string — MIME type of the media.
    - `filename` string — Filename for documents.
    - `latitude` number — Latitude for location messages.
    - `longitude` number — Longitude for location messages.
    - `locationName` string — Name of the location.
    - `locationAddress` string — Address of the location.
    - `contacts` object[] — Contact cards for contact messages.
      - `name` string
      - `phones` string[]
    - `buttons` object[] — Interactive buttons (max 3).
      - `id` string, required
      - `title` string, required
    - `listButton` string — Button text for list messages.
    - `sections` object[] — Sections for list messages.
      - `title` string, required
      - `rows` object[], required
        - `id` string, required
        - `title` string, required
        - `description` string
    - `ctaDisplayText` string — Button label for cta_url messages.
    - `ctaUrl` string, uri — Destination URL opened in the device's default browser when the button is tapped. Used with messageType=cta_url. WhatsApp requires HTTPS in production.
    - `ctaHeaderType` 'text' | 'image' | 'video' | 'document' — Optional header type for cta_url messages.
    - `ctaHeaderText` string — Header text when ctaHeaderType is 'text'.
    - `ctaHeaderMediaUrl` string, uri — 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` string — Optional footer text for cta_url messages.
    - `emoji` string — Emoji for reaction messages.
    - `reactToMessageId` string — Message ID to react to.
    - `replyToMessageId` string — Zavu message ID of the quoted message this message replies to. 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` string — Provider message ID (WhatsApp WAMID) of the quoted message. Present whenever an inbound message is a reply, even if the quoted message is not stored in Zavu.
    - `replyToFrom` string — Sender of the quoted message (phone number in E.164 format).
    - `replyToText` string — Truncated snippet of the quoted message's text, for display. Empty when the quoted message has no text (e.g. media).
    - `replyToMessageType` string — Type of the quoted message (text, image, video, etc.).
    - `templateId` string — Template ID for template messages.
    - `templateVariables` object — 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.
    - `templateButtonVariables` object — 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. **WhatsApp constraints:** - 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. - At most one placeholder per URL button. - A template may have at most three buttons. - Static URL buttons (no placeholder) and `quick_reply` buttons are not included here.
    - `templateHeaderVariables` object — 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.
  - `subject` string — Email subject line. Required when channel is 'email' or recipient is an email address.
  - `htmlBody` string — HTML body for email messages. If provided, email will be sent as multipart with both text and HTML.
  - `replyTo` string, email — Reply-To email address for email messages.
  - `idempotencyKey` string — Optional idempotency key to avoid duplicate sends.
  - `metadata` object — Arbitrary metadata to associate with the message.
  - `fallbackEnabled` boolean — Whether to enable automatic fallback to SMS if WhatsApp fails. Defaults to true.
  - `voiceLanguage` string — 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.
  - `attachments` EmailAttachmentInput[] — Email attachments. Only supported when channel is 'email'. Maximum 40MB total size.
    - `filename` string, required — Name of the attached file.
    - `content` string — Content of the attached file as a Base64-encoded string.
    - `path` string, uri — URL where the attachment file is hosted. The server will fetch the file.
    - `content_type` string — MIME type of the attachment. If not set, will be derived from the filename.
    - `content_id` string — Content ID for inline images. Reference in HTML as `<img src="cid:your_content_id">`.

## Response `202`

Message accepted for delivery.

- MessageResponse
  - `message` Message, required
    - `id` string, required
    - `to` string, required
    - `from` string
    - `senderId` string
    - `channel` 'auto' | 'sms' | 'sms_oneway' | 'whatsapp' | 'telegram' | 'email' | 'instagram' | 'messenger' | 'voice', required — Delivery channel. Use 'auto' for intelligent routing.
    - `messageType` 'text' | 'image' | 'video' | 'audio' | 'document' | 'sticker' | 'location' | 'contact' | 'buttons' | 'list' | 'cta_url' | 'request_contact_info' | 'location_request' | 'reaction' | 'template', required — Type of message. Non-text types are supported by WhatsApp and Telegram (varies by type). `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. `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.
    - `status` 'queued' | 'sending' | 'sent' | 'delivered' | 'read' | 'failed' | 'received' | 'pending_url_verification', required
    - `text` string — Text content or caption.
    - `content` MessageContent — Content for non-text message types (WhatsApp and Telegram).
      - `mediaUrl` string — URL of the media file (for image, video, audio, document, sticker).
      - `mediaId` string — WhatsApp media ID if already uploaded.
      - `mimeType` string — MIME type of the media.
      - `filename` string — Filename for documents.
      - `latitude` number — Latitude for location messages.
      - `longitude` number — Longitude for location messages.
      - `locationName` string — Name of the location.
      - `locationAddress` string — Address of the location.
      - `contacts` object[] — Contact cards for contact messages.
        - `name` string
        - `phones` string[]
      - `buttons` object[] — Interactive buttons (max 3).
        - `id` string, required
        - `title` string, required
      - `listButton` string — Button text for list messages.
      - `sections` object[] — Sections for list messages.
        - `title` string, required
        - `rows` object[], required
          - `id` string, required
          - `title` string, required
          - `description` string
      - `ctaDisplayText` string — Button label for cta_url messages.
      - `ctaUrl` string, uri — Destination URL opened in the device's default browser when the button is tapped. Used with messageType=cta_url. WhatsApp requires HTTPS in production.
      - `ctaHeaderType` 'text' | 'image' | 'video' | 'document' — Optional header type for cta_url messages.
      - `ctaHeaderText` string — Header text when ctaHeaderType is 'text'.
      - `ctaHeaderMediaUrl` string, uri — 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` string — Optional footer text for cta_url messages.
      - `emoji` string — Emoji for reaction messages.
      - `reactToMessageId` string — Message ID to react to.
      - `replyToMessageId` string — Zavu message ID of the quoted message this message replies to. 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` string — Provider message ID (WhatsApp WAMID) of the quoted message. Present whenever an inbound message is a reply, even if the quoted message is not stored in Zavu.
      - `replyToFrom` string — Sender of the quoted message (phone number in E.164 format).
      - `replyToText` string — Truncated snippet of the quoted message's text, for display. Empty when the quoted message has no text (e.g. media).
      - `replyToMessageType` string — Type of the quoted message (text, image, video, etc.).
      - `templateId` string — Template ID for template messages.
      - `templateVariables` object — 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.
      - `templateButtonVariables` object — 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. **WhatsApp constraints:** - 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. - At most one placeholder per URL button. - A template may have at most three buttons. - Static URL buttons (no placeholder) and `quick_reply` buttons are not included here.
      - `templateHeaderVariables` object — 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.
    - `conversationId` string — 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.
    - `providerMessageId` string — Message ID from the delivery provider.
    - `errorCode` string, nullable
    - `errorMessage` string, nullable
    - `cost` number, nullable — Zavu platform charge in USD for this message. Messaging is billed against your plan's monthly limits plus usage-based overage.
    - `costProvider` number, nullable — Carrier and delivery cost in USD.
    - `costTotal` number, nullable — Total cost in USD (platform charge + delivery cost).
    - `metadata` object
    - `createdAt` string, date-time, required
    - `updatedAt` string, date-time

## Other responses

- `400` — Invalid request or WhatsApp 24h window not open.
- `401` — Unauthorized.
- `402` — Insufficient balance. Email is billed from your prepaid balance in 1,000-message blocks; SMS and voice are billed per message. When the balance cannot cover the send, the message is rejected until you add funds.
- `403` — Message blocked due to URL restrictions.
- `404` — Template or sender not found.
- `409` — Idempotency conflict (message already sent).
- `429` — Rate limit exceeded.
- `500` — Server error.

---

[API](https://skmtc.net/zavudev/apis/zavu-unified-messaging-layer-api.md) · [All operations](https://skmtc.net/zavudev/apis/zavu-unified-messaging-layer-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/zavudev/zavu-unified-messaging-layer-api/versions/07b87b6ae707/schema)
