> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zavu.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Send a message

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



## OpenAPI

````yaml /openapi.json post /v1/messages
openapi: 3.0.3
info:
  title: Zavu Unified Messaging Layer API
  version: 0.2.0
  description: >
    Unified multi-channel messaging API for Zavu.


    Supported channels:

    - **SMS**: Simple text messages

    - **WhatsApp**: Rich messaging with media, buttons, lists, CTA URL buttons,
    location requests, and templates

    - **Telegram**: Bot messaging with text, media, and interactive elements

    - **Email**: Transactional emails via Amazon SES


    Design goals:

    - Simple `send()` entrypoint for developers

    - Project-level authentication via Bearer token

    - Support for all WhatsApp message types (text, image, video, audio,
    document, sticker, location, contact, buttons, list, cta_url,
    location_request, reaction, template)

    - If a non-text message type is sent, WhatsApp channel is used automatically

    - 24-hour WhatsApp conversation window enforcement

    - Universal `to` field accepts phone numbers (E.164), email addresses, or
    numeric chat IDs (Telegram/Instagram/Messenger)
servers:
  - url: https://api.zavu.dev
security:
  - bearerAuth: []
paths:
  /v1/messages:
    post:
      summary: Send a message
      description: >-
        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`.
      operationId: sendMessage
      parameters:
        - $ref: '#/components/parameters/SenderHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MessageRequest'
            examples:
              sms:
                summary: Simple SMS
                value:
                  to: '+56912345678'
                  text: Your verification code is 123456
              whatsapp_text:
                summary: WhatsApp text message
                value:
                  to: '+56912345678'
                  channel: whatsapp
                  text: Hello from Zavu!
              whatsapp_image:
                summary: WhatsApp image
                value:
                  to: '+56912345678'
                  messageType: image
                  text: Check out this product!
                  content:
                    mediaUrl: https://example.com/product.jpg
              whatsapp_buttons:
                summary: WhatsApp interactive buttons
                value:
                  to: '+56912345678'
                  messageType: buttons
                  text: How would you rate your experience?
                  content:
                    buttons:
                      - id: great
                        title: Great!
                      - id: okay
                        title: It was okay
                      - id: poor
                        title: Not good
              whatsapp_template:
                summary: WhatsApp template message
                value:
                  to: '+56912345678'
                  messageType: template
                  content:
                    templateId: tmpl_abc123
                    templateVariables:
                      '1': John
                      '2': ORD-12345
              whatsapp_template_with_url_button:
                summary: WhatsApp template with dynamic URL button
                value:
                  to: '+56912345678'
                  messageType: template
                  content:
                    templateId: tmpl_abc123
                    templateVariables:
                      '1': ORDAZ BRAVO MARCO
                      '2': 'REPORTE: Conducta inadecuada'
                    templateButtonVariables:
                      '0': abc-report-token
              whatsapp_cta_url:
                summary: WhatsApp CTA URL button
                value:
                  to: '+56912345678'
                  channel: whatsapp
                  messageType: cta_url
                  text: Tap the button below to see available dates.
                  content:
                    ctaDisplayText: See Dates
                    ctaUrl: https://example.com/schedule?ref=zavu
                    ctaHeaderType: image
                    ctaHeaderMediaUrl: https://example.com/banner.png
                    footerText: Dates subject to change.
              whatsapp_location_request:
                summary: WhatsApp location request
                value:
                  to: '+56912345678'
                  channel: whatsapp
                  messageType: location_request
                  text: To finish your order, share the delivery address.
              whatsapp_request_contact_info:
                summary: WhatsApp contact info request (ask for phone number)
                value:
                  to: US.13491208655302741918
                  channel: whatsapp
                  messageType: request_contact_info
                  text: Share your phone number so our team can call you back.
              whatsapp_to_bsuid:
                summary: WhatsApp message to a BSUID (username contact)
                value:
                  to: US.13491208655302741918
                  channel: whatsapp
                  text: Thanks for reaching out! How can we help?
              telegram_text:
                summary: Telegram text message
                value:
                  to: '123456789'
                  channel: telegram
                  text: Hello from Zavu via Telegram!
              telegram_template:
                summary: Telegram template message
                value:
                  to: '123456789'
                  channel: telegram
                  messageType: template
                  content:
                    templateId: tmpl_abc123
                    templateVariables:
                      '1': John
                      '2': ORD-12345
              sms_template:
                summary: SMS template message
                value:
                  to: '+56912345678'
                  channel: sms
                  messageType: template
                  content:
                    templateId: tmpl_abc123
                    templateVariables:
                      '1': John
                      '2': ORD-12345
              instagram_text:
                summary: Instagram text message
                value:
                  to: '17841400000000000'
                  channel: instagram
                  text: Hello from Zavu via Instagram!
              messenger_text:
                summary: Messenger text message (Facebook Page / Marketplace chat)
                value:
                  to: '24025631120151183'
                  channel: messenger
                  text: Hello from Zavu via Messenger!
              voice_tts:
                summary: Voice text-to-speech message
                value:
                  to: '+56912345678'
                  channel: voice
                  text: Your verification code is 1 2 3 4 5 6
              voice_tts_spanish:
                summary: Voice TTS in Spanish
                value:
                  to: '+56912345678'
                  channel: voice
                  text: Su codigo de verificacion es 1 2 3 4 5 6
                  voiceLanguage: es-ES
              email_simple:
                summary: Simple email
                value:
                  to: user@example.com
                  channel: email
                  subject: Your order has shipped
                  text: >-
                    Hi John, your order #12345 has shipped and will arrive in
                    2-3 business days.
              email_html:
                summary: Email with HTML body
                value:
                  to: user@example.com
                  channel: email
                  subject: Welcome to Zavu
                  text: Welcome to Zavu! We're excited to have you.
                  htmlBody: >-
                    <h1>Welcome to Zavu!</h1><p>We're excited to have you on
                    board.</p>
                  replyTo: support@example.com
              email_attachment:
                summary: Email with attachments
                value:
                  to: user@example.com
                  channel: email
                  subject: Your invoice
                  text: Please find your invoice attached.
                  attachments:
                    - filename: invoice.pdf
                      content: JVBERi0xLjQK...
                      content_type: application/pdf
              email_inline_image:
                summary: Email with inline image
                value:
                  to: user@example.com
                  channel: email
                  subject: Your report
                  text: See your report below.
                  htmlBody: <h1>Report</h1><img src="cid:chart"><p>Details above.</p>
                  attachments:
                    - filename: chart.png
                      path: https://example.com/chart.png
                      content_type: image/png
                      content_id: chart
      responses:
        '202':
          description: Message accepted for delivery.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'
        '400':
          description: Invalid request or WhatsApp 24h window not open.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                window_closed:
                  summary: WhatsApp window closed
                  value:
                    code: whatsapp_window_closed
                    message: >-
                      WhatsApp 24-hour window is not open. Use a template
                      message or wait for user to message first.
        '401':
          description: Unauthorized.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '402':
          description: >-
            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.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: insufficient_balance
                message: >-
                  Insufficient balance to send email. Add funds from the
                  dashboard to resume.
        '403':
          description: Message blocked due to URL restrictions.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                url_not_verified:
                  summary: URL not verified
                  value:
                    code: url_not_verified
                    message: >-
                      Message blocked: Contains unverified URLs. Submit URLs for
                      verification via the /v1/urls endpoint.
                    details:
                      urls:
                        - https://example.com
                      submissionEndpoint: /v1/urls
                url_shortener_blocked:
                  summary: URL shortener blocked
                  value:
                    code: url_shortener_blocked
                    message: >-
                      Message blocked: URL shorteners are not allowed. Please
                      use the full destination URL.
                    details:
                      blockedUrls:
                        - https://bit.ly/abc123
                      reason: url_shorteners_hidden_destination
        '404':
          description: Template or sender not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: Idempotency conflict (message already sent).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Server error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security:
        - bearerAuth: []
components:
  parameters:
    SenderHeader:
      name: Zavu-Sender
      in: header
      required: false
      description: >-
        Optional sender profile ID. If omitted, the project's default sender
        will be used.
      schema:
        type: string
        example: sender_12345
  schemas:
    MessageRequest:
      type: object
      description: Request body to send a message.
      required:
        - to
      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 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.
          example: '+56912345678'
        channel:
          $ref: '#/components/schemas/Channel'
          description: >-
            Delivery channel. Use 'auto' for intelligent routing. If omitted,
            channel is auto-selected based on sender capabilities and recipient
            type. For email recipients, defaults to 'email'.
          default: auto
        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'
    MessageResponse:
      type: object
      required:
        - message
      properties:
        message:
          $ref: '#/components/schemas/Message'
    Error:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: string
          example: invalid_request
        message:
          type: string
          example: Phone number is invalid
        details:
          type: object
          additionalProperties: true
    Channel:
      type: string
      description: Delivery channel. Use 'auto' for intelligent routing.
      enum:
        - auto
        - sms
        - sms_oneway
        - whatsapp
        - telegram
        - email
        - instagram
        - messenger
        - voice
    MessageType:
      type: string
      description: >-
        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.
      enum:
        - text
        - image
        - video
        - audio
        - document
        - sticker
        - location
        - contact
        - buttons
        - list
        - cta_url
        - request_contact_info
        - location_request
        - reaction
        - template
    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.
            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) of the quoted message. Present
            whenever an inbound message is a reply, even if the quoted message
            is not stored in Zavu.
        replyToFrom:
          type: string
          description: Sender of the quoted message (phone number in E.164 format).
        replyToText:
          type: string
          description: >-
            Truncated snippet of the quoted message's text, for display. Empty
            when the quoted message has no text (e.g. media).
        replyToMessageType:
          type: string
          description: Type of the quoted message (text, image, video, etc.).
        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.


            **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.
          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
    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
    Message:
      type: object
      required:
        - id
        - to
        - status
        - channel
        - messageType
        - createdAt
      properties:
        id:
          type: string
          example: jd7x2k3m4n5p6q7r8s9t0
        to:
          type: string
          example: '+56912345678'
        from:
          type: string
          example: '+13125551212'
        senderId:
          type: string
          example: sender_12345
        channel:
          $ref: '#/components/schemas/Channel'
        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
        errorMessage:
          type: string
          nullable: true
        cost:
          type: number
          nullable: true
          description: >-
            Zavu platform charge in USD for this message. Messaging is billed
            against your plan's monthly limits plus usage-based overage.
        costProvider:
          type: number
          nullable: true
          description: Carrier and delivery cost in USD.
        costTotal:
          type: number
          nullable: true
          description: Total cost in USD (platform charge + delivery cost).
        metadata:
          type: object
          additionalProperties:
            type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    MessageStatus:
      type: string
      enum:
        - queued
        - sending
        - sent
        - delivered
        - read
        - failed
        - received
        - pending_url_verification
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

````