API reference
Messages API
Free-form text, media, location and interactive messages, plus message history.
A thin pass-through to Meta: one send endpoint with eight message types, plus two read endpoints for history and conversation threads. Nothing here chooses a template for you or orders its parameters.
The 24-hour window decides what you can send
WhatsApp will not deliver a free-form message to a customer unless that customer messaged you in the last 24 hours. The window opens at the timestamp of their most recent inbound message, closes exactly 24 hours later, and every new inbound message resets it.
- Window open
- Anything: text, image, video, audio, document, location, interactive, or a template.
- Window closed, or they have never messaged you
- An approved template only. Nothing else is delivered.
On this endpoint every type except template is checked against the window and refused with 403 when it is closed:
{ "error": "The 24-hour customer service window is closed for this contact. Per WhatsApp policy, only an approved template message can start or re-open a conversation." }This endpoint is the right one for two things: replying to a customer while their window is open, and sending a template you wrote yourself and are prepared to build the components for.
Sending a message
One endpoint, eight message types. Send exactly one type-specific object per request.
Common body parameters
| Field | Required | Type | Description |
|---|---|---|---|
| phoneNumberId | Yes | string | Meta phone number id from GET /v1/phones. Unlike the notifications API this is not optional. There is no default number here.Example: 1030974986765331 |
| to | Yes | string | Recipient in E.164. This endpoint parses and validates the number rather than only length-checking it, so a number that cannot exist is rejected with a 400 instead of being accepted by Meta and delivered to nobody. That makes it stricter than /v1/notifications/send, which only checks the length, a value one endpoint accepts, the other can refuse. Fictional numbers from the 555-01xx range validate; +1 555 123 4567 does not, so use a real recipient when testing.Example: +12025550123 |
| type | Yes | string | text, template, image, video, audio, document, location or interactive. |
type: "text"
| Field | Required | Type | Description |
|---|---|---|---|
| text.body | Yes | string | The message text. |
| text.previewUrl | No | boolean | Render a link preview for the first URL in the body. Default: false |
{
"phoneNumberId": "1030974986765331",
"to": "+12025550123",
"type": "text",
"text": {
"body": "Hello! How can we help?",
"previewUrl": false
}
}type: "template"
| Field | Required | Type | Description |
|---|---|---|---|
| template.name | Yes | string | An approved template name. |
| template.language | No | object | Language wrapper. |
| template.language.code | No | string | Locale code. Default: en_US |
| template.components | No | array | Meta's component array. Optional to the schema, but required by Meta whenever the template body has placeholders, and the positional order must match the approved body exactly, or the send fails with error 132000. |
{
"phoneNumberId": "1030974986765331",
"to": "+12025550123",
"type": "template",
"template": {
"name": "order_confirmation",
"language": { "code": "en_US" },
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "Alex" },
{ "type": "text", "text": "#ORD-8821" },
{ "type": "text", "text": "1,499.00" },
{ "type": "text", "text": "https://example.com/track/ORD-8821" }
]
}
]
}
}Getting that array right is exactly what the notifications API does for you for the 18 catalog events.
type: "image" / "video" / "audio" / "document"
All four share the media object. Supply link or id, one of the two is required.
| Field | Required | Type | Description |
|---|---|---|---|
| media.link | No | string | A publicly reachable URL Meta can fetch. Required unless id is set. |
| media.id | No | string | A media id from a previous upload. Required unless link is set, and takes precedence when both are given. The link is dropped silently. |
| media.caption | No | string | Caption. Used by image, video and document. Ignored for audio. |
| media.filename | No | string | Filename shown to the recipient. Used by document only , ignored for image, video and audio. |
Sending neither link nor id is a 400: `media.link` or `media.id` is required for media messages.
{
"phoneNumberId": "1030974986765331",
"to": "+12025550123",
"type": "document",
"media": {
"link": "https://example.com/invoice.pdf",
"filename": "invoice.pdf",
"caption": "Your invoice"
}
}type: "location"
| Field | Required | Type | Description |
|---|---|---|---|
| location.latitude | Yes | number | Decimal degrees. A number, not a string, a quoted value fails validation. Example: 24.8607 |
| location.longitude | Yes | number | Decimal degrees. A number, not a string. Example: 67.0011 |
| location.name | No | string | Place name. |
| location.address | No | string | Street address. |
{
"phoneNumberId": "1030974986765331",
"to": "+12025550123",
"type": "location",
"location": {
"latitude": 24.8607,
"longitude": 67.0011,
"name": "Head Office",
"address": "Karachi"
}
}type: "interactive"
| Field | Required | Type | Description |
|---|---|---|---|
| interactive | Yes | object | Passed to Meta verbatim. It is not validated beyond “is an object”, so Meta's own rules are the only thing checking it, and a violation comes back as a 502 after the request has left you. |
Because nothing local validates the object, the limits below are worth enforcing in your own code. Meta truncates nothing: an over-long title rejects the whole message.
Meta's limits
| Field | Required | Type | Description |
|---|---|---|---|
| action.buttons | No | array | Maximum 3 reply buttons. |
| buttons[].reply.title | No | string | Maximum 20 characters. |
| action.sections[].rows | No | array | Maximum 10 rows in total, counted across all sections. |
| rows[].title | No | string | Maximum 24 characters. |
| rows[].description | No | string | Maximum 72 characters. |
| header.text | No | string | Maximum 60 characters. |
| footer.text | No | string | Maximum 60 characters. |
| body.text | No | string | Maximum 1024 characters. |
Reply buttons:
{
"phoneNumberId": "1030974986765331",
"to": "+12025550123",
"type": "interactive",
"interactive": {
"type": "button",
"body": { "text": "Confirm your order?" },
"action": {
"buttons": [
{ "type": "reply", "reply": { "id": "yes", "title": "Yes" } },
{ "type": "reply", "reply": { "id": "no", "title": "No" } }
]
}
}
}A list menu:
{
"phoneNumberId": "1030974986765331",
"to": "+12025550123",
"type": "interactive",
"interactive": {
"type": "list",
"body": { "text": "Pick a department" },
"action": {
"button": "Choose",
"sections": [
{ "rows": [
{ "id": "billing", "title": "Billing" },
{ "id": "support", "title": "Support" }
] }
]
}
}
}A single product from a catalog:
{
"phoneNumberId": "1030974986765331",
"to": "+12025550123",
"type": "interactive",
"interactive": {
"type": "product",
"body": { "text": "Check out this item" },
"action": { "catalog_id": "1234567890", "product_retailer_id": "SKU_001" }
}
}The reply a customer taps comes back on your inbound webhook, in one of two shapes depending on whether the button was on an interactive message or on a template. Both are covered under Receiving messages.
Response
Response 200, all types
| Field | Required | Type | Description |
|---|---|---|---|
| success | Yes | boolean | Always true. |
| messageId | Yes | string | Meta message id ( wamid.…). |
{ "success": true, "messageId": "wamid.HBgLMTU1NTEyMzQ1NjcVAgARGBI..." }Errors
400, the body failed schema validation, in which case error is a Zod object. The type-specific checks run later and return plain strings:
{ "error": "`text.body` is required for type=text" }{ "error": "`template` is required for type=template" }{ "error": "`media.link` or `media.id` is required for media messages" }{ "error": "`location` is required for type=location" }{ "error": "`interactive` is required for type=interactive" }402, insufficient credits or wallet balance. Note the validation-order warning above. A free-form reply inside an open window is a SERVICE conversation and costs nothing, so it succeeds even on a zero balance.
403, the 24-hour window is closed, for any type other than template.
404, the phoneNumberId is not on this account, or has no access token:
{ "error": "Phone number not found or not configured" }502, Meta rejected the send. The charge is refunded:
{ "error": "Message delivery failed: (#132000) Number of parameters does not match the expected number of params" }Message history
Message history for the account, newest first, inbound and outbound, from every surface.
Query parameters
| Field | Required | Type | Description |
|---|---|---|---|
| phoneNumberId | No | string | Filter by number. An id that is not on your account is ignored rather than rejected, and you get unfiltered results, check the id if a filter seems to do nothing. Default: all numbers |
| direction | No | string | inbound or outbound. Any other value matches nothing and returns an empty array.Default: both |
| limit | No | string | Maximum rows, capped at 200. A non-numeric value falls back to 50. Default: 50 |
| cursor | No | string | The last row's id, to fetch older records. |
curl "https://whatsapp-api.growcord.in/api/v1/messages?phoneNumberId=1030974986765331&direction=outbound&limit=50" \
-H "Authorization: Bearer sk_live_xxx"Response 200, array of
| Field | Required | Type | Description |
|---|---|---|---|
| id | Yes | string | Row id. Use it as the next cursor. |
| recipientWaId | Yes | string | The counterparty, digits only. |
| messageType | Yes | string | text, template, image, interactive, and so on. |
| direction | Yes | string | inbound or outbound. |
| status | No | string | null | sent, delivered, read, failed or received. |
| metaMessageId | No | string | null | wamid.…. De-duplicate on this. |
| payload | No | string | null | A JSON string, not an object. For inbound rows it is the raw Meta message; for notification sends it is { event, template, variables }; for /v1/messages/send it is null. |
| createdAt | Yes | string | ISO 8601. |
| phoneNumber | Yes | object | { phoneNumberId, displayPhoneNumber }. |
[
{
"id": "clx8f2a0b0000abcd1234efgh",
"recipientWaId": "919876543210",
"messageType": "template",
"direction": "outbound",
"status": "delivered",
"metaMessageId": "wamid.HBgMOTE5ODc2NTQzMjEw...",
"payload": "{\"event\":\"order_shipped\",\"template\":\"order_shipped\",\"variables\":{\"orderId\":\"#ORD-9001\"}}",
"createdAt": "2026-08-05T10:00:04.000Z",
"phoneNumber": { "phoneNumberId": "1030974986765331", "displayPhoneNumber": "+15556052643" }
}
]Errors: 401, 429.
Conversations
Conversation threads, most recently active first.
Query parameters
| Field | Required | Type | Description |
|---|---|---|---|
| status | No | string | OPEN, PENDING, RESOLVED or BOT. Case-sensitive, an unrecognised value returns an empty array rather than an error.Default: all |
| limit | No | string | Maximum rows, capped at 200. Default: 50 |
| cursor | No | string | The last row's id, for pagination. |
curl "https://whatsapp-api.growcord.in/api/v1/conversations?status=OPEN&limit=50" \
-H "Authorization: Bearer sk_live_xxx"Response 200, array of
| Field | Required | Type | Description |
|---|---|---|---|
| id | Yes | string | Conversation id. Use it as the next cursor. |
| contactWaId | Yes | string | The contact's WhatsApp id, digits only. |
| contactPhone | No | string | null | The contact's phone number. |
| contactName | No | string | null | WhatsApp profile name, as the customer set it. |
| status | Yes | string | OPEN, PENDING, RESOLVED or BOT. |
| lastMessage | No | string | null | Preview, truncated to 100 characters. |
| lastMessageAt | No | string | null | ISO 8601. With lastMessageDir: "inbound" this is when the 24-hour window last opened. |
| lastMessageDir | No | string | null | inbound or outbound. |
| unreadCount | Yes | integer | Unread inbound messages. |
| phoneNumber | Yes | object | { phoneNumberId, displayPhoneNumber }. |
[
{
"id": "clx8f2a0b0001abcd5678ijkl",
"contactWaId": "919876543210",
"contactPhone": "919876543210",
"contactName": "Alex",
"status": "OPEN",
"lastMessage": "Thanks!",
"lastMessageAt": "2026-08-05T10:30:00.000Z",
"lastMessageDir": "inbound",
"unreadCount": 2,
"phoneNumber": { "phoneNumberId": "1030974986765331", "displayPhoneNumber": "+15556052643" }
}
]Errors: 401, 429.
