← Blog

·

How to send a message with the WhatsApp Business API: cURL, Node.js and Python examples

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_messaging permission. 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:

FieldRequiredValue
messaging_productYesAlways "whatsapp"
recipient_typeNo"individual" (default) or "group"
toYesRecipient’s number, ideally with +
typeYestext, template, image, document, interactive, etc.
<type>YesObject with the content, e.g. text: { body }
context.message_idNowamid 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:

  1. Store the wamid. It’s what shows up in the sent, delivered, read and failed status webhooks. See message statuses via webhook.
  2. 200 is not delivery. Errors such as the per-user marketing limit arrive later, in the webhook, with failed status.
  3. 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 delivered status 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

CodeWhat it meansHow to fix it
131047More than 24 hours since the customer’s last messageSend a template instead
132001Template doesn’t exist in that language or isn’t approvedCheck the name, language code and status
132000Number of variables doesn’t match the templateSend one value per variable
132012Variables in the wrong formatUse the template’s format (positional or named)
131026Message undeliverable (no WhatsApp, old app version, terms not accepted)Confirm the number with the customer
130429Messages-per-second limit reachedSlow down and retry with exponential backoff
131056Too many messages to the same recipient in a short timeWait before messaging that number again
133010Business number not registered on the platformRegister the number
190Expired tokenGenerate 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 wamid after 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, 131056 and 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 URLhttps://graph.facebook.com/v26.0/<PHONE_NUMBER_ID>/messageshttps://apis.vectalk.com.br/api/messages
TokenMeta token (system user)Token generated in Vectalk
Phone Number IDIn the URLResolved by an instance token, or in the body with an organization token
Payload and responseMeta’s formatIdentical

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.

Integre o WhatsApp oficial ao seu sistema

A Vectalk entrega a Cloud API da Meta com os mesmos payloads e webhooks, um token para todas as contas e documentação completa.