Messaging
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:sendpermission (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| Parameter | Type | Description |
|---|---|---|
torequired | string | Recipient phone number in E.164, e.g. +60123456789. |
typerequired | string | "template" or "text". |
template | object | Required when type is template. |
template.namerequired | string | Name of an approved template, e.g. order_update. |
template.languagerequired | string | Template language code, e.g. en_US. |
template.parameters | object | Values 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. |
text | object | Required when type is text. Only inside the 24-hour window. |
text.bodyrequired | string | Message text (up to 4096 characters). |
text.preview_url | boolean | Show 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
| Status | Code | When |
|---|---|---|
400 | invalid_parameter | Invalid JSON, phone number not in E.164, unknown type, or missing template / text object. |
401 | unauthorized | Missing or invalid API key. |
403 | permission_denied | The key lacks the whatsapp:send permission. |
409 | whatsapp_not_connected | No WhatsApp number is connected for this brand. |
422 | outside_service_window | Free-form text outside the 24-hour window — send a template. |
429 | too_many_requests | Rate limit exceeded; wait Retry-After seconds. |
Get a message
GET
/v1/whatsapp/messages/{messageId}Permission whatsapp:sendReturns 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
}| Parameter | Type | Description |
|---|---|---|
id | string (uuid) | SnappyMails message id (the messageId returned by send). |
direction | string | OUTBOUND or INBOUND. |
wa_message_id | string | null | Meta's wamid, once the message was accepted by WhatsApp. |
phone | string | Customer phone number (E.164). |
contact_id | string | null | Matching contact in your audience, if any. |
contact_name | string | null | Contact or WhatsApp profile name. |
campaign_id | string | null | Broadcast campaign that sent the message, if any. |
type | string | text, template, image, document, audio, video, location, interactive, reaction or unknown. |
text | string | null | Text body for text messages. |
template | object | null | { name, language, parameters } for template messages. |
media | object | null | { id, mime_type, caption } for inbound media. |
status | string | QUEUED, SENT, DELIVERED, READ, FAILED or RECEIVED (inbound). See statuses. |
error | object | null | Meta error { code, title } when the status is FAILED, e.g. 131047 (re-engagement required). |
source | string | api, portal, campaign or inbox. |
created_at | date-time | When the message was created. |
sent_at / delivered_at / read_at / failed_at | date-time | null | When each stage was reached. |