Sending Messages

All message types use a single endpoint. The type field determines the payload shape.

Endpoint: POST /v1/phones/{phoneId}/messages

Required headers: Authorization: Bearer <API_KEY> and Content-Type: application/json — nothing else.


Common Fields

Field Type Required Description
type string Yes Message type (see sections below)
to string Yes Recipient phone number, legacy WhatsApp ID, or BSUID. When replying to a webhook, use data.contactReplyTo.

For existing phone-based integrations, to can still be a phone number with country code. For BSUID/usernames support, do not validate this field as digits-only. See WhatsApp Identities and BSUID.

Phone number format: digits only with country code, no + (e.g. 5491155551234). The API applies no normalization — the value is passed to Meta as-is, so use the contact's WhatsApp ID (wa_id) exactly as WhatsApp reports it. When replying to a webhook, that is data.contactReplyTo. Argentine numbers include the mobile 9 (549...); Mexican numbers include the 1 (521...).

Response

All message types return the same response:

{
  "data": {
    "messageId": null,
    "wamid": "wamid.HBgLNTQ5MTE1NTU..."
  }
}

messageId is always null in this response — correlate the send with webhooks and the messages API through the wamid. A 200 with a wamid means Meta accepted the message, not that it was delivered; the final state (delivered, read, or failed) arrives via the message.status_updated webhook event. See Error Handling below.


Text Message

{
  "type": "text",
  "to": "5491155551234",
  "text": "Hello, how can I help you?",
  "previewUrl": false
}
Field Type Required Description
text string Yes Message body
previewUrl boolean No Enable URL preview (default: false)

Media Messages (Image, Video, Audio, Document, Sticker)

Send media by URL or by pre-uploaded media ID. All five media types accept either — there is no type that requires a pre-upload.

Image by URL:

{
  "type": "image",
  "to": "5491155551234",
  "mediaUrl": "https://example.com/photo.jpg",
  "caption": "Check out this photo!"
}

Document by URL (no pre-upload needed — ideal for forwarding files you already host):

{
  "type": "document",
  "to": "5491155551234",
  "mediaUrl": "https://example.com/coupons/coupon-1234.pdf",
  "filename": "coupon-1234.pdf",
  "caption": "Here is your coupon"
}

Document by pre-uploaded media ID:

{
  "type": "document",
  "to": "5491155551234",
  "mediaId": "1234567890",
  "caption": "Monthly report",
  "filename": "report-march-2026.pdf"
}

When using mediaUrl, the file is downloaded by Meta's servers, not by this API. The URL must be publicly reachable over HTTPS — no authentication, no localhost/private IPs. If the URL requires auth, upload the file first and send by mediaId instead.

Field Type Required Description
type string Yes "image", "video", "audio", "document", or "sticker"
mediaId string No* WhatsApp media ID (from upload endpoint)
mediaUrl string No* Public URL to the media file
caption string No Caption text (image, video, document only)
filename string No Filename (document only)

*Either mediaId or mediaUrl must be provided, but not both.

Uploading Media

POST /v1/phones/{phoneId}/media with Content-Type: multipart/form-data and a file field.

Returns { "data": { "id": "1234567890" } }

The media type and constraints are derived from the file's MIME type. Supported formats and size limits:

Type Accepted formats Max size
Image JPEG, PNG 5MB
Video MP4, 3GPP 16MB
Audio AAC, AMR, MP3 (audio/mpeg), MP4 (audio/mp4), OGG with Opus codec (audio/ogg; codecs=opus) 16MB
Document any 100MB
Sticker WebP (static 500KB / animated 100KB)

Audio contract

The API does not transcode audio — the uploaded file is validated and forwarded to WhatsApp as-is. The caller is responsible for sending a WhatsApp-compatible format:


Template Message

{
  "type": "template",
  "to": "5491155551234",
  "templateName": "order_confirmation",
  "languageCode": "en_US",
  "parameters": {
    "header": [
      { "type": "text", "text": "Order #1234" }
    ],
    "body": [
      { "type": "text", "text": "John" },
      { "type": "text", "text": "$99.00" }
    ],
    "buttons": [
      {
        "type": "button",
        "subType": "url",
        "index": 0,
        "parameters": [
          { "type": "text", "text": "order-1234" }
        ]
      }
    ]
  }
}
Field Type Required Description
templateName string Yes Template name as registered in Meta
languageCode string Yes Language code (e.g. "en_US", "es_AR")
parameters object No Header, body, and button parameters

Interactive Button Message

{
  "type": "interactive",
  "to": "5491155551234",
  "interactiveType": "button",
  "body": "How would you like to proceed?",
  "header": { "type": "text", "text": "Choose an option" },
  "footer": "Reply within 24 hours",
  "buttons": [
    { "id": "btn_buy", "title": "Buy Now" },
    { "id": "btn_info", "title": "More Info" },
    { "id": "btn_cancel", "title": "Cancel" }
  ]
}
Field Type Required Description
interactiveType string Yes Must be "button"
body string Yes Message body text
header object No { type: "text", text: string } (text-only headers)
footer string No Footer text
buttons array Yes 1-3 buttons: { id: string, title: string } (title max 20 chars)

When the contact taps a button, you receive a message.received webhook with type: "interactive" and content.buttonId / content.buttonTitle matching the tapped button — see the Webhooks guide.


Interactive List Message

{
  "type": "interactive",
  "to": "5491155551234",
  "interactiveType": "list",
  "body": "Browse our catalog:",
  "buttonText": "View Products",
  "sections": [
    {
      "title": "Electronics",
      "rows": [
        { "id": "phone_1", "title": "Smartphone X", "description": "Latest model" },
        { "id": "laptop_1", "title": "Laptop Pro", "description": "16-inch display" }
      ]
    }
  ]
}
Field Type Required Description
interactiveType string Yes Must be "list"
body string Yes Message body text
buttonText string Yes Text on the list button (max 20 chars)
sections array Yes At least 1 section with title and rows

When the contact picks a row, you receive a message.received webhook with type: "interactive" and content.listId / content.listTitle matching the selected row.


CTA URL Message

{
  "type": "cta_url",
  "to": "5491155551234",
  "bodyText": "Visit our store for exclusive deals",
  "buttonText": "Open Store",
  "url": "https://store.example.com",
  "headerText": "Special Offer",
  "footerText": "Offer expires soon"
}

Flow Message

Before sending a Flow, create and publish it through the Flows guide.

{
  "type": "flow",
  "to": "5491155551234",
  "bodyText": "Complete your registration",
  "flowToken": "token_abc123",
  "flowId": "1234567890",
  "flowCta": "Start Registration",
  "flowAction": "navigate",
  "screen": "WELCOME_SCREEN",
  "data": { "userId": "usr_123" }
}

Reaction Message

React to a previously sent or received message with an emoji.

{
  "type": "reaction",
  "to": "5491155551234",
  "messageId": "550e8400-e29b-41d4-a716-446655440000",
  "emoji": "👍"
}

messageId is the internal message UUID (the id from the messages list), not the WhatsApp wamid — the API resolves it to the wamid for you. Send an empty emoji ("") to remove a reaction you previously applied. Reactions are not standalone messages: they are aggregated onto the target message's reactions[] field (see Conversations & Messages).


Mark Message as Read

POST /v1/phones/{phoneId}/messages/{messageId}/read

messageId is the internal message UUID (the id from the messages list) of an inbound message. This marks the message read on WhatsApp and updates the local status to read. WhatsApp does not send a status webhook back when you mark an inbound message as read, so this endpoint is the only place that read state is recorded.

{ "showTypingIndicator": false }

Delete a Message

DELETE /v1/phones/{phoneId}/messages/{messageId}

Soft-deletes a message locally by its internal UUID so it no longer appears in message listings. Restricted to organization API keys (shared inbox links cannot delete messages). Returns { "data": { "success": true } }.

Delete for everyone is not possible. The WhatsApp Cloud API provides no endpoint to delete, unsend, or recall a message that was already sent — Meta does not support it. This endpoint is a local "delete for me" only: the message is hidden from your own data, but the recipient still sees it on their device.


The 24-Hour Customer Service Window

WhatsApp only allows free-form messages (text, media, interactive, CTA URL, flows) within 24 hours of the contact's last inbound message. Outside that window, only pre-approved template messages are accepted. This is Meta's standard Cloud API rule — the API forwards your send to Meta and surfaces Meta's verdict; it does not pre-validate the window locally.

Check the window before sending: conversation responses expose lastInboundMessageAt, serviceWindowExpiresAt, and isServiceWindowOpen, and message webhook events carry lastInboundMessageAt / serviceWindowExpiresAt — use them to choose between a free-form message and a template without waiting for Meta's rejection. See Conversations — The 24-Hour Service Window. These fields are computed from local data; Meta remains the source of truth (Click-to-WhatsApp ad conversations have a longer 72h window).


Error Handling

Synchronous errors (HTTP response)

When Meta rejects a send immediately, the endpoint returns HTTP 400 with code MESSAGE_SEND_FAILED. The raw Meta error JSON is embedded in error.message after the WhatsApp API Error: prefix:

{
  "error": {
    "message": "WhatsApp API Error: {\"error\":{\"message\":\"(#131047) Re-engagement message\",\"type\":\"OAuthException\",\"code\":131047,\"error_data\":{\"messaging_product\":\"whatsapp\",\"details\":\"Message failed to send because more than 24 hours have passed since the customer last replied to this number.\"},\"fbtrace_id\":\"AbCdEfGh123\"}}",
    "code": "MESSAGE_SEND_FAILED"
  }
}

To read Meta's error code and details, strip the WhatsApp API Error: prefix and parse the remaining JSON (error.code, error.error_data.details).

Other 400 responses you may see:

Asynchronous failures (webhook)

A 200 response with a wamid means Meta accepted the message — not that it was delivered. Some failures only surface afterwards (window expired, undeliverable recipient, spam limits). They arrive as message.status_updated events with data.status: "failed" and the raw Meta error in data.error:

{
  "event": "message.status_updated",
  "data": {
    "status": "failed",
    "wamid": "wamid.HBgLNTQ5MTE1NTU...",
    "error": {
      "code": 131047,
      "title": "Re-engagement message",
      "error_data": {
        "details": "Message failed to send because more than 24 hours have passed since the customer last replied to this number."
      }
    }
  }
}

If delivery matters to your integration, always subscribe to message.status_updated and treat data.status: "failed" as the source of truth.

Common Meta Error Codes

Code Meaning Typical fix
131047 Outside the 24-hour customer service window Send a template message instead
131026 Message undeliverable (recipient not on WhatsApp, or hasn't accepted new ToS) Verify the number is an active WhatsApp user
131056 Pair rate limit — too many messages to the same recipient in a short period Throttle per-recipient sends; retry after a pause
131048 Spam rate limit — sending paused due to quality Reduce volume; check quality rating via phone health
131049 Meta chose not to deliver (per-user marketing message limits) Do not retry immediately
100 Invalid parameter (malformed to, bad template params, unreachable mediaUrl...) Fix the request payload
133010 Phone not registered on Cloud API Re-register via POST /v1/phones/{phoneId}/registration
80007 WABA rate limit reached Slow down overall throughput

Rate Limits

The API adds no rate limiting, queueing, or per-recipient locking of its own — requests are forwarded to Meta as they arrive, and there is no 409-style error when two messages target the same recipient simultaneously. Meta's own limits are the ones that apply:

Meta rate-limit rejections surface like any other Meta error: synchronously as a 400 MESSAGE_SEND_FAILED or asynchronously as a failed status update. If your automation can burst multiple messages to one recipient, add a small delay between consecutive sends to the same number.