Skip to content

Send a message

View .md
POST

Send one message. The body is the /v1 message body plus from and conversationId. There is no Zavu-Sender header.

Where it leaves from: conversationId if given, else from (an address), else the project default. See from for ambiguous addresses.

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.

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.

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.

Limits, risk review, pricing and verification are exactly /v1's: see POST /v1/messages.

Authorization

  • Authorizationheaderrequired

    Bearer YOUR_API_KEY. Create a key in the dashboard.

Body

application/json

  • tostring

    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.

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

  • channelstring

    auto, or a transport. auto applies the fixed order described on POST /v2/messages.

    One ofautosmssms_onewaywhatsapptelegramemailinstagrammessengervoice
  • messageTypestring

    Type of message. Defaults to 'text'.

    One oftextimagevideoaudiodocumentstickerlocationcontactbuttonslistcta_urlrequest_contact_infolocation_requestreactiontemplate
    Defaulttext
  • textstring

    Text body for text messages or caption for media messages.

  • contentobject

    Additional content for non-text message types.

    32 properties
    • mediaUrlstring

      URL of the media file (for image, video, audio, document, sticker).

    • mediaIdstring

      WhatsApp media ID if already uploaded.

    • mimeTypestring

      MIME type of the media.

    • filenamestring

      Filename for documents.

    • latitudenumber

      Latitude for location messages.

    • longitudenumber

      Longitude for location messages.

    • locationNamestring

      Name of the location.

    • locationAddressstring

      Address of the location.

    • contactsobject[]

      Contact cards for contact messages.

      2 properties
      • namestring
      • phonesstring[]
    • buttonsobject[]

      Interactive buttons (max 3).

      Limitsmax items 3
      2 properties
      • idstringrequired
        Limitsmax length 256
      • titlestringrequired
        Limitsmax length 20
    • listButtonstring

      Button text for list messages.

      Limitsmax length 20
    • sectionsobject[]

      Sections for list messages.

      2 properties
      • titlestringrequired
      • rowsobject[]required
        Limitsmax items 10
        3 properties
        • idstringrequired
        • titlestringrequired
          Limitsmax length 24
        • descriptionstring
          Limitsmax length 72
    • ctaDisplayTextstring

      Button label for cta_url messages.

      Limitsmax length 20
    • ctaUrlstring<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.

    • ctaHeaderTypestring

      Optional header type for cta_url messages.

      One oftextimagevideodocument
    • ctaHeaderTextstring

      Header text when ctaHeaderType is 'text'.

      Limitsmax length 60
    • ctaHeaderMediaUrlstring<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.

    • footerTextstring

      Optional footer text for cta_url messages.

      Limitsmax length 60
    • emojistring

      Emoji for reaction messages.

    • reactToMessageIdstring

      Message ID to react to.

    • replyToMessageIdstring

      Zavu message ID of the quoted message this message replies to.

      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.

      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.

    • replyToProviderMessageIdstring

      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.

    • replyToFromstring

      Sender of the quoted message (phone number in E.164 format, or the Telegram chat ID). Read-only.

    • replyToTextstring

      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.

    • replyToMessageTypestring

      Type of the quoted message (text, image, video, etc.). Read-only.

    • templateIdstring

      Template ID for template messages.

    • templateVariablesobject

      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.

      1 properties
      • {key}string
    • templateButtonVariablesobject

      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.

      1 properties
      • {key}string
    • templateHeaderVariablesobject

      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.

      1 properties
      • {key}string
    • templateHeaderMediaUrlstring<uri>

      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.

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

      Limitsmax length 2048
    • templateHeaderFilenamestring

      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.

      Limitsmin length 1 · max length 240
    • referralobject

      Click-to-WhatsApp (CTWA) ad attribution: where an inbound conversation came from.

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

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

      10 properties
      • sourceTypestring

        Where the click came from.

        One ofadpost
      • sourceIdstring

        Identifier of the ad or post that produced the click.

      • sourceUrlstring<uri>

        Meta permalink to the ad or post.

      • headlinestring

        Headline of the ad or post.

      • bodystring

        Body copy of the ad or post.

      • mediaTypestring

        Type of media on the ad, when it had any.

        One ofimagevideo
      • imageUrlstring<uri>

        Image of the ad. Present when mediaType is image.

      • videoUrlstring<uri>

        Video of the ad. Present when mediaType is video.

      • thumbnailUrlstring<uri>

        Thumbnail of the ad media.

      • ctwaClidstring

        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.

  • subjectstring

    Email subject line. Required when channel is 'email' or recipient is an email address.

    Limitsmax length 998
  • htmlBodystring

    HTML body for email messages. If provided, email will be sent as multipart with both text and HTML.

  • replyTostring<email>

    Reply-To email address for email messages.

  • idempotencyKeystring

    Optional idempotency key to avoid duplicate sends.

  • metadataobject

    Arbitrary metadata to associate with the message.

    1 properties
    • {key}string
  • fallbackEnabledboolean

    Whether to enable automatic fallback to SMS if WhatsApp fails. Defaults to true.

    Defaulttrue
  • voiceLanguagestring

    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.

  • attachmentsobject[]

    Email attachments. Only supported when channel is 'email'. Maximum 40MB total size.

    Limitsmax items 50
    5 properties
    • filenamestringrequired

      Name of the attached file.

      Limitsmax length 255
    • contentstring

      Content of the attached file as a Base64-encoded string.

    • pathstring<uri>

      URL where the attachment file is hosted. The server will fetch the file.

    • content_typestring

      MIME type of the attachment. If not set, will be derived from the filename.

    • content_idstring

      Content ID for inline images. Reference in HTML as <img src="cid:your_content_id">.

      Limitsmax length 100
  • fromstring

    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.

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

  • conversationIdstring

    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.

