Updated on October 25, 2026
To send a message with the official WhatsApp Business API, make a POST to https://graph.facebook.com/v26.0/<PHONE_NUMBER_ID>/messages with your token in the Authorization header and a JSON body containing messaging_product, to, type and the content. Free-form text can only be sent inside the 24-hour window opened by the customer; outside it, you need an approved template. A 200 response means Meta accepted the request, and delivery is confirmed later by webhook.
Below are copy-paste examples for text and template messages in cURL, Node.js and Python, the same call through Vectalk, and a table of the errors that show up most on a first send.
What you need first
- The Phone Number ID of the sending number. It’s not the number itself; it’s the ID Meta generates for it. See the API glossary.
- An access token with the
whatsapp_business_messagingpermission. For production, use a system user token, as in permanent token. - The recipient’s number with
+and the country code, for example+5584999999999. - An approved template, if the customer hasn’t written in the last 24 hours.
On number format: the send messages documentation recommends always including the + and the country code. Without the +, Meta prepends your business number’s country code, which can send the message to the wrong person. The same page notes that for Brazil and Mexico, the API may adjust the number’s prefix, and that this is expected behavior.
Payload structure
Every send uses the same endpoint. What changes is type and the object with the same name:
| Field | Required | Value |
|---|---|---|
messaging_product | Yes | Always "whatsapp" |
recipient_type | No | "individual" (default) or "group" |
to | Yes | Recipient’s number, ideally with + |
type | Yes | text, template, image, document, interactive, etc. |
<type> | Yes | Object with the content, e.g. text: { body } |
context.message_id | No | wamid of a customer message, to reply quoting it |
Sending text
A text message is a service message: it only works while the customer service window is open. The body accepts up to 4,096 characters, and preview_url: true asks WhatsApp to render a preview of the first link (text messages).
cURL
curl 'https://graph.facebook.com/v26.0/YOUR_PHONE_NUMBER_ID/messages' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-d '{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": "+15551234567",
"type": "text",
"text": {
"preview_url": true,
"body": "Your order has shipped. Track it at https://example.com/track/123"
}
}'
Node.js (18 or later)
const PHONE_NUMBER_ID = process.env.WA_PHONE_NUMBER_ID;
const TOKEN = process.env.WA_TOKEN;
async function sendText(to, text) {
const r = await fetch(`https://graph.facebook.com/v26.0/${PHONE_NUMBER_ID}/messages`, {
method: 'POST',
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
messaging_product: 'whatsapp',
to,
type: 'text',
text: { body: text },
}),
});
const body = await r.json();
if (!r.ok) throw new Error(`${body.error?.code}: ${body.error?.message}`);
return body.messages[0].id; // wamid
}
sendText('+15551234567', 'Hi! We got your message.').then(console.log);
Python
import os
import requests
PHONE_NUMBER_ID = os.environ["WA_PHONE_NUMBER_ID"]
TOKEN = os.environ["WA_TOKEN"]
def send_text(to: str, text: str) -> str:
r = requests.post(
f"https://graph.facebook.com/v26.0/{PHONE_NUMBER_ID}/messages",
headers={"Authorization": f"Bearer {TOKEN}"},
json={
"messaging_product": "whatsapp",
"to": to,
"type": "text",
"text": {"body": text},
},
timeout=15,
)
body = r.json()
if not r.ok:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
return body["messages"][0]["id"] # wamid
print(send_text("+15551234567", "Hi! We got your message."))
Sending a template
Templates are what let you start a conversation. The template must have APPROVED status, and you pass the exact name and language code it was created with. Its category (marketing, utility or authentication) was set at creation and determines the price; see template categories.
Say you have a utility template order_confirmation with the body “Hi {{1}}! Your order {{2}} is confirmed.”:
cURL
curl 'https://graph.facebook.com/v26.0/YOUR_PHONE_NUMBER_ID/messages' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-d '{
"messaging_product": "whatsapp",
"to": "+15551234567",
"type": "template",
"template": {
"name": "order_confirmation",
"language": { "code": "en_US" },
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "Maria" },
{ "type": "text", "text": "#12345" }
]
}
]
}
}'
Node.js
async function sendTemplate(to, name, variables, language = 'en_US') {
const r = await fetch(`https://graph.facebook.com/v26.0/${PHONE_NUMBER_ID}/messages`, {
method: 'POST',
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
messaging_product: 'whatsapp',
to,
type: 'template',
template: {
name,
language: { code: language },
components: [
{ type: 'body', parameters: variables.map((v) => ({ type: 'text', text: v })) },
],
},
}),
});
const body = await r.json();
if (!r.ok) throw new Error(`${body.error?.code}: ${body.error?.message}`);
return body.messages[0].id;
}
sendTemplate('+15551234567', 'order_confirmation', ['Maria', '#12345']).then(console.log);
Python
def send_template(to: str, name: str, variables: list[str], language: str = "en_US") -> str:
r = requests.post(
f"https://graph.facebook.com/v26.0/{PHONE_NUMBER_ID}/messages",
headers={"Authorization": f"Bearer {TOKEN}"},
json={
"messaging_product": "whatsapp",
"to": to,
"type": "template",
"template": {
"name": name,
"language": {"code": language},
"components": [
{"type": "body", "parameters": [{"type": "text", "text": v} for v in variables]}
],
},
},
timeout=15,
)
body = r.json()
if not r.ok:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
return body["messages"][0]["id"]
print(send_template("+15551234567", "order_confirmation", ["Maria", "#12345"]))
If the template was created with named parameters ({{customer_name}}), each parameter also carries parameter_name, and order no longer matters (templates):
{ "type": "text", "parameter_name": "customer_name", "text": "Maria" }
For templates with an image, document or video header, add a header component with the file. Sending media is covered in how to send images, PDFs, audio and video.
What the API returns
When Meta accepts the request, you get:
{
"messaging_product": "whatsapp",
"contacts": [{ "input": "+15551234567", "wa_id": "15551234567" }],
"messages": [{ "id": "wamid.HBgLMTU1NTEyMzQ1NjcVAgARGBI..." }]
}
Three things to keep in mind:
- Store the
wamid. It’s what shows up in thesent,delivered,readandfailedstatus webhooks. See message statuses via webhook. - 200 is not delivery. Errors such as the per-user marketing limit arrive later, in the webhook, with
failedstatus. - Order isn’t guaranteed. If you send several messages in a row, Meta doesn’t guarantee they arrive in request order. To enforce a sequence, wait for one message’s
deliveredstatus before sending the next.
Meta keeps trying to deliver a message for up to 30 days (10 minutes for authentication templates). If no delivered status arrives in that time, treat the message as dropped.
Most common errors on a first send
| Code | What it means | How to fix it |
|---|---|---|
131047 | More than 24 hours since the customer’s last message | Send a template instead |
132001 | Template doesn’t exist in that language or isn’t approved | Check the name, language code and status |
132000 | Number of variables doesn’t match the template | Send one value per variable |
132012 | Variables in the wrong format | Use the template’s format (positional or named) |
131026 | Message undeliverable (no WhatsApp, old app version, terms not accepted) | Confirm the number with the customer |
130429 | Messages-per-second limit reached | Slow down and retry with exponential backoff |
131056 | Too many messages to the same recipient in a short time | Wait before messaging that number again |
133010 | Business number not registered on the platform | Register the number |
190 | Expired token | Generate a new system user token |
Source: Meta error codes. The full list, with causes and fixes, is in WhatsApp API errors.
Best practices for your sending code
The examples above are the minimum to get a message out. In production, a few habits prevent duplicate messages, unnecessary costs and quality problems:
- Record each send before calling the API. Store the request with an internal ID and save the
wamidafter the response. If the process crashes midway, you know what already went out and won’t resend it. - Only retry what’s worth retrying. Retry with backoff on
130429,131056and network or HTTP 5xx failures. Parameter errors (132000,132012) and template errors (132001) won’t improve on retry: fix the payload. - Respect the per-user marketing limit. Meta advises waiting at least 24 hours before resending a template that came back with
131049. - Set timeouts on calls. A request without a timeout can stall your whole queue if the network hiccups.
- Keep the token secret. Read it from an environment variable or a secrets manager and never ship it to the front end or commit it.
- Validate numbers up front. Normalize numbers to international format with
+when you collect them, not at send time. - Mark as read. When you process a customer message, you can mark it as read through the API, which shows them the two blue ticks.
The same send through Vectalk
Vectalk is a proxy for the official WhatsApp Cloud API. Payloads are identical to Meta’s; only the base URL and token change, per the documentation introduction.
| Meta (direct) | Vectalk | |
|---|---|---|
| Send URL | https://graph.facebook.com/v26.0/<PHONE_NUMBER_ID>/messages | https://apis.vectalk.com.br/api/messages |
| Token | Meta token (system user) | Token generated in Vectalk |
| Phone Number ID | In the URL | Resolved by an instance token, or in the body with an organization token |
| Payload and response | Meta’s format | Identical |
With an instance token, the number is resolved automatically:
curl -X POST https://apis.vectalk.com.br/api/messages \
-H "Authorization: Bearer YOUR_VECTALK_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "template",
"template": {
"name": "order_confirmation",
"language": { "code": "en_US" },
"components": [
{ "type": "body", "parameters": [
{ "type": "text", "text": "Maria" },
{ "type": "text", "text": "#12345" }
] }
]
}
}'
With an organization token, which reaches all your instances, include phone_number_id in the body:
import os
import requests
r = requests.post(
"https://apis.vectalk.com.br/api/messages",
headers={"Authorization": f"Bearer {os.environ['VECTALK_TOKEN']}"},
json={
"messaging_product": "whatsapp",
"phone_number_id": "1234567890",
"to": "15551234567",
"type": "text",
"text": {"body": "Hi! We got your message."},
},
timeout=15,
)
print(r.json()["messages"][0]["id"])
Each message type is documented under Messages, and the token types under Authentication. To get your token, create your account.
Frequently asked questions
Can I send text to someone who has never messaged me?
No. Outside the 24-hour window, including for people who never wrote to you, only approved templates can be sent. The API returns error 131047 if you try free-form text. And under WhatsApp policy, the person must have opted in.
The API returned 200 but the message never arrived. Why?
A 200 only confirms Meta accepted the request. Delivery is reported in the status webhook. If it comes back failed, the error code explains why, such as 131049 (per-user marketing limit) or 131026 (number without WhatsApp).
Which Graph API version should I use?
The examples in Meta’s current documentation use v26.0. Pin the version in your URL and check Meta’s changelog before upgrading, since each version has a deprecation date.
How do I message many contacts at once?
There’s no batch send endpoint: each message is its own call. Control your sending rate, stay within your portfolio’s messaging limit and handle error 130429.
How do I reply quoting the customer’s message?
Add "context": { "message_id": "<wamid of the customer's message>" } to the payload. The reply shows up in WhatsApp as a quote of the original message.
Official sources consulted on October 25, 2026: Send messages (service messages), Text messages, Templates, Error codes, Status webhook, Access tokens, Vectalk documentation: introduction, messages and authentication.
