Guides

How to Send WhatsApp API Messages Programmatically (Code & API Guide 2026)

Published on
Updated on
18 min read
By Kapil Nagar
Developer sending WhatsApp API messages programmatically with code examples and error handling

How do I send WhatsApp API messages programmatically?

Send WhatsApp API messages programmatically via Cloud API — HTTP requests, templates, session messages, error handling, retry logic, and production patterns for Node, Python, and PHP. POST JSON to https://graph.facebook.com/v21.0/{PHONE_NUMBER_ID}/messages with Bearer token, E.164 recipient phone, and a template or text payload.

Last updated: 6 July 2026

Need to send WhatsApp API messages programmatically from your app, backend, or script? This developer guide covers the WhatsApp Cloud API endpoint, authentication, template payloads, session messages, interactive messages, error handling, and retry logic — the same patterns used by Twilio, 360dialog, and Ominiflow under the hood.

Written for backend engineers shipping production integrations — not hello-world tutorials. You'll see idempotent send patterns, rate-limit backoff, template variable pitfalls, and when to call Graph API directly vs route through a BSP.

Start here if you're in a hurry

  • Endpoint: POST https://graph.facebook.com/v21.0/{PHONE_NUMBER_ID}/messages
  • Auth: Authorization: Bearer {ACCESS_TOKEN}
  • Business-initiated messages require approved templates
  • Phone format: E.164 without + (e.g. 919876543210)
  • Return value includes messages[0].id — store for deduplication and status webhooks
  • Implement exponential backoff on error 130429 (rate limit)
  • Full architecture: integration guide

Prerequisites

  • WhatsApp Business Account and Phone Number ID from Meta
  • Permanent or long-lived access token (not short-lived dev token in production)
  • Approved message template (utility, marketing, or authentication)
  • Webhook URL configured for inbound messages and delivery statuses
  • WABA subscribed to your app via POST /{WABA_ID}/subscribed_apps

New to the platform? Start with WhatsApp Business API India onboarding on Ominiflow. Checklist: onboarding checklist.

Send a template message (HTTP POST)

curl -X POST \
  "https://graph.facebook.com/v21.0/PHONE_NUMBER_ID/messages" \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "messaging_product": "whatsapp",
    "recipient_type": "individual",
    "to": "919876543210",
    "type": "template",
    "template": {
      "name": "order_update",
      "language": { "code": "en" },
      "components": [{
        "type": "body",
        "parameters": [
          { "type": "text", "text": "ORD-12345" },
          { "type": "text", "text": "18 Jul 2026" }
        ]
      }]
    }
  }'

Successful response:

{
  "messaging_product": "whatsapp",
  "contacts": [{ "input": "919876543210", "wa_id": "919876543210" }],
  "messages": [{ "id": "wamid.HBgNOTE..." }]
}

Replace order_update with your exact approved template name. Parameter count must match Meta approval — error #132000 when mismatched. Template guide: template message API.

Template components — header, body, buttons

Templates with image header and URL button need additional components:

{
  "template": {
    "name": "order_shipped",
    "language": { "code": "en" },
    "components": [
      {
        "type": "header",
        "parameters": [
          { "type": "image", "image": { "link": "https://cdn.example.com/product.jpg" } }
        ]
      },
      {
        "type": "body",
        "parameters": [
          { "type": "text", "text": "Priya" },
          { "type": "text", "text": "ORD-88421" }
        ]
      },
      {
        "type": "button",
        "sub_type": "url",
        "index": "0",
        "parameters": [
          { "type": "text", "text": "track/88421" }
        ]
      }
    ]
  }
}

Send a session message (within 24 hours)

After a customer messages you, reply with free-form text inside the 24-hour customer service window:

{
  "messaging_product": "whatsapp",
  "to": "919876543210",
  "type": "text",
  "text": { "body": "Thanks for your order! Tracking: https://track.example/88421" }
}

Outside the 24-hour window, error 131047 (re-engagement required) — you must send an approved template instead.

Interactive messages (buttons and lists)

{
  "messaging_product": "whatsapp",
  "to": "919876543210",
  "type": "interactive",
  "interactive": {
    "type": "button",
    "body": { "text": "Confirm your COD order #ORD-88421 for ₹1,299?" },
    "action": {
      "buttons": [
        { "type": "reply", "reply": { "id": "confirm_yes", "title": "YES" } },
        { "type": "reply", "reply": { "id": "confirm_no", "title": "CANCEL" } }
      ]
    }
  }
}

COD flow: COD confirmation India

Production code — Node.js with retry

const PHONE_NUMBER_ID = process.env.WA_PHONE_ID;
const ACCESS_TOKEN = process.env.WA_ACCESS_TOKEN;
const API_URL = `https://graph.facebook.com/v21.0/${PHONE_NUMBER_ID}/messages`;

async function sendTemplate(to, templateName, bodyParams, retries = 3) {
  const payload = {
    messaging_product: 'whatsapp',
    to,
    type: 'template',
    template: {
      name: templateName,
      language: { code: 'en' },
      components: [{
        type: 'body',
        parameters: bodyParams.map(text => ({ type: 'text', text })),
      }],
    },
  };

  for (let attempt = 0; attempt < retries; attempt++) {
    const res = await fetch(API_URL, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${ACCESS_TOKEN}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(payload),
    });
    const data = await res.json();

    if (res.ok) {
      return { messageId: data.messages[0].id, raw: data };
    }

    const code = data?.error?.code;
    if (code === 130429) {
      const delay = Math.pow(2, attempt) * 1000 + Math.random() * 500;
      await new Promise(r => setTimeout(r, delay));
      continue;
    }
    throw new Error(`WA send failed: ${code} ${data?.error?.message}`);
  }
  throw new Error('WA send failed after retries (rate limit)');
}

