Messaging

WhatsApp

Send WhatsApp messages from your own WhatsApp Business number via the Meta Cloud API, using the same API key as for email.

Before you start

  • Connect your WhatsApp Business Account (WABA) and phone number in the portal under WhatsApp. Until a number is connected, sends return 409 whatsapp_not_connected.
  • Use an API key with the whatsapp:send permission (or a key without permission restrictions).
  • Phone numbers are in E.164 format: a +, the country code and the number, without spaces, e.g. +60123456789.
  • Meta bills conversations directly to the WABA owner. WhatsApp messages do not consume your email quota.

The 24-hour customer service window

WhatsApp only allows free-form messages (type: "text") while the customer service window is open, i.e. within 24 hours of the last message the customer sent you. Outside the window the request is rejected with 422 outside_service_window and you must send an approved template instead. Templates can be sent at any time.

A common pattern: send a template first (for example an order update with a "Reply to chat" button). When the customer replies, the window opens and you can continue with free-form text for 24 hours.

Send a message

POST/v1/whatsapp/sendPermission whatsapp:send
ParameterTypeDescription
torequiredstringRecipient phone number in E.164, e.g. +60123456789.
typerequiredstring"template" or "text".
templateobjectRequired when type is template.
template.namerequiredstringName of an approved template, e.g. order_update.
template.languagerequiredstringTemplate language code, e.g. en_US.
template.parametersobjectValues for the template variables, in order: header, body and buttons, each an array of strings, e.g. {"body": ["A-1077", "RM 42.50"]} fills {{1}} and {{2}} of the body.
textobjectRequired when type is text. Only inside the 24-hour window.
text.bodyrequiredstringMessage text (up to 4096 characters).
text.preview_urlbooleanShow a link preview for the first URL in the text.

Template message

curl -X POST 'https://app.snappymails.com/v1/whatsapp/send' \
  -H "api-key: $SNAPPYMAILS_API_KEY" \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
    "to": "+60123456789",
    "type": "template",
    "template": {
      "name": "order_update",
      "language": "en_US",
      "parameters": {
        "body": [
          "A-1077",
          "RM 42.50"
        ]
      }
    }
  }'

Text message (inside the 24-hour window)

curl -X POST 'https://app.snappymails.com/v1/whatsapp/send' \
  -H "api-key: $SNAPPYMAILS_API_KEY" \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
    "to": "+60123456789",
    "type": "text",
    "text": {
      "body": "Your parcel is out for delivery: https://track.acme.com/A-1077",
      "preview_url": true
    }
  }'

201 Created

Response
{
  "messageId": "7d9f1a52-3c4b-4e8a-9f61-2b0c5d7e8a13",
  "status": "QUEUED"
}

The message is queued and sent to Meta asynchronously. Poll its status with the message endpoint below.

Errors

StatusCodeWhen
400invalid_parameterInvalid JSON, phone number not in E.164, unknown type, or missing template / text object.
401unauthorizedMissing or invalid API key.
403permission_deniedThe key lacks the whatsapp:send permission.
409whatsapp_not_connectedNo WhatsApp number is connected for this brand.
422outside_service_windowFree-form text outside the 24-hour window — send a template.
429too_many_requestsRate limit exceeded; wait Retry-After seconds.

Get a message

GET/v1/whatsapp/messages/{messageId}Permission whatsapp:send

Returns the message with its current status and timestamps. Unknown ids (or ids of another brand) return 404 document_not_found.

curl 'https://app.snappymails.com/v1/whatsapp/messages/7d9f1a52-3c4b-4e8a-9f61-2b0c5d7e8a13' \
  -H "api-key: $SNAPPYMAILS_API_KEY" \
  -H 'Accept: application/json'

200 OK

WaMessage
{
  "id": "7d9f1a52-3c4b-4e8a-9f61-2b0c5d7e8a13",
  "direction": "OUTBOUND",
  "wa_message_id": "wamid.HBgLNjAxMjM0NTY3ODkVAgARGBI1...",
  "phone": "+60123456789",
  "contact_id": null,
  "contact_name": "Jane Doe",
  "campaign_id": null,
  "type": "template",
  "text": null,
  "template": {
    "name": "order_update",
    "language": "en_US",
    "parameters": { "body": ["A-1077", "RM 42.50"] }
  },
  "media": null,
  "status": "READ",
  "error": null,
  "source": "api",
  "created_at": "2026-10-16T09:00:00Z",
  "sent_at": "2026-10-16T09:00:01Z",
  "delivered_at": "2026-10-16T09:00:03Z",
  "read_at": "2026-10-16T09:02:47Z",
  "failed_at": null
}
ParameterTypeDescription
idstring (uuid)SnappyMails message id (the messageId returned by send).
directionstringOUTBOUND or INBOUND.
wa_message_idstring | nullMeta's wamid, once the message was accepted by WhatsApp.
phonestringCustomer phone number (E.164).
contact_idstring | nullMatching contact in your audience, if any.
contact_namestring | nullContact or WhatsApp profile name.
campaign_idstring | nullBroadcast campaign that sent the message, if any.
typestringtext, template, image, document, audio, video, location, interactive, reaction or unknown.
textstring | nullText body for text messages.
templateobject | null{ name, language, parameters } for template messages.
mediaobject | null{ id, mime_type, caption } for inbound media.
statusstringQUEUED, SENT, DELIVERED, READ, FAILED or RECEIVED (inbound). See statuses.
errorobject | nullMeta error { code, title } when the status is FAILED, e.g. 131047 (re-engagement required).
sourcestringapi, portal, campaign or inbox.
created_atdate-timeWhen the message was created.
sent_at / delivered_at / read_at / failed_atdate-time | nullWhen each stage was reached.