Send a WhatsApp Message via API: cURL, Node, Python, PHP
How to send a WhatsApp message with an API: working cURL, Node.js, Python and PHP examples for templates and session messages, plus status tracking and errors.
By OmniChannels.ai Editorial Team · Reviewed by BroadNet Messaging Solutions Team
Integration
Short answer
With the OmniChannels.ai API you send every WhatsApp message through one endpoint, POST https://api.broadnet.me/whatsapp/message/sendbulk, as multipart form data with an Authorization: Bearer header. The messageType field selects the payload: TEMPLATE for approved templates, which you need to start a conversation or write outside the 24-hour window, and TEXT, IMAGE or INTERACTIVE for replies inside it.
On this page
- How do I send a WhatsApp message through the API?
- Template or session message: which do I send?
- How do I send an approved template?
- Which request fields matter most?
- How do I send a template with a button or an OTP?
- How do I reply inside the customer service window?
- How do I track delivery?
- What errors should I handle?
- Worked example: an order notification service
- What are the common mistakes?
- How OmniChannels.ai helps
Key takeaways
- All WhatsApp messages go through POST /whatsapp/message/sendbulk; messageType (TEMPLATE, TEXT, IMAGE, INTERACTIVE and others) decides which fields are required.
- The request is multipart/form-data, not JSON, so Python needs files= and PHP needs an array body to avoid url-encoding.
- Outside the 24-hour customer service window only an approved template can be sent; inside it you can send free-form text, media and interactive messages.
- The call queues the messages and returns a batchId immediately; per-recipient statuses arrive on your webhook and from the bulk-insights endpoint.
- The API takes no idempotency key, so record each send before you make it and check status after a timeout instead of resending.
This guide is for developers integrating WhatsApp sending into an application. It shows working requests in cURL, Node.js, Python and PHP, explains the difference between template and session messages, and covers tracking and errors. Every endpoint and field comes from our WhatsApp API reference and OpenAPI spec.
How do I send a WhatsApp message through the API?
You make one authenticated POST to https://api.broadnet.me/whatsapp/message/sendbulk with form fields. The messageType field chooses the kind of message, and the response confirms that the messages were queued.
| Item | Value |
|---|---|
| Base URL | https://api.broadnet.me (no /v1 prefix) |
| Endpoint | POST /whatsapp/message/sendbulk |
| Authentication | Authorization: Bearer $BROADNET_TOKEN |
| Content type | multipart/form-data (set automatically by your HTTP client) |
| Response | JSON batch summary with a batchId |
Test credentials are issued after a short access request, and test sends go to your own verified numbers. See the quickstart and authentication pages. Keep the token on your server only.
Template or session message: which do I send?
Send a template to start a conversation or to write outside the window, and a session message when the customer wrote to you within the last 24 hours. Meta sets this rule, not us.
| Template message | Session message | |
|---|---|---|
messageType |
TEMPLATE |
TEXT, IMAGE, VIDEO, AUDIO, DOCUMENT, INTERACTIVE and others |
| When allowed | Any time, once the template is approved | Only while the customer service window is open |
| Needs approval | Yes, by Meta | No |
| Typical use | Order updates, OTP, reminders, campaigns | Support replies, answers to questions |
Meta charges per delivered message by category, and its rules for service messages and utility templates changed on 1 October 2026. Our window guide covers the mechanics, and our pricing page and calculator cover cost.
How do I send an approved template?
Set messageType=TEMPLATE, name the template and its language code, and pass the variable values. Create batchId=0 to start a new batch.
curl -X POST https://api.broadnet.me/whatsapp/message/sendbulk \
-H "Authorization: Bearer $BROADNET_TOKEN" \
-F batchId=0 \
-F campaignName=order-updates \
-F destinationNo=971500000000 \
-F messageType=TEMPLATE \
-F templateName=order_update \
-F templateLanguage=en_US \
-F bodyTemplateVariables=INV-400
// Node.js 18+ (built-in fetch and FormData)
const form = new FormData();
form.append("batchId", "0");
form.append("campaignName", "order-updates");
form.append("destinationNo", "971500000000");
form.append("messageType", "TEMPLATE");
form.append("templateName", "order_update");
form.append("templateLanguage", "en_US");
form.append("bodyTemplateVariables", "INV-400");
const res = await fetch("https://api.broadnet.me/whatsapp/message/sendbulk", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.BROADNET_TOKEN}` }, // fetch sets the multipart boundary
body: form,
});
console.log(await res.json());
import os
import requests
fields = {
"batchId": "0",
"campaignName": "order-updates",
"destinationNo": "971500000000",
"messageType": "TEMPLATE",
"templateName": "order_update",
"templateLanguage": "en_US",
"bodyTemplateVariables": "INV-400",
}
# Send every field as a multipart part (files=) rather than url-encoded (data=).
r = requests.post(
"https://api.broadnet.me/whatsapp/message/sendbulk",
headers={"Authorization": f"Bearer {os.environ['BROADNET_TOKEN']}"},
files={k: (None, v) for k, v in fields.items()},
timeout=30,
)
print(r.json())
<?php
$ch = curl_init("https://api.broadnet.me/whatsapp/message/sendbulk");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("BROADNET_TOKEN")],
// An array (not a string) makes cURL send multipart/form-data.
CURLOPT_POSTFIELDS => [
"batchId" => "0",
"campaignName" => "order-updates",
"destinationNo" => "971500000000",
"messageType" => "TEMPLATE",
"templateName" => "order_update",
"templateLanguage" => "en_US",
"bodyTemplateVariables" => "INV-400",
],
]);
echo curl_exec($ch);
curl_close($ch);
The destination is country code plus number, digits only. The call returns at once with a batch summary:
{
"totalFiles": 1,
"totalSuccess": 1,
"totalFailed": 0,
"totalDuplicates": 0,
"batchId": "2609091405000000001",
"fileResults": [
{
"fileName": "direct",
"totalRecords": 1,
"successCount": 1,
"failedRecipients": [],
"duplicateRecipients": []
}
],
"message": "Messages queued for delivery"
}
Store the batchId next to your own record. "Queued" is not "delivered".
Which request fields matter most?
Which fields are required depends on messageType. These are the common ones.
| Field | Required | Description |
|---|---|---|
messageType |
Yes | TEXT, IMAGE, VIDEO, AUDIO, DOCUMENT, STICKER, LOCATION, CONTACTS, TEMPLATE, INTERACTIVE, REACTION or FLOW |
destinationNo or destinationFiles |
One of the two | Numbers separated by commas, semicolons or new lines; or a TXT, CSV or XLSX file |
batchId |
No | 0 starts a new batch |
campaignName |
No | Your label for reporting |
allowDuplicate |
No | false removes duplicate recipients |
templateName, templateLanguage |
For TEMPLATE |
Approved template name and language code, such as en_US or ar |
bodyTemplateVariables |
Per template | Body variable value; named parameters are passed as JSON |
buttonsTemplateVariable |
Per template | Dynamic button value: URL suffix, coupon code or OTP code |
content |
For TEXT |
Text body, up to 4,096 characters, or a caption for media |
mediaUrl |
Per type | Public HTTPS URL of the media |
interactivePayload |
For INTERACTIVE |
Meta's interactive object as a JSON string |
Scheduling uses scheduled, scheduleTime and gmt, and delay throttles delivery. The complete list is in the WhatsApp API reference.
How do I send a template with a button or an OTP?
Add the button value in buttonsTemplateVariable. For an authentication template with a copy-code button, pass the code in both the body and the button fields.
curl -X POST https://api.broadnet.me/whatsapp/message/sendbulk \
-H "Authorization: Bearer $BROADNET_TOKEN" \
-F batchId=0 \
-F destinationNo=971500000000 \
-F messageType=TEMPLATE \
-F templateName=login_code \
-F templateLanguage=en_US \
-F bodyTemplateVariables=482913 \
-F buttonsTemplateVariable=482913
Here login_code is a placeholder for your own approved authentication template. For several buttons, buttonsTemplateVariable takes a JSON array. Generate the code on your server, store it hashed with an expiry, and never log it. Our OTP guide covers one-tap, zero-tap and copy-code delivery.
How do I reply inside the customer service window?
Use a session message type. A plain text reply needs messageType=TEXT and content.
curl -X POST https://api.broadnet.me/whatsapp/message/sendbulk \
-H "Authorization: Bearer $BROADNET_TOKEN" \
-F batchId=0 \
-F destinationNo=971500000000 \
-F messageType=TEXT \
-F campaignName=support \
-F "content=Your order has shipped. Reply here if you need anything."
For reply buttons, send messageType=INTERACTIVE and an interactivePayload that follows Meta's interactive message format.
curl -X POST https://api.broadnet.me/whatsapp/message/sendbulk \
-H "Authorization: Bearer $BROADNET_TOKEN" \
-F batchId=0 \
-F destinationNo=971500000000 \
-F messageType=INTERACTIVE \
-F campaignName=support \
-F 'interactivePayload={"type":"button","body":{"text":"Confirm your delivery slot?"},"action":{"buttons":[{"type":"reply","reply":{"id":"slot_ok","title":"Confirm"}},{"type":"reply","reply":{"id":"slot_change","title":"Change"}}]}}'
To mark an inbound message as read, and optionally show a typing indicator, call POST /whatsapp/message/read with JSON:
curl -X POST https://api.broadnet.me/whatsapp/message/read \
-H "Authorization: Bearer $BROADNET_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "messageId": "wamid.HBgMOTcxNTAwMDAwMDAwFQIAEhgg", "typing": true }'
See our guide to interactive messages for design guidance.
How do I track delivery?
Statuses are pushed to the webhook configured on your account and can also be read per batch. Each recipient moves through SUBMITTED, SENT, DELIVERED and READ, or ends as FAILED or REJECTED with a reason.
curl "https://api.broadnet.me/whatsapp/message/bulk-insights?batch_Id=2609091405000000001&status=FAILED&page=0&size=50" \
-H "Authorization: Bearer $BROADNET_TOKEN"
batch_Id is required; status, destination, search, page and size are optional, and page is zero-based. Use polling as a backstop for missed events, and build your receiver as described in our webhooks guide. Batches can also be paused, resumed and aborted with the batch endpoints in the reference.
What errors should I handle?
Read the HTTP status first, then the error body. The API raises errors in a five-field JSON body, and WhatsApp-specific failures can come from Meta.
| Status | Meaning | Action |
|---|---|---|
400 |
Validation error, such as a missing type-specific field or invalid destination | Fix the request; do not retry unchanged |
401 / 403 |
Missing or invalid token, or operation not allowed | Fix the token or ask us to enable the operation |
404 |
Template or sender not found | Check the template name and language |
413 |
Uploaded file too large | Reduce the file |
429 |
Rate limit | Wait for Retry-After |
502 |
Meta rejected the call; message carries Meta's error code and text |
Read it; retry only if transient |
500, 503, 504 |
Server or upstream trouble | Retry with exponential backoff and jitter |
Our error codes guide explains common Meta codes. Because there is no idempotency key, follow the duplicate-protection steps in Errors and limits: write a pending record first, store the returned batchId, and after a timeout check status before sending again.
Worked example: an order notification service
"Souq Rafiq" (synthetic), a UAE retailer, sends an order_update template when an order ships.
- The order service writes a row keyed
order-8842-shippedwith statuspending. - It calls
sendbulkwith the template and the order number asbodyTemplateVariables. - On
200, it stores thebatchIdand marks the rowaccepted. - The webhook handler updates the row to
DELIVEREDorREAD, orFAILEDwith a reason. - If the customer replies, the support tool uses a
TEXTsession message inside the window. - A nightly job reads
bulk-insightsfor yesterday's batches and reconciles any missing statuses.
What are the common mistakes?
- Sending JSON. The WhatsApp send endpoint is multipart;
Content-Type: application/jsonwill not work. - Python
data=instead offiles=, which url-encodes the form. - Free-form text outside the window, which fails. Use a template.
- Wrong language code, such as
eninstead of the template'sen_US. - Treating
200as delivered. It means queued. - Blind retries after a timeout, which can message a customer twice.
- Token in client code. Call the API from your backend only.
- Limits: payload details vary by template. Confirm the variable layout for your first personalised campaign with our team.
How OmniChannels.ai helps
BroadNet is an official Meta Business Solution Provider, and onboarding to the WhatsApp Business API uses Meta Embedded Signup; our team prepares the registration with you, while approvals remain with Meta. The REST API is documented with an OpenAPI 3.1 spec and a TypeScript SDK, @broadnet/cpaas-sdk, and delivery statuses are normalised across channels. Start with the WhatsApp API docs or the quickstart.
Frequently asked questions
Do I send WhatsApp messages as JSON or form data?
Form data. The WhatsApp send endpoint, POST /whatsapp/message/sendbulk, takes multipart/form-data, with each field as a form part. SMS single sends use JSON, which is a common source of confusion. In Python send fields through files= rather than data=, and in PHP pass an array as the body so cURL builds multipart.
Can I send a WhatsApp message without a template?
Only inside the customer service window. When a customer messages you, a 24-hour window opens and you can reply with text, media or interactive messages. Outside it, or to start a conversation, you must send an approved template. The API enforces Meta's rules, and a free-form send to a closed window will fail.
How do I know whether the message was delivered?
The send call only confirms that messages were queued and returns a batchId. Per-recipient statuses move through SUBMITTED, SENT, DELIVERED and READ, or end as FAILED or REJECTED. They are pushed to your webhook, and you can also read them per batch from the bulk-insights endpoint.
Can I send to many recipients in one request?
Yes. destinationNo accepts several numbers separated by commas, semicolons or new lines, and destinationFiles accepts a TXT, CSV or XLSX file. For a template with different variables per recipient, upload a personalised file with one row per recipient. Set allowDuplicate to false to remove duplicate numbers.
Is there an idempotency key to prevent duplicate sends?
No. The API does not accept an idempotency key. If a request times out, you cannot tell from the client whether it was accepted. Record each send in your own database before calling, store the returned batchId, and check status or the batch list before any retry.
Do I need an opt-in before I send?
Yes. Meta's policy requires that each recipient has opted in to receive messages from your business, and templates must be approved before use. The API does not check consent for you. Capture opt-in at the source, store the evidence and suppress anyone who opts out.
Sources
Keep reading
More guidesIntegration
WhatsApp Webhooks Guide: Events, Security and Retries
How WhatsApp webhooks work: message and status events, a reliable handler, Meta's signature check, retries, idempotency and recovery when events go missing.
10 min read
Integration
Send SMS via API: Python, Node.js, PHP and cURL Examples
Send SMS with an API in Python, Node.js, PHP and cURL: single and bulk sends, Unicode and Arabic, segment counting, delivery status webhooks and error handling.
10 min read
WhatsApp
WhatsApp OTP vs SMS OTP: Cost, Delivery and Fallback
WhatsApp OTP vs SMS OTP compared: cost model, delivery, user experience, authentication templates, WhatsApp-to-SMS fallback and fraud controls.
10 min read