Responses

202Accepted. `message.status` is `queued`.
  • messageobjectrequired
    23 properties
    • idstringrequired
    • tostringrequired
    • fromstring

      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.

    • channelstringrequired

      A transport a message travels on.

      One ofsmssms_onewaywhatsapptelegramemailinstagrammessengervoice
    • directionstringrequired

      Who sent the message. Needed to render a thread: status cannot tell the two apart, because an inbound message is also stored as delivered.

      One ofinboundoutbound
    • messageTypestringrequired

      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.

      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.

      One oftextimagevideoaudiodocumentstickerlocationcontactbuttonslistcta_urlrequest_contact_infolocation_requestreactiontemplate
    • statusstringrequired
      One ofqueuedsendingsentdeliveredreadfailedreceivedpending_url_verification
    • textstring

      Text content or caption.

    • contentobject

      Content for non-text message types (WhatsApp and Telegram).

      32 properties
      • mediaUrlstring

        URL of the media file (for image, video, audio, document, sticker).

      • mediaIdstring

        WhatsApp media ID if already uploaded.

      • mimeTypestring

        MIME type of the media.

      • filenamestring

        Filename for documents.

      • latitudenumber

        Latitude for location messages.

      • longitudenumber

        Longitude for location messages.

      • locationNamestring

        Name of the location.

      • locationAddressstring

        Address of the location.

      • contactsobject[]

        Contact cards for contact messages.

        2 properties
        • namestring
        • phonesstring[]
      • buttonsobject[]

        Interactive buttons (max 3).

        Limitsmax items 3
        2 properties
        • idstringrequired
          Limitsmax length 256
        • titlestringrequired
          Limitsmax length 20
      • listButtonstring

        Button text for list messages.

        Limitsmax length 20
      • sectionsobject[]

        Sections for list messages.

        2 properties
        • titlestringrequired
        • rowsobject[]required
          Limitsmax items 10
          3 properties
          • idstringrequired
          • titlestringrequired
            Limitsmax length 24
          • descriptionstring
            Limitsmax length 72
      • ctaDisplayTextstring

        Button label for cta_url messages.

        Limitsmax length 20
      • ctaUrlstring<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.

      • ctaHeaderTypestring

        Optional header type for cta_url messages.

        One oftextimagevideodocument
      • ctaHeaderTextstring

        Header text when ctaHeaderType is 'text'.

        Limitsmax length 60
      • ctaHeaderMediaUrlstring<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.

      • footerTextstring

        Optional footer text for cta_url messages.

        Limitsmax length 60
      • emojistring

        Emoji for reaction messages.

      • reactToMessageIdstring

        Message ID to react to.

      • replyToMessageIdstring

        Zavu message ID of the quoted message this message replies to.

        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.

        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.

      • replyToProviderMessageIdstring

        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.

      • replyToFromstring

        Sender of the quoted message (phone number in E.164 format, or the Telegram chat ID). Read-only.

      • replyToTextstring

        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.

      • replyToMessageTypestring

        Type of the quoted message (text, image, video, etc.). Read-only.

      • templateIdstring

        Template ID for template messages.

      • templateVariablesobject

        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.

        1 properties
        • {key}string
      • templateButtonVariablesobject

        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.

        1 properties
        • {key}string
      • templateHeaderVariablesobject

        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.

        1 properties
        • {key}string
      • templateHeaderMediaUrlstring<uri>

        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.

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

        Limitsmax length 2048
      • templateHeaderFilenamestring

        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.

        Limitsmin length 1 · max length 240
      • referralobject

        Click-to-WhatsApp (CTWA) ad attribution: where an inbound conversation came from.

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

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

        10 properties
        • sourceTypestring

          Where the click came from.

          One ofadpost
        • sourceIdstring

          Identifier of the ad or post that produced the click.

        • sourceUrlstring<uri>

          Meta permalink to the ad or post.

        • headlinestring

          Headline of the ad or post.

        • bodystring

          Body copy of the ad or post.

        • mediaTypestring

          Type of media on the ad, when it had any.

          One ofimagevideo
        • imageUrlstring<uri>

          Image of the ad. Present when mediaType is image.

        • videoUrlstring<uri>

          Video of the ad. Present when mediaType is video.

        • thumbnailUrlstring<uri>

          Thumbnail of the ad media.

        • ctwaClidstring

          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.

    • conversationIdstring

      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.

    • providerMessageIdstring

      Message ID from the delivery provider.

    • errorCodestringnullable

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

    • errorMessagestringnullable
    • costnumbernullable

      Zavu's own per-message platform charge, in USD (6 decimals). Messaging is billed against the plan's allowance, so it is often 0.

    • costProvidernumbernullable

      What delivering the message cost Zavu (carrier cost), in USD. Filled in when the delivery report brings it, so null until then.

    • costTotalnumbernullable

      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.

    • metadataobject
      1 properties
      • {key}string
    • createdAtstring<date-time>required
    • updatedAtstring<date-time>
    • attemptedChannelsstring[]

      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.

      One ofsmssms_onewaywhatsapptelegramemailinstagrammessengervoice
    • riskHoldstringnullable

      held while the message waits for risk review (it stays queued), released once a reviewer let it go, null when it was never held.

      One ofheldreleasednull
    • errorDetailsobjectnullable

      Present when errorCode translates a delivery provider's code.

      1 properties
      • providerCodestring

        The provider's own code, unchanged.

    • chargednumbernullable

      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.

  • routingobjectrequired

    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.

    4 properties
    • channelstringrequired

      A transport a message travels on.

      One ofsmssms_onewaywhatsapptelegramemailinstagrammessengervoice
    • fromstringrequired

      The address the message leaves from, as GET /v2/channels lists it for channel. With a test-mode key, the WhatsApp sandbox number.

    • reasonstringrequired

      Why this transport was chosen. Closed list: - explicit: channel was given. - 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. - conversation: the sender came from conversationId (the sender, not the transport; the transport is then chosen as for auto_order unless channel is given). - recipient_type: to is an email address, so email. - message_type: messageType is WhatsApp-only (media, interactive, template...), so WhatsApp. - sandbox: test-mode key, which always uses the WhatsApp sandbox. An automatic fallback is never a send-time reason: it happens later, see Message.attemptedChannels.

      One ofexplicitauto_orderconversationrecipient_typemessage_typesandbox
    • estimatedCostnumberrequirednullable

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