Python with error handling

import time
import requests

def send_template(to: str, template_name: str, params: list[str], max_retries: int = 3):
    url = f"https://graph.facebook.com/v21.0/{PHONE_NUMBER_ID}/messages"
    headers = {"Authorization": f"Bearer {ACCESS_TOKEN}"}
    payload = {
        "messaging_product": "whatsapp",
        "to": to,
        "type": "template",
        "template": {
            "name": template_name,
            "language": {"code": "en"},
            "components": [{
                "type": "body",
                "parameters": [{"type": "text", "text": p} for p in params],
            }],
        },
    }

    for attempt in range(max_retries):
        r = requests.post(url, json=payload, headers=headers, timeout=10)
        data = r.json()

        if r.ok:
            return data["messages"][0]["id"]

        code = data.get("error", {}).get("code")
        if code == 130429:
            time.sleep(2 ** attempt + 0.5)
            continue

        raise RuntimeError(f"WA error {code}: {data.get('error', {}).get('message')}")

    raise RuntimeError("Rate limit exceeded after retries")

PHP with idempotency key

function sendWhatsAppTemplate(
    string $to,
    string $templateName,
    array $bodyParams,
    string $idempotencyKey
): array {
    // Check DB: if idempotencyKey exists, return cached message_id
    if ($cached = getCachedSend($idempotencyKey)) {
        return $cached;
    }

    $payload = [
        'messaging_product' => 'whatsapp',
        'to' => $to,
        'type' => 'template',
        'template' => [
            'name' => $templateName,
            'language' => ['code' => 'en'],
            'components' => [[
                'type' => 'body',
                'parameters' => array_map(
                    fn($t) => ['type' => 'text', 'text' => $t],
                    $bodyParams
                ),
            ]],
        ],
    ];

    $ch = curl_init("https://graph.facebook.com/v21.0/" . PHONE_NUMBER_ID . "/messages");
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer ' . ACCESS_TOKEN,
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS => json_encode($payload),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 15,
    ]);
    $response = json_decode(curl_exec($ch), true);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($httpCode >= 200 && $httpCode < 300) {
        cacheSend($idempotencyKey, $response);
        return $response;
    }
    throw new RuntimeException(json_encode($response['error'] ?? $response));
}

Use order ID + event type as idempotency key — prevents duplicate sends when your webhook or queue retries.

Common API errors and fixes

CodeMeaningFix
131026Template not found or not approvedCheck exact template name and language code
132000Template param count mismatch3 placeholders in template, 2 in payload
131047Outside 24h windowSend approved template to re-open
130429Rate limitExponential backoff; throttle bulk sends
131031Account lockedQuality review — pause marketing
100Invalid parameterCheck E.164 format, remove + prefix

More: common API mistakes

Retry logic — production rules

  • Retry: 130429 (rate limit), 5xx from Graph API, network timeouts
  • Do not retry: 131026, 132000, 131047, 100 — fix payload first
  • Backoff: exponential with jitter — 2^attempt * 1000ms + random(500ms)
  • Idempotency: store message.id keyed by business event ID
  • Dead letter queue: after 3 failures, alert ops — don't infinite loop

Production tip: Log template name + recipient hash (not full phone) + error code on every failure. You'll debug Diwali incidents at 2 AM with this, not with "it didn't work."

Receive messages and statuses

  1. Configure webhook URL in Meta App Dashboard or Ominiflow
  2. Verify GET challenge with hub.verify_token
  3. Parse POST payload for messages and statuses arrays
  4. Verify X-Hub-Signature-256 on every POST
  5. Return HTTP 200 in under 5 seconds; queue heavy work

Deep dive: webhook integration guide

Direct API vs BSP

ApproachYou ownBest for
Direct Graph APITokens, webhooks, inbox, templates, rate limitsProduct companies with backend team
OminiflowBusiness logic; BSP handles infra + inbox + botsTeams wanting API power without building everything
Twilio / 360dialogSDK + API; less inbox/bot toolingDeveloper-only messaging layer

Send messages via Ominiflow

Get API credentials, approved templates, and webhook setup in one platform. REST API mirrors Graph patterns with inbox and bot tooling on top.

Get API Access →

Related: integration guide · OTP templates · API pricing

FAQ

How do I send a WhatsApp API message programmatically? expand_more

POST JSON to graph.facebook.com/v21.0/{PHONE_NUMBER_ID}/messages with Bearer token, recipient in E.164 without +, and either a template (business-initiated) or text (within 24h session).

Can I send WhatsApp messages without a template? expand_more

Only when replying inside the 24-hour window after the user messaged you first. Otherwise you need an approved template.

What is the WhatsApp API send message endpoint? expand_more

POST /v21.0/{phone-number-id}/messages on graph.facebook.com with messaging_product set to whatsapp.

How do I handle rate limits? expand_more

Implement exponential backoff on error 130429. Throttle bulk sends. Don't retry validation errors (132000, 131026).

Should I use idempotency keys? expand_more

Yes &mdash; key on business event ID (order_id + event_type) to prevent duplicate messages when queues or webhooks retry.

Is Twilio WhatsApp API different from Meta? expand_more

Twilio wraps Meta's API with their SDK. Underlying rules (templates, opt-in, 24h window) are identical.

How do I know if a message was delivered? expand_more

Subscribe to statuses webhooks &mdash; sent, delivered, read, failed. Store message.id from send response to correlate. Last updated July 2026. Developer guide for sending WhatsApp API messages programmatically &mdash; verified against Meta Graph API v21.0 documentation.

Need help? Chat with AI