WhatsApp API & Developers

Send a WhatsApp Message by API: curl Examples

You have a backend, a WhatsApp number and a reason to message someone: an order shipped, a class moved, a quote is ready. What you want is one HTTP request that puts a message on their phone. What most guides give you is a long tour of the Meta dashboard.

This post is the request itself. To send a WhatsApp message by API you need three values, an approved template for anything the customer didn't just ask for, and a clear idea of what the response actually promises.

Below are working curl calls for Meta's Cloud API (a template, then a free-form reply) and for Tenashi's API, which sends from a number your team already uses, including into WhatsApp groups. For the wider picture, start with our complete guide to the WhatsApp API.

The three values every send needs

Every Cloud API send is a POST to /PHONE_NUMBER_ID/messages on the Graph API with a bearer token. Get these three right and most "it doesn't work" problems disappear.

  1. An access token. For anything beyond a quick test, use a system user token with the whatsapp_business_messaging permission. The temporary token on the API Setup page expires quickly, which is why scripts that worked yesterday start failing with error 190. Our guide to the permanent access token walks through creating one.
  2. The phone number ID. This is a numeric ID Meta assigns to your business number, not the number itself. It appears in App Dashboard, WhatsApp, API Setup, and it goes in the URL.
  3. The recipient in international format, with a plus sign. Meta's docs are blunt about this: if you leave off the +, your business number's country code is prepended.

Meta's own example, for a business registered in India (country code 91):

Number in your requestWhere Meta delivers it
+16315551234+1 631 555 1234 (correct)
+1 (631) 555-1234+1 631 555 1234 (correct)
(631) 555-1234+91 631 555 1234 (probably a stranger)
1 (631) 555-1234+91 1 631 555 1234 (probably nobody)

Spaces, hyphens and brackets are fine. A missing plus sign is not. Normalise numbers to E.164 when you collect them, not when you send.

The examples below use Graph API v23.0. Use the current version from Meta's changelog when you build.

export WHATSAPP_TOKEN="paste-your-system-user-token"
# Replace PHONE_NUMBER_ID in the URLs below with your number's ID

Send a template message (works any time)

If the customer hasn't messaged you in the last 24 hours, the only thing you're allowed to send is an approved template. Meta's quickstart uses a sample template called hello_world, which is the fastest way to prove your token, number ID and recipient are all correct:

curl "https://graph.facebook.com/v23.0/PHONE_NUMBER_ID/messages" \
  -H "Authorization: Bearer $WHATSAPP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "messaging_product": "whatsapp",
    "recipient_type": "individual",
    "to": "+919811122233",
    "type": "template",
    "template": {
      "name": "hello_world",
      "language": { "code": "en_US" }
    }
  }'

A real template has variables. Here is a utility template called order_shipped with the body "Hi {{1}}, order {{2}} is on its way." The values go in components, in the same order as the placeholders:

curl "https://graph.facebook.com/v23.0/PHONE_NUMBER_ID/messages" \
  -H "Authorization: Bearer $WHATSAPP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "messaging_product": "whatsapp",
    "recipient_type": "individual",
    "to": "+919811122233",
    "type": "template",
    "template": {
      "name": "order_shipped",
      "language": { "code": "en" },
      "components": [
        {
          "type": "body",
          "parameters": [
            { "type": "text", "text": "Priya" },
            { "type": "text", "text": "#1042" }
          ]
        }
      ]
    }
  }'

Two errors catch nearly everyone on their first real template:

  • 132001: the template doesn't exist in that language or isn't approved. en and en_US are different languages to Meta. Use the exact code the template was approved under.
  • 132000: the number of parameters doesn't match the number of placeholders.

Templates that use named parameters instead of numbers add a parameter_name to each value. How to write templates that get approved is its own topic; see our guide to WhatsApp message templates.

Send a free-form message inside the 24-hour window

When a customer messages you, a 24-hour customer service window opens, and it resets every time they write again. Inside it you can send ordinary messages with no template: text, images, documents, buttons, locations. Try it outside the window and the send fails with 131047 ("more than 24 hours have passed").

A plain text reply, with a link preview:

curl "https://graph.facebook.com/v23.0/PHONE_NUMBER_ID/messages" \
  -H "Authorization: Bearer $WHATSAPP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "messaging_product": "whatsapp",
    "recipient_type": "individual",
    "to": "+919811122233",
    "type": "text",
    "text": {
      "preview_url": true,
      "body": "Here is the tracking page: https://example.com/track/1042"
    }
  }'

The body can be up to 4,096 characters, and only the first URL gets a preview. To quote the customer's message in a reply bubble, add a context object with the wamid of the message you're answering:

{
  "messaging_product": "whatsapp",
  "recipient_type": "individual",
  "to": "+919811122233",
  "context": { "message_id": "wamid.HBgLMTY1MDM4Nzk0MzkVAgASGBQzQTdC..." },
  "type": "text",
  "text": { "body": "Yes, Thursday at 5 pm works. You're booked." }
}

Free-form replies are no longer automatically free. Since October 1, 2026 each business number gets 1,000 free service messages a month, then service messages are billed per message. The 24-hour window guide covers what opens the window and what changed.