400Invalid 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`.
  • codestringrequired
  • messagestringrequired
  • detailsobject

    Machine-readable context. Keys that mean the same thing everywhere: - 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"]). - recommendation: one sentence saying what to do next. - dashboardUrl: the dashboard screen that fixes it. - 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). - address / from: the address the error is about, under the name the request used for it.

    1 properties
    • errorsobject[]
      3 properties
      • pathobject[]required
      • messagestringrequired
      • codestring
401Unauthorized.
  • codestringrequired
  • messagestringrequired
  • detailsobject

    Machine-readable context. Keys that mean the same thing everywhere: - 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"]). - recommendation: one sentence saying what to do next. - dashboardUrl: the dashboard screen that fixes it. - 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). - address / from: the address the error is about, under the name the request used for it.

    1 properties
    • errorsobject[]
      3 properties
      • pathobject[]required
      • messagestringrequired
      • codestring
402`insufficient_balance`: the balance cannot cover the SMS or email.
  • codestringrequired
  • messagestringrequired
  • detailsobject

    Machine-readable context. Keys that mean the same thing everywhere: - 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"]). - recommendation: one sentence saying what to do next. - dashboardUrl: the dashboard screen that fixes it. - 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). - address / from: the address the error is about, under the name the request used for it.

    1 properties
    • errorsobject[]
      3 properties
      • pathobject[]required
      • messagestringrequired
      • codestring
403Refused 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`.
  • codestringrequired
  • messagestringrequired
  • detailsobject

    Machine-readable context. Keys that mean the same thing everywhere: - 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"]). - recommendation: one sentence saying what to do next. - dashboardUrl: the dashboard screen that fixes it. - 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). - address / from: the address the error is about, under the name the request used for it.

    1 properties
    • errorsobject[]
      3 properties
      • pathobject[]required
      • messagestringrequired
      • codestring
404`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.
  • codestringrequired
  • messagestringrequired
  • detailsobject

    Machine-readable context. Keys that mean the same thing everywhere: - 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"]). - recommendation: one sentence saying what to do next. - dashboardUrl: the dashboard screen that fixes it. - 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). - address / from: the address the error is about, under the name the request used for it.

    1 properties
    • errorsobject[]
      3 properties
      • pathobject[]required
      • messagestringrequired
      • codestring
409`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.
  • codestringrequired
  • messagestringrequired
  • detailsobject

    Machine-readable context. Keys that mean the same thing everywhere: - 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"]). - recommendation: one sentence saying what to do next. - dashboardUrl: the dashboard screen that fixes it. - 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). - address / from: the address the error is about, under the name the request used for it.

    1 properties
    • errorsobject[]
      3 properties
      • pathobject[]required
      • messagestringrequired
      • codestring
422`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.
  • codestringrequired
  • messagestringrequired
  • detailsobject

    Machine-readable context. Keys that mean the same thing everywhere: - 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"]). - recommendation: one sentence saying what to do next. - dashboardUrl: the dashboard screen that fixes it. - 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). - address / from: the address the error is about, under the name the request used for it.

    1 properties
    • errorsobject[]
      3 properties
      • pathobject[]required
      • messagestringrequired
      • codestring
429`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`.
  • codestringrequired
  • messagestringrequired
  • detailsobject

    Machine-readable context. Keys that mean the same thing everywhere: - 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"]). - recommendation: one sentence saying what to do next. - dashboardUrl: the dashboard screen that fixes it. - 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). - address / from: the address the error is about, under the name the request used for it.

    1 properties
    • errorsobject[]
      3 properties
      • pathobject[]required
      • messagestringrequired
      • codestring