Conversations & Messages
Conversations are automatically created when a message is sent or received. Each conversation is
scoped to a connected phone inbox and a contact identity. contactPhone remains available for
legacy integrations, but new integrations should use contactIdentifier and contactReplyTo.
Building a live UI on top of these endpoints? Instead of polling them on an interval, subscribe to the Realtime WebSocket and refetch only when an event arrives.
List Conversations
GET /v1/phones/{phoneId}/conversations?page=1&limit=50
Returns conversations ordered by most recent message.
{
"data": [
{
"id": "conv-uuid",
"contactPhone": "5491155551234",
"contactName": "John Doe",
"contactId": "contact-uuid",
"contactIdentifier": "5491155551234",
"contactIdentifierType": "phone",
"contactReplyTo": "5491155551234",
"lastMessageAt": "2026-03-10T14:30:00.000Z",
"lastInboundMessageAt": "2026-03-10T14:30:00.000Z",
"serviceWindowExpiresAt": "2026-03-11T14:30:00.000Z",
"isServiceWindowOpen": true,
"createdAt": "2026-03-01T10:00:00.000Z"
}
],
"meta": { "pagination": { "page": 1, "limit": 50, "total": 12, "hasMore": false } }
}
The 24-Hour Service Window
Every conversation (list and detail) exposes the state of Meta's 24-hour customer service window, so you can decide whether to send a free-form message or a template before calling the send endpoint:
| Field | Type | Description |
|---|---|---|
lastInboundMessageAt |
string | null | Timestamp of the contact's last inbound message. Null when the contact never wrote. |
serviceWindowExpiresAt |
string | null | When the window closes: lastInboundMessageAt + 24h. Null when the window never opened (only templates can be sent). |
isServiceWindowOpen |
boolean | Whether free-form messages can be sent right now, computed at response time. |
Typical integration logic:
if (conversation.isServiceWindowOpen) {
send free-form message (text, media, interactive, ...)
} else {
send a template message
}
Caveats:
- The fields are computed from local data; Meta remains the source of truth. Conversations
originating from Click-to-WhatsApp ads have a longer (72h) free entry point window that is not
modeled here, and an out-of-window send is still rejected by Meta with error
131047(see Sending Messages — Error Handling). isServiceWindowOpenis evaluated when the response is generated — if you cache conversations, compareserviceWindowExpiresAtagainst the current time instead.- Inbound reactions do not open or extend the window.
- The same
lastInboundMessageAt/serviceWindowExpiresAtfields are included in all message webhook events (see the Webhooks guide).
Get Messages
GET /v1/phones/{phoneId}/conversations/{conversationId}/messages?page=1&limit=50
Returns messages ordered by timestamp (newest first).
{
"data": [
{
"id": "msg-uuid",
"conversationId": "conv-uuid",
"wamid": "wamid.HBgLNTQ5MTE1NTU...",
"direction": "inbound",
"type": "text",
"content": "Hi there!",
"status": "delivered",
"timestamp": "2026-03-10T14:30:00.000Z",
"editedAt": null,
"mediaId": null,
"mediaUrl": null,
"mimeType": null,
"filename": null,
"caption": null,
"reactions": [
{
"emoji": "👍",
"reactor": "business",
"direction": "outbound",
"timestamp": "2026-03-10T14:31:00.000Z"
}
],
"createdAt": "2026-03-10T14:30:00.000Z"
}
]
}
content is a plain string with the message's display text: the text body, the media caption, or
the tapped button/list title — empty string when there is none. Type-specific data is exposed as
flat top-level fields (see below).
This differs from webhook payloads, where
data.contentis an object ({ "text": ... },{ "mediaId": ... }). See the Webhooks guide for that shape.
Message Statuses
Messages go through these statuses:
| Status | Direction | Description |
|---|---|---|
pending |
Outbound | Message queued for sending |
sent |
Outbound | Delivered to WhatsApp servers |
delivered |
Outbound | Delivered to the recipient's device |
read |
Outbound | Read by the recipient |
failed |
Outbound | Delivery failed (see error field) |
Inbound messages are stored as delivered when received, and become read once you mark them read with the mark-as-read endpoint. Status updates only ever advance (a late, out-of-order webhook can never move a message backwards).
Message Fields by Type
Each message is returned flattened: content is the display text string, and type-specific data
lives in dedicated top-level fields. Fields that do not apply to the message type are null.
| Field group | Fields | Populated for |
|---|---|---|
| Core | id, conversationId, wamid, direction, type, content, status, timestamp, editedAt, createdAt |
All messages |
| Media | mediaId, mediaUrl, mimeType, filename, caption, transcript |
image, video, audio, document, sticker |
| Template | templateName, templateLanguage, templateParameters |
template |
| Interactive | interactiveType, buttons, interactiveHeader, interactiveBody, interactiveFooter, interactiveSections |
interactive |
| Quoted reply | quotedMessageId, quotedMessageWamid, quotedMessageType, quotedMessagePreview, quotedMessageDirection |
Messages quoting a previous message |
| CTA URL | ctaUrlBodyText, ctaUrlButtonText, ctaUrlUrl, ctaUrlHeaderText, ctaUrlHeaderImage, ctaUrlFooterText |
cta_url |
| Referral | referral, referralSourceType, referralSourceId, referralHeadline, referralCtwaClid |
Inbound messages from Click-to-WhatsApp ads/posts |
| Reactions | reactionEmoji, reactedToMessageId, reactions[] |
See Reactions |
Example — inbound document message:
{
"id": "msg-uuid",
"conversationId": "conv-uuid",
"wamid": "wamid.HBgLNTQ5MTE1NTU...",
"direction": "inbound",
"type": "document",
"content": "Here is my payment receipt",
"status": "delivered",
"timestamp": "2026-03-10T14:30:00.000Z",
"mediaId": "1234567890",
"mimeType": "application/pdf",
"filename": "receipt.pdf",
"caption": "Here is my payment receipt",
"reactions": []
}
To download inbound media from the messages API, use the mediaId with
GET /v1/phones/{phoneId}/media/{mediaId}/download (file bytes) or
GET /v1/phones/{phoneId}/media/{mediaId} (metadata). Both require Bearer auth. The
pre-built mediaUrl/downloadUrl convenience links are only included in webhook payloads.
Reactions are not returned as standalone messages — they are aggregated onto their target message's
reactions[]field (see below).
Reactions
Every message includes a reactions array aggregating all reactions applied to it (from the contact and/or the business):
"reactions": [
{ "emoji": "👍", "reactor": "5491155551234", "direction": "inbound", "timestamp": "2026-03-10T14:31:00.000Z" },
{ "emoji": "❤️", "reactor": "business", "direction": "outbound", "timestamp": "2026-03-10T14:32:00.000Z" }
]
| Field | Description |
|---|---|
emoji |
The reaction emoji |
reactor |
Who reacted: the contact identifier (for inbound) or "business" (for outbound) |
direction |
inbound (from the contact) or outbound (from the business) |
timestamp |
ISO 8601 time the reaction was applied |
Each reactor has at most one active reaction per message: reacting again replaces it, and removing a reaction (empty emoji) deletes it. To apply a reaction, see Sending Messages.