What the response tells you (and what it doesn't)

A successful send returns HTTP 200 and a body like this:

{
  "messaging_product": "whatsapp",
  "contacts": [{ "input": "+919811122233", "wa_id": "919811122233" }],
  "messages": [{ "id": "wamid.HBgLMTY0NjcwNDM1OTUVAgARGBI1RjQy..." }]
}

Meta's documentation states it plainly: this response only means the API accepted your request. It says nothing about delivery. Three things to do with it:

  • Store messages[0].id (the wamid) against your own record. Every later status update refers to it.
  • Don't assume input equals wa_id. Meta notes they can differ, so key your records on the wamid and your own IDs, not on string-matching phone numbers.
  • Watch for message_status on template sends. It's added to the messages object when the template is being paced: accepted means it's going out, held_for_quality_assessment means Meta is holding it while it gathers feedback on the template.

Delivery comes later, through sent, delivered, read and failed events on your webhook. Our post on message status webhooks shows how to store them without getting confused by out-of-order arrivals.

Two more facts from Meta's docs that shape how you send:

  • Order isn't guaranteed. If message two must land after message one, wait for the delivered status of the first before sending the second.
  • There are speed limits. A number handles 80 messages per second by default (error 130429 above that), and Meta's pair rate limit allows about one message every 6 seconds to the same person (error 131056). Undelivered messages are retried for up to 30 days, or 10 minutes for authentication templates.

Send through Tenashi's API: a phone number or a group

Tenashi's developer API sends from a WhatsApp number your team has linked to Tenashi with a QR scan. Because it isn't Meta's Business Platform, there's no template, no 24-hour rule and no per-message fee. The trade-off is that sends are deliberately paced to look like a person typing.

Send to a phone number:

curl https://usetenashi.com/api/v1/messages \
  -H "Authorization: Bearer $TENASHI_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: booking-4821-reminder" \
  -d '{"to": "+919811122233", "text": "Hi Priya, your class is at 5 pm today."}'

Post into a WhatsApp group your number is already in: list your groups, take the conversation id, and send with conversationId instead of to.

curl "https://usetenashi.com/api/v1/conversations?filter=groups" \
  -H "Authorization: Bearer $TENASHI_KEY"

curl https://usetenashi.com/api/v1/messages \
  -H "Authorization: Bearer $TENASHI_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: batch-12-monday-reminder" \
  -d '{"conversationId": "cmh0a9s8d7f6g5h4j3k2l1", "text": "Reminder: the batch starts Monday at 7 am."}'

The response is 202 Accepted with the queued message: an id and status of queued. Keep the id to match the message.sent, message.delivered and message.read webhooks that follow, or poll GET /api/v1/messages/:id.

What to expect from the pacing: each number sends one message every 4 to 12 seconds, so a batch of 100 takes about 10 minutes and nothing is dropped. The API allows 50 new chats per number per day (people who never wrote to you), and the same exact text sent to 5 chats within 10 minutes is paused with 422 send_blocked, so personalise each message. If groups are the main reason you're here, the group messaging API guide compares Meta's Groups API with this approach.

Prefer a full walkthrough in your stack? We have step-by-step guides for the WhatsApp API in Node.js, Python, PHP and Java, plus testing the WhatsApp API in Postman before you write any code.

Where Tenashi fits

If your sends are replies and reminders to people who already talk to your team, or posts into groups your number is part of, Tenashi's API is the short path. You link the number you already use, call one endpoint, and every API message lands in the shared team inbox, so whoever picks up the reply sees the whole thread. There are no templates to get approved and no per-message fees; plans start from $18/mo and the API is included on every paid plan.

The honest limit: Tenashi is not Meta's official API, carries no green tick, and is the wrong tool for OTPs or thousands of messages a minute. Sends are paced on purpose, and while ban protection lowers the risk, WhatsApp can still ban a number that spams. For high-volume transactional traffic, use the Cloud API.

Read the API docs · Get started

FAQ

Do I need a template to send a WhatsApp message by API?

On Meta's Cloud API, yes, unless the customer messaged you in the last 24 hours. Inside that window you can send free-form text, media and buttons. Outside it, only an approved template goes through, and anything else fails with error 131047. Linked-number APIs such as Tenashi's don't use templates at all.

Why did the API return 200 but the message never arrived?

A 200 response only means Meta accepted the request. Delivery is reported later on your webhook as sent, delivered, read or failed. Check the failed event's error code, confirm the number had a plus sign and country code, and remember Meta holds messages for offline users for up to 30 days.

Can I send a WhatsApp message to a group by API?

Meta's Groups API can send to groups that your business creates through the API, for businesses with an Official Business Account, with invite-only joining and a small participant cap. It can't post into groups your number already belongs to. Tenashi's API can, using the group's conversationId.

What phone number format does the WhatsApp API expect?

Use the full international number with a plus sign, such as +919811122233. Spaces, hyphens and brackets are accepted. Without the plus sign, Meta prepends your business number's country code, so a US number sent from an Indian business can reach the wrong person. Normalise numbers to E.164 when you store them.

How many messages per second can I send with the Cloud API?

By default each registered number handles 80 messages per second, counting inbound and outbound. Meta upgrades eligible high-volume numbers to 1,000 automatically. Separately, the pair rate limit allows roughly one message every 6 seconds to the same user, and exceeding it returns error 131056 until you slow down.


The send call is the easy part; knowing what happened afterwards is where integrations get solid. For replies, reminders and group posts from a number people already recognise, start with Tenashi's API docs.