WhatsApp

Answer WhatsApp in a mailbox

Connect a WhatsApp number to a mailbox so a person or an agent reads customer messages and replies in the same thread.

A mailbox can own a WhatsApp number next to its email addresses. Customer messages to that number arrive in the mailbox as threads, and your people, your app, or an agent reply from the same place they answer email. Replies are governed sends: the mailbox's grants, policies, and approvals apply to them exactly as they do to email.

Add the number to a mailbox

The number must be one your organization has connected through the business account flow. Add it as an endpoint on an existing mailbox, or pass it as the endpoint of a new one. address is the number in E.164 form, or a national number in your organization's default region. displayName is optional.

curl -X POST https://api.samva.dev/v1/mailboxes/mbx_.../endpoints \
  -H "X-API-Key: samva_sk_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "whatsapp",
    "address": "+16175550123",
    "displayName": "Acme Support"
  }'

The response is the mailbox. The new endpoint carries channel: "whatsapp" and the businessAccountId that owns the number. A number the organization has not connected is refused, and so is one another mailbox already holds (409). A mailbox sends WhatsApp from the number its thread belongs to, so connect the number before you expect replies to leave.

What arrives

A customer's messages to the number join one thread per customer number. The thread is the conversation between that customer and your number, however many messages it holds. Each arrival publishes mailbox.message.received to your webhooks and to the mailbox event stream, with the same envelope an email carries. message.channel is whatsapp, message.from.address is the customer's number, message.subject is null, and message.text is the text or a media caption. The mailbox events reference lists the full notification body.

Reading the message through the API returns the chat content: text, media metadata (type, content type, file name, caption), the template name and language when the message was sent from a template, and contextMessageId when the customer quoted or reacted to an earlier message. There is no subject or headers. WhatsApp messages are not screened for quarantine; screening applies to email only.

Reading a thread also returns whatsappServiceWindow, { open, expiresAt }, or null when the thread is not a WhatsApp thread. The window opens and refreshes when the customer writes to you. See the 24-hour service window.

Reply inside the window

While the window is open, reply with free-form text. The reply goes to the customer's number from the number the thread belongs to.

curl -X POST https://api.samva.dev/v1/mailboxes/mbx_.../messages/msg_.../reply \
  -H "X-API-Key: samva_sk_live_your_api_key" \
  -H "Idempotency-Key: order-123-reply" \
  -H "Content-Type: application/json" \
  -d '{ "channel": "whatsapp", "text": "Your order shipped today." }'

The response is a send receipt, the same one email returns. Follow it with the mailbox action and read the outcome there; the sent message appears in the thread and publishes mailbox.message.sent. A send passes through the same consent and suppression checks as POST /v1/messages, so a number that opted out is not messaged from a mailbox either.

Reply with a template

A template sends whether or not the window is open, and it is how you reach a customer after the window closes. Pass the approved template's templateId and its templateData instead of text. A request carries exactly one of text and templateId.

curl -X POST https://api.samva.dev/v1/mailboxes/mbx_.../messages/msg_.../reply \
  -H "X-API-Key: samva_sk_live_your_api_key" \
  -H "Idempotency-Key: order-123-followup" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "whatsapp",
    "templateId": "tmpl_...",
    "templateData": { "firstName": "Alice", "orderNumber": "ORD-12345" }
  }'

When the window is closed

Free-form text outside the window is refused with 422 WhatsAppServiceWindowClosedError. The error names the remedy, sending an approved template, and carries closedAt, which is null when the customer has never written to the number. Nothing is converted for you and nothing is sent.

{
  "_tag": "WhatsAppServiceWindowClosedError",
  "message": "The 24-hour service window +16175559876 opened with +16175550123 closed at 2026-10-16T09:12:00.000Z. Free-form WhatsApp text needs an open window: send an approved template with `templateId` (and its `templateData`) instead.",
  "closedAt": "2026-10-16T09:12:00.000Z"
}

Check whatsappServiceWindow on the thread before composing free-form text, then fall back to a template when open is false.

Start a conversation or save a draft

POST /v1/mailboxes/{id}/messages starts a conversation from the mailbox's default WhatsApp endpoint. It takes channel: "whatsapp", one to number, and either text or templateId. A number the mailbox already has a thread with continues that thread. A first message to someone who has not written to you is outside any window, so it must be a template.

curl -X POST https://api.samva.dev/v1/mailboxes/mbx_.../messages \
  -H "X-API-Key: samva_sk_live_your_api_key" \
  -H "Idempotency-Key: order-123-start" \
  -H "Content-Type: application/json" \
  -d '{ "channel": "whatsapp", "to": "+16175559876", "templateId": "tmpl_..." }'

A WhatsApp draft holds channel, an optional to, optional text, and inReplyToMessageId when it answers a message. Subject, cc, bcc, and attachments belong to email, and a forward is an email operation. Send media and interactive messages with POST /v1/messages.

Until the channel is enabled for your organization, adding a WhatsApp endpoint or sending on one is refused with 403 FlagDisabledError.

Let an agent reply

The hosted MCP server's mailbox tools carry WhatsApp threads. mailbox_thread_get returns each message's chat content and the thread's whatsappServiceWindow. mailbox_reply takes text or a templateId with templateData, and mailbox_send starts a conversation with one phone number in to. Inbound text is untrusted, exactly like email. A mailbox-scoped key with a decision rule setting decide: "person" keeps a person in the loop.

Next steps

Related documentation

On this page