Guide · For developers
WhatsApp API for developers: send your first template message with curl, Node.js and Python
A hands-on developer tutorial: channel token, template details, a complete request with body variables, image header and URL button in curl, Node.js and Python, error handling, delivery webhooks and the simpler campaign endpoint.
By Broadchat teamPublished Updated 10 min read
In short
To send a WhatsApp template message with the API, generate a channel access token in Channels → All Channels → ⋮ → Access Token, then POST a Cloud API template body to <API URL>/<API Version>/<Phone Number ID>/messages with Authorization: Bearer <token>. The template name and language must match an approved template, and the components must supply every variable. A 200 response means accepted, not delivered; track delivery through channel webhooks.
Key takeaways
- Broadchat's channel endpoint accepts Meta's Cloud API request format, so the same body works with a different base URL and token.
- Tokens are created only when you click **Generate new token** in the Access Token window; generate one per integration and store it in a secret manager.
- Template name, language code and the number of parameters must match the approved template exactly, or Meta rejects the send.
- Template sends are charged to the channel wallet first and refunded if Meta rejects them; a 402 with code 131042 means recharge.
- A 200 response carries a message id; delivered, read and failed statuses arrive later through a channel webhook.
Send a WhatsApp template message through the API
Time needed: 30 min. You will need: A terminal with curl, Node.js 18+ or Python 3, Owner login to the Broadchat panel.
Generate a channel access token
In Channels → All Channels, open the ⋮ menu on the channel card and choose Access Token. Copy the Access Token, API URL, API Version and Phone Number ID from the API Details window; the token is shown only once.
Read the template's exact details
In CRM → WhatsApp Templates note the template name, language code, number of body variables, header type and any URL button with a variable.
Send the request with curl
POST to <API URL>/<API Version>/<Phone Number ID>/messages with the Bearer token and a JSON body of type template, including header, body and button components as the template requires.
Move it into Node.js or Python
Wrap the same request in a small function that reads the token from an environment variable, sends JSON and raises a clear error with the HTTP status and error code.
Handle errors and retries
Map 401, 403, 400 and 402 responses to fixes, pass Meta's own errors through, and retry only on network failures or 5xx responses with a back-off.
Track delivery with a webhook
In the channel's Webhooks tab, click Add Webhook, enter an HTTPS URL and select Sent, Delivered, Read and Failed, then match statuses to the message id you stored.
What you will build
By the end of this tutorial you will send a WhatsApp template message with the API from your own server: an order-shipped notification with the customer's name in the body, a product image in the header and a tracking link button. Templates are how a business starts a conversation on WhatsApp; free-form messages are allowed only within 24 hours of the customer's last message.
Broadchat exposes a channel endpoint that accepts Meta's Cloud API request format for your connected number and forwards it to Meta with the channel's credentials. If you have used the Cloud API before, only the base URL and the token change. For an overview of both developer options (this endpoint and API campaigns), see the Broadchat REST API page.
Step 1: generate a channel access token
Only the account owner can do this:
- Open Channels → All Channels.
- On your channel's card, open the ⋮ menu and choose Access Token.
- The API Details window shows Access Token (it starts with
bc_), API URL, API Version, WABA ID, Phone Number ID and Business ID. Click Copy Token and store it in your secret manager or environment variables.
The API URL is your panel's own address plus /api/meta, for example https://app.yourpanel.in/api/meta. A token works only for its own channel's phone number id and WhatsApp Business account id.
Step 2: read the template's exact details
Open CRM → WhatsApp Templates and find the approved template. You need four facts, and each must match exactly:
| Detail | Our example | What goes wrong if it does not match |
|---|---|---|
| Name | order_shipped | Template not found |
| Language code | en (not en_US unless that is what you approved) | Template not found in that language |
| Body variables | 3: name, order number, courier | Parameter count mismatch |
| Header and buttons | Image header; URL button https://rangolihome.in/track/{{1}} | Missing header media or button parameter |
The approved body reads: Hi {{1}}, your Rangoli Home order {{2}} has been shipped with {{3}}. Track it with the button below. The order update templates show similar approved wording.
Step 3: send the request with curl
curl -X POST "https://app.yourpanel.in/api/meta/v21.0/123456789012345/messages" \
-H "Authorization: Bearer $BC_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": "919876543210",
"type": "template",
"template": {
"name": "order_shipped",
"language": { "code": "en" },
"components": [
{
"type": "header",
"parameters": [
{ "type": "image", "image": { "link": "https://cdn.rangolihome.in/products/cushion-48213.jpg" } }
]
},
{
"type": "body",
"parameters": [
{ "type": "text", "text": "Priya" },
{ "type": "text", "text": "#48213" },
{ "type": "text", "text": "Delhivery" }
]
},
{
"type": "button",
"sub_type": "url",
"index": "0",
"parameters": [ { "type": "text", "text": "48213" } ]
}
]
}
}'Replace the host, version and phone number id with the values from your API Details window. Notes on the body:
tois the number with country code, digits only (919876543210).- Body parameters are positional: the first fills
{{1}}, and so on. Send exactly as many as the template has. - The button
indexis the button's position, starting at 0, and its parameter fills the variable at the end of the URL. - Media goes in by public HTTPS link. Multipart media uploads are not supported through the channel endpoint, so host images on your CDN or storage.
A successful call returns Meta's response, for example:
{
"messaging_product": "whatsapp",
"contacts": [ { "input": "919876543210", "wa_id": "919876543210" } ],
"messages": [ { "id": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgSQTQ1RkY2..." } ]
}Store messages[0].id. It is how you match delivery statuses later.
Step 4: move it into Node.js or Python
Node.js 18 or later has fetch built in:
// send-template.mjs
const API_URL = process.env.BC_API_URL; // e.g. https://app.yourpanel.in/api/meta
const API_VERSION = process.env.BC_API_VERSION; // e.g. v21.0, from the API Details window
const PHONE_ID = process.env.BC_PHONE_NUMBER_ID;
const TOKEN = process.env.BC_TOKEN;
export async function sendOrderShipped({ to, name, orderNo, courier, imageUrl }) {
const body = {
messaging_product: 'whatsapp',
to,
type: 'template',
template: {
name: 'order_shipped',
language: { code: 'en' },
components: [
{ type: 'header', parameters: [{ type: 'image', image: { link: imageUrl } }] },
{ type: 'body', parameters: [name, `#${orderNo}`, courier].map((text) => ({ type: 'text', text })) },
{ type: 'button', sub_type: 'url', index: '0', parameters: [{ type: 'text', text: String(orderNo) }] },
],
},
};
const res = await fetch(`${API_URL}/${API_VERSION}/${PHONE_ID}/messages`, {
method: 'POST',
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const e = data.error || {};
throw Object.assign(new Error(`WhatsApp send failed: ${res.status} code ${e.code}: ${e.message}`), { status: res.status, code: e.code });
}
return data.messages[0].id; // save this with the order
}Python with the requests library:
# send_template.py
import os
import requests
API_URL = os.environ["BC_API_URL"] # e.g. https://app.yourpanel.in/api/meta
API_VERSION = os.environ["BC_API_VERSION"] # e.g. v21.0
PHONE_ID = os.environ["BC_PHONE_NUMBER_ID"]
TOKEN = os.environ["BC_TOKEN"]
def send_order_shipped(to, name, order_no, courier, image_url):
body = {
"messaging_product": "whatsapp",
"to": to,
"type": "template",
"template": {
"name": "order_shipped",
"language": {"code": "en"},
"components": [
{"type": "header", "parameters": [{"type": "image", "image": {"link": image_url}}]},
{"type": "body", "parameters": [{"type": "text", "text": t} for t in (name, f"#{order_no}", courier)]},
{"type": "button", "sub_type": "url", "index": "0", "parameters": [{"type": "text", "text": str(order_no)}]},
],
},
}
r = requests.post(
f"{API_URL}/{API_VERSION}/{PHONE_ID}/messages",
headers={"Authorization": f"Bearer {TOKEN}"},
json=body,
timeout=20,
)
data = r.json() if r.content else {}
if not r.ok:
err = data.get("error", {})
raise RuntimeError(f"WhatsApp send failed: {r.status_code} code {err.get('code')}: {err.get('message')}")
return data["messages"][0]["id"]Step 5: handle errors and retries
Errors come back in Meta's shape, {"error": {"message", "type", "code"}}, whether they come from Broadchat's checks or from Meta, so one handler covers both:
| HTTP / code | Meaning | Fix |
|---|---|---|
| 401, code 190 | Missing or invalid token | Check the Bearer prefix; generate a new token if it was revoked |
| 403, code 10 | Token used for another number or account, or the channel is disabled | Use the token of this channel; re-enable the channel |
| 400, code 100 | Body is not a JSON object, or to / template.name is missing | Fix the request body |
| 402, code 131042 | The channel's messaging wallet cannot cover the template | Recharge the channel wallet |
| 502, code 2 | Broadchat could not reach Meta | Retry later with back-off |
| Meta 132000 / 132001 | Parameter count mismatch / template not found in that language | Match the template exactly (step 2) |
| Meta 131049 | Marketing template held back by per-user limits | Do not retry for 24 hours |
Retry only on network errors and 5xx responses, with an exponential back-off, and never in a tight loop: there is no idempotency key, so a blind retry after a timeout can send the same message twice. Check your logs for a stored message id before retrying. All API routes share a limit of 300 requests per minute per IP address; queue bulk sends rather than firing them in parallel. The WhatsApp API error codes article explains the Meta codes in depth.
Step 6: track delivery with a webhook
A 200 response means Meta accepted the message, not that it was delivered. To receive statuses:
- Open Channels, open your channel and go to the Webhooks tab.
- Click Add Webhook, enter a Title and an HTTPS URL, and select the Channel Events you need: Sent, Delivered, Read, Failed and, for replies, Incoming Messages.
- Statuses arrive in Meta's webhook format, one change per POST, with headers such as
X-Webhook-Event. Matchstatuses[0].idto the message id you stored.
Webhooks are sent once with a 5-second timeout and are not retried, so answer with a 2xx immediately and process asynchronously. Use a hard-to-guess URL path, because requests do not currently carry a signature you can verify. The WhatsApp webhooks integration page lists every event and payload.
The shortcut: an API campaign
If you only need "send this approved template to this number" with reporting in the panel, an API campaign has a simpler body. Create one in CRM → Bulk Campaign → Create New Campaign → API Campaign (choose the channel, a name and the Token Validity); the report page shows the Endpoint, a Campaign Token (starting bcc_, shown once) and a Sample Request:
curl -X POST "https://app.yourpanel.in/api/v1/campaigns/<CAMPAIGN_ID>/send" \
-H "Authorization: Bearer $BC_CAMPAIGN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"phone": "919876543210",
"name": "Priya Sharma",
"template": "order_shipped",
"language": "en",
"variables": ["Priya", "#48213", "Delhivery"],
"headerMedia": "https://cdn.rangolihome.in/products/cushion-48213.jpg",
"buttons": ["48213"]
}'Success returns {"success": true, "data": {"recipientId": "…", "status": "SENT", "messageId": "…"}}. A failed send, including a low wallet balance or a rejection by Meta, returns HTTP 502 with success: false and the reason in message, so read the message before retrying. The contact is created or updated with the API as its source, the send appears in the campaign report, and each campaign accepts up to 120 calls per minute.
Costs and compliance
Template sends are charged to the channel's wallet before they reach Meta and refunded automatically if Meta rejects them. Meta charges per delivered message by category: for India, ₹0.115 per utility message and ₹0.8631 per marketing message (Meta rate card effective 1 Oct 2026, excl. GST); see WhatsApp API pricing. The API does not check consent for you: send only to people who opted in, and honour opt-outs in your own system. For the platform basics, read what the WhatsApp Business API is; SaaS companies that want to offer WhatsApp sending inside their own product can do it under their own brand with the white-label platform.
Sources
- Send message templates — Meta for Developers, checked 12 October 2026
- Template components — Meta for Developers, checked 12 October 2026
- Cloud API error codes — Meta for Developers, checked 12 October 2026
- Webhooks for the WhatsApp Business Platform — Meta for Developers, checked 12 October 2026
- Pricing on the WhatsApp Business Platform — Meta for Developers, checked 12 October 2026
Broadchat team · Product and partner success team
The people who build and support Broadchat, the white-label WhatsApp Business API platform. We write from what we see while onboarding partners and their clients on the official WhatsApp Cloud API, and we check every article against Meta's current documentation.
Last updated:
Frequently asked questions
Is Broadchat's API the same as Meta's WhatsApp Cloud API?
The channel endpoint accepts the same paths and request bodies as Meta's Cloud API for your channel's phone number and WhatsApp Business account, and returns Meta's responses. You authenticate with a Broadchat channel token instead of a Meta system user token, and template sends are billed to your channel wallet.
Why does my template send fail with a parameter error?
The number of body parameters must equal the number of variables in the approved template, and media headers and URL buttons with variables need their own components. Also check the language code matches the approved version exactly.
Can I send WhatsApp messages in bulk with one API call?
No. Each call sends to one recipient. Loop over recipients from a server-side queue within the rate limits, or upload the list to a Bulk Campaign in the panel, which paces sends to your number's messaging limit.
How do I know if a WhatsApp API message was delivered?
Add a channel webhook with the Sent, Delivered, Read and Failed events. Each status carries the message id returned by the send call, so you can update your own records when it arrives.
Can I send a free-form text message through the API?
Yes, through the channel endpoint with a Cloud API text body, but only to a customer who messaged you within the last 24 hours. Outside that window Meta rejects free-form messages and you must use a template.