---
title: "Send a WhatsApp Message via API: cURL, Node, Python, PHP"
description: "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."
canonical: "https://omnichannels.ai/blog/send-whatsapp-message-api-examples/"
language: "en"
updated: "2026-10-16"
publisher: "BroadNet Technologies"
brand: "OmniChannels.ai"
category: "integration"
primaryQuery: "send whatsapp message api"
keywords: ["whatsapp api curl example", "send whatsapp template node.js", "whatsapp api python", "whatsapp api php", "whatsapp business api send message", "whatsapp session message api", "whatsapp multipart form-data api"]
published: "2026-10-16"
author: "OmniChannels.ai Editorial Team"
reviewer: "BroadNet Messaging Solutions Team"
readingMinutes: 10
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."
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."]
faq: 
  - q: "Do I send WhatsApp messages as JSON or form data?"
    a: "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."
  - q: "Can I send a WhatsApp message without a template?"
    a: "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."
  - q: "How do I know whether the message was delivered?"
    a: "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."
  - q: "Can I send to many recipients in one request?"
    a: "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."
  - q: "Is there an idempotency key to prevent duplicate sends?"
    a: "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."
  - q: "Do I need an opt-in before I send?"
    a: "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: 
  - title: "WhatsApp Business Messaging Policy"
    url: "https://business.whatsapp.com/policy"
  - title: "Pricing on the WhatsApp Business Platform — Meta for Developers"
    url: "https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing"
  - title: "RFC 7578: Returning Values from Forms: multipart/form-data"
    url: "https://www.rfc-editor.org/rfc/rfc7578"
---

# Send a WhatsApp Message via API: cURL, Node, Python, PHP

Canonical: https://omnichannels.ai/blog/send-whatsapp-message-api-examples/
Language: en
Publisher: OmniChannels.ai by BroadNet Technologies

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](/developers/whatsapp/) 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](/developers/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](/blog/whatsapp-24-hour-customer-service-window/) covers the mechanics, and our [pricing page](/whatsapp-pricing/) and [calculator](/tools/whatsapp-pricing-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.

```bash
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
```

```js
// 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());
```

```python
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
<?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:

```json
{
  "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](/developers/whatsapp/).

## 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.

```bash
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](/blog/otp-authentication-messages-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`.

```bash
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.

```bash
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:

```bash
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.

```bash
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](/blog/whatsapp-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](/developers/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.

1. The order service writes a row keyed `order-8842-shipped` with status `pending`.
2. It calls `sendbulk` with the template and the order number as `bodyTemplateVariables`.
3. On `200`, it stores the `batchId` and marks the row `accepted`.
4. The webhook handler updates the row to `DELIVERED` or `READ`, or `FAILED` with a reason.
5. If the customer replies, the support tool uses a `TEXT` session message inside the window.
6. A nightly job reads `bulk-insights` for yesterday's batches and reconciles any missing statuses.

## What are the common mistakes?

- **Sending JSON.** The WhatsApp send endpoint is multipart; `Content-Type: application/json` will not work.
- **Python `data=`** instead of `files=`, which url-encodes the form.
- **Free-form text outside the window**, which fails. Use a template.
- **Wrong language code**, such as `en` instead of the template's `en_US`.
- **Treating `200` as 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](/developers/whatsapp/) or the [quickstart](/developers/quickstart/).
