curl -X POST "https://api.bulkneo.com/v1/messages/list" -H "apikey: YOUR_API_KEY" -H "Content-Type: application/json" -d "{\"instance_id\":\"3f9c1a2b-7d4e-4c81-9f0a-2b6d5e8c1a34\",\"to\":\"919000000000\",\"title\":\"Book an appointment\",\"description\":\"Pick a time that suits you\",\"button_text\":\"View slots\",\"sections\":[{\"title\":\"Tomorrow\",\"rows\":[{\"title\":\"10:00 AM\",\"row_id\":\"slot_1000\",\"description\":\"With Dr Shah\"},{\"title\":\"2:30 PM\",\"row_id\":\"slot_1430\"}]}]}"const options = {
method: 'POST',
headers: {apikey: '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
instance_id: '3f9c1a2b-7d4e-4c81-9f0a-2b6d5e8c1a34',
to: '919000000000',
title: 'Book an appointment',
description: 'Pick a time that suits you',
button_text: 'View slots',
sections: [
{
title: 'Tomorrow',
rows: [
{title: '10:00 AM', row_id: 'slot_1000', description: 'With Dr Shah'},
{title: '2:30 PM', row_id: 'slot_1430'}
]
}
]
})
};
fetch('https://api.bulkneo.com/v1/messages/list', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.bulkneo.com/v1/messages/list"
payload = {
"instance_id": "3f9c1a2b-7d4e-4c81-9f0a-2b6d5e8c1a34",
"to": "919000000000",
"title": "Book an appointment",
"description": "Pick a time that suits you",
"button_text": "View slots",
"sections": [
{
"title": "Tomorrow",
"rows": [
{
"title": "10:00 AM",
"row_id": "slot_1000",
"description": "With Dr Shah"
},
{
"title": "2:30 PM",
"row_id": "slot_1430"
}
]
}
]
}
headers = {
"apikey": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"success": true,
"data": {
"instance_id": "3f9c1a2b-7d4e-4c81-9f0a-2b6d5e8c1a34",
"to": "919000000000",
"type": "list",
"status": "sent",
"message_id": "3EB0C1D2F4A5B6C7D8E9",
"provider_status": "PENDING",
"best_effort": true,
"note": "Interactive messages are best-effort: many WhatsApp clients no longer render them and may show a plain-text fallback instead. A successful response means the message was accepted for delivery, not that the buttons will appear. Do not depend on it for anything critical — send a text message with clear instructions as a fallback."
},
"request_id": "b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
}{
"success": false,
"error": {
"code": "invalid_request",
"message": "Field \"text\" is required and must be a non-empty string."
},
"request_id": "b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
}{
"success": false,
"error": {
"code": "invalid_api_key",
"message": "Invalid or revoked API key."
},
"request_id": "b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
}{
"success": false,
"error": {
"code": "subscription_expired",
"message": "Your subscription has expired. Renew it to continue sending messages."
},
"request_id": "b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
}{
"success": false,
"error": {
"code": "trial_quota_exceeded",
"message": "Trial message quota reached (500 messages). Upgrade to a paid plan to keep sending.",
"details": {
"trial_message_limit": 500
}
},
"request_id": "b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
}{
"success": false,
"error": {
"code": "instance_not_found",
"message": "Instance not found. Check the instance_id and that it belongs to your account."
},
"request_id": "b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
}{
"success": false,
"error": {
"code": "instance_not_connected",
"message": "This WhatsApp instance is not connected. Connect it before sending messages."
},
"request_id": "b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
}{
"success": false,
"error": {
"code": "invalid_recipient",
"message": "The recipient number is not a valid WhatsApp number."
},
"request_id": "b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
}{
"success": false,
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded, retry in 12s.",
"details": {
"retry_after_seconds": 12,
"limit_per_minute": 20,
"scope": "messages"
}
},
"request_id": "b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
}{
"success": false,
"error": {
"code": "internal_error",
"message": "An unexpected error occurred."
},
"request_id": "b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
}{
"success": false,
"error": {
"code": "upstream_unavailable",
"message": "The messaging service is temporarily unavailable. Please retry shortly."
},
"request_id": "b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
}Send a list menu
POST /v1/messages/list — a tappable menu grouped into sections.
curl -X POST "https://api.bulkneo.com/v1/messages/list" -H "apikey: YOUR_API_KEY" -H "Content-Type: application/json" -d "{\"instance_id\":\"3f9c1a2b-7d4e-4c81-9f0a-2b6d5e8c1a34\",\"to\":\"919000000000\",\"title\":\"Book an appointment\",\"description\":\"Pick a time that suits you\",\"button_text\":\"View slots\",\"sections\":[{\"title\":\"Tomorrow\",\"rows\":[{\"title\":\"10:00 AM\",\"row_id\":\"slot_1000\",\"description\":\"With Dr Shah\"},{\"title\":\"2:30 PM\",\"row_id\":\"slot_1430\"}]}]}"const options = {
method: 'POST',
headers: {apikey: '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
instance_id: '3f9c1a2b-7d4e-4c81-9f0a-2b6d5e8c1a34',
to: '919000000000',
title: 'Book an appointment',
description: 'Pick a time that suits you',
button_text: 'View slots',
sections: [
{
title: 'Tomorrow',
rows: [
{title: '10:00 AM', row_id: 'slot_1000', description: 'With Dr Shah'},
{title: '2:30 PM', row_id: 'slot_1430'}
]
}
]
})
};
fetch('https://api.bulkneo.com/v1/messages/list', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.bulkneo.com/v1/messages/list"
payload = {
"instance_id": "3f9c1a2b-7d4e-4c81-9f0a-2b6d5e8c1a34",
"to": "919000000000",
"title": "Book an appointment",
"description": "Pick a time that suits you",
"button_text": "View slots",
"sections": [
{
"title": "Tomorrow",
"rows": [
{
"title": "10:00 AM",
"row_id": "slot_1000",
"description": "With Dr Shah"
},
{
"title": "2:30 PM",
"row_id": "slot_1430"
}
]
}
]
}
headers = {
"apikey": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"success": true,
"data": {
"instance_id": "3f9c1a2b-7d4e-4c81-9f0a-2b6d5e8c1a34",
"to": "919000000000",
"type": "list",
"status": "sent",
"message_id": "3EB0C1D2F4A5B6C7D8E9",
"provider_status": "PENDING",
"best_effort": true,
"note": "Interactive messages are best-effort: many WhatsApp clients no longer render them and may show a plain-text fallback instead. A successful response means the message was accepted for delivery, not that the buttons will appear. Do not depend on it for anything critical — send a text message with clear instructions as a fallback."
},
"request_id": "b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
}{
"success": false,
"error": {
"code": "invalid_request",
"message": "Field \"text\" is required and must be a non-empty string."
},
"request_id": "b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
}{
"success": false,
"error": {
"code": "invalid_api_key",
"message": "Invalid or revoked API key."
},
"request_id": "b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
}{
"success": false,
"error": {
"code": "subscription_expired",
"message": "Your subscription has expired. Renew it to continue sending messages."
},
"request_id": "b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
}{
"success": false,
"error": {
"code": "trial_quota_exceeded",
"message": "Trial message quota reached (500 messages). Upgrade to a paid plan to keep sending.",
"details": {
"trial_message_limit": 500
}
},
"request_id": "b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
}{
"success": false,
"error": {
"code": "instance_not_found",
"message": "Instance not found. Check the instance_id and that it belongs to your account."
},
"request_id": "b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
}{
"success": false,
"error": {
"code": "instance_not_connected",
"message": "This WhatsApp instance is not connected. Connect it before sending messages."
},
"request_id": "b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
}{
"success": false,
"error": {
"code": "invalid_recipient",
"message": "The recipient number is not a valid WhatsApp number."
},
"request_id": "b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
}{
"success": false,
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded, retry in 12s.",
"details": {
"retry_after_seconds": 12,
"limit_per_minute": 20,
"scope": "messages"
}
},
"request_id": "b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
}{
"success": false,
"error": {
"code": "internal_error",
"message": "An unexpected error occurred."
},
"request_id": "b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
}{
"success": false,
"error": {
"code": "upstream_unavailable",
"message": "The messaging service is temporarily unavailable. Please retry shortly."
},
"request_id": "b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
}200 means the message was accepted for delivery — not that the menu will appear. The response carries "best_effort": true to say so explicitly. Read Buttons and lists are best-effort before you build anything on this.https://api.bulkneo.com — a send delivers a real WhatsApp message to the number you type, and an instance call really changes your account. Use your own number while you are exploring, and paste a key only from a machine you trust.Your browser remembers what you type into the playground, so a key you paste here is still in the Authorization box on your next visit. Nobody else sees it — it never leaves your machine and it is not part of this site — but on a shared or borrowed computer, clear the site data when you are done.Notes
Limits, all enforced before the message leaves us:| Field | Limit |
|---|---|
title | 1–1,024 characters, required |
button_text | 1–24 characters, required |
description | up to 1,024 characters |
footer_text | up to 60 characters |
sections | 1–10 sections, required |
sections[].title | 1–24 characters, required |
sections[].rows | 1–10 rows per section, required |
sections[].rows[].title | 1–24 characters, required |
sections[].rows[].row_id | 1–200 characters, required, unique across the whole message |
sections[].rows[].description | up to 72 characters |
| Total rows | 30 across all sections |
row_idis what identifies the row someone picked. Two rows sharing an id would make the selection unreadable, so duplicates are rejected.- Omit a row’s
descriptionentirely rather than sending""— an empty string is not accepted.
Example
curl -X POST https://api.bulkneo.com/v1/messages/list \
-H "apikey: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"instance_id": "YOUR_INSTANCE_ID",
"to": "919000000000",
"title": "Book an appointment",
"description": "Pick a time that suits you",
"button_text": "View slots",
"footer_text": "Clinic hours 9am-6pm",
"sections": [
{
"title": "Tomorrow",
"rows": [
{ "title": "10:00 AM", "row_id": "slot_1000", "description": "With Dr Shah" },
{ "title": "2:30 PM", "row_id": "slot_1430" }
]
},
{
"title": "Thursday",
"rows": [
{ "title": "11:00 AM", "row_id": "slot_thu_1100" }
]
}
]
}'
curl -X POST "https://api.bulkneo.com/v1/messages/list" -H "apikey: YOUR_API_KEY" -H "Content-Type: application/json" -d "{\"instance_id\":\"YOUR_INSTANCE_ID\",\"to\":\"919000000000\",\"title\":\"Book an appointment\",\"description\":\"Pick a time that suits you\",\"button_text\":\"View slots\",\"footer_text\":\"Clinic hours 9am-6pm\",\"sections\":[{\"title\":\"Tomorrow\",\"rows\":[{\"title\":\"10:00 AM\",\"row_id\":\"slot_1000\",\"description\":\"With Dr Shah\"},{\"title\":\"2:30 PM\",\"row_id\":\"slot_1430\"}]},{\"title\":\"Thursday\",\"rows\":[{\"title\":\"11:00 AM\",\"row_id\":\"slot_thu_1100\"}]}]}"
The response
Successful responses on this endpoint carry two extra fields:{
"success": true,
"data": {
"instance_id": "3f9c1a2b-7d4e-4c81-9f0a-2b6d5e8c1a34",
"to": "919000000000",
"type": "list",
"status": "sent",
"message_id": "3EB0C1D2F4A5B6C7D8E9",
"provider_status": "PENDING",
"best_effort": true,
"note": "Interactive messages are best-effort: many WhatsApp clients no longer render them and may show a plain-text fallback instead. …"
},
"request_id": "b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
}
row_id is meaningful only to a human reading the chat on the phone.Authorizations
Your API key, sent in an apikey request header. Never in the URL, never as a Bearer token.
Headers
OPTIONAL. A unique string you choose, 8-255 characters of letters, digits or . _ : ~ -
Send the same key with the same body again within 24 hours and the ORIGINAL result is returned without sending a second message — which is what makes a network retry safe. A different body on the same key is refused with 422 idempotency_key_reused rather than silently overwriting either result.
Responses carry Idempotency-Replayed: true|false while this is active. If that header is ABSENT, idempotency is not enabled on this deployment and retries will send again.
8 - 255^[A-Za-z0-9._:~-]{8,255}$Body
Which of your connected numbers to send FROM. Copy it from your numbers screen in the portal, or from GET /v1/instances. It must belong to your account.
"3f9c1a2b-7d4e-4c81-9f0a-2b6d5e8c1a34"
The recipient, with country code and no leading zero — for example 919000000000. Spaces, dashes, brackets and a leading + are accepted and stripped; 6 to 15 digits must remain.
"919000000000"
Heading of the message.
1 - 1024"Book an appointment"
Label on the button that opens the menu. Up to 24 characters.
1 - 24"View slots"
1 to 10 sections, and no more than 30 rows in total across all of them.
1 - 10 elementsShow child attributes
Show child attributes
Body text under the title.
1024"Pick a time that suits you"
Small print under the menu. Up to 60 characters.
60Send this message as a reply, quoting an earlier one. Use the data.message_id returned by a previous send.
128"3EB0C1D2F4A5B6C7D8E9"
Set true when the quoted message is one you sent from this number. Only valid together with quoted_message_id.

