Nurture TechnologiesNurture Tech
Integrations19 min read·July 16, 2026

WhatsApp Business API Integration GuideArchitecture, Automation, Security, and Implementation

WhatsApp APINode.jsFastAPILaravelOpenAI

Everything you need to integrate the WhatsApp Business API into a production system, from Meta App setup and webhook handling to AI automation, CRM sync, security, and a deployment checklist.

WhatsApp is where your customers already are. With over two billion active users, it is the default communication channel in most markets outside North America, and increasingly a preferred channel inside them. Businesses that respond to customer inquiries on WhatsApp in minutes close deals faster, reduce churn, and earn higher satisfaction scores than those routing customers through email or web forms.

The WhatsApp Business API (now part of the Meta Cloud API) makes it possible to build production-grade integrations: automated support bots, lead qualification flows, appointment booking, CRM synchronization, order status notifications, and multi-agent support dashboards. Unlike the WhatsApp Business App, the API has no GUI, it is designed entirely for programmatic access and scales to millions of messages.

This guide is built for the people who build and operate these systems. Business owners and operations leaders will find the architecture section and automation examples most useful. Developers and architects will find working code for Node.js, NestJS, Laravel, and FastAPI, along with webhook security, CRM integration patterns, and a production checklist.

Overview

The WhatsApp Business API is a cloud-based API provided by Meta that lets businesses send and receive WhatsApp messages programmatically. Access is through the Meta Cloud API (hosted by Meta) or through a Business Solution Provider (BSP). The Cloud API is the recommended path for new integrations, it is free to use, does not require third-party hosting fees, and gives you direct access to the latest features.

The pricing model is conversation-based. Meta charges per 24-hour conversation window, not per message. Conversations are categorized as marketing, utility, authentication, or service. Service conversations (user-initiated) are free in most regions. Marketing conversations (business-initiated promotions) carry a per-conversation charge that varies by country. You can find current rates on the Meta for Developers pricing page.

  • Automated customer support that handles FAQs and common requests without human agents
  • Lead qualification flows that collect information and score leads before routing to sales
  • Appointment booking with real-time availability checks and confirmation messages
  • Order status updates, shipping notifications, and payment confirmations
  • Multi-agent support dashboards where human agents handle escalations from the bot
  • CRM synchronization that creates and updates contact records from WhatsApp conversations
  • AI-powered assistants that use your knowledge base to answer product and support questions

Business Benefits

Automated Customer Support

A WhatsApp bot handles the questions your team answers dozens of times per day, pricing, availability, policy questions, order status, without human involvement. The bot responds in seconds, at any hour, in any volume. Support teams focus on the conversations that actually require human judgment, and first-response times drop from hours to seconds across the board.

Faster Lead Qualification

Inbound leads often go cold while they wait for a sales rep to respond. A WhatsApp qualification flow asks the right questions immediately, scores the lead based on the answers, and routes high-intent prospects to a sales rep within minutes of their first message. Low-intent leads enter a nurture sequence automatically. The pipeline fills faster with better-qualified opportunities.

Improved Customer Experience

Customers prefer WhatsApp over web chat, email, and phone for everyday support interactions. They are already on the app. The conversation is persistent, they can pick it up hours later. Responses arrive as push notifications. WhatsApp integration meets customers where they are, in the channel they already use, which consistently produces higher satisfaction scores than any other support channel.

Reduced Operational Costs

Automation handles the high-volume, low-complexity interactions that currently consume support capacity. A single WhatsApp bot integration typically deflects 40 to 60 percent of inbound tickets in the first 90 days. The support team handles more complex conversations in the same headcount. For businesses paying per-agent SaaS fees on support platforms, the cost reduction is immediate and measurable.

Automated Notifications

WhatsApp's open rate for business messages exceeds 90 percent, far above email's 20 to 25 percent average. Order confirmations, appointment reminders, payment receipts, shipping updates, and renewal notices delivered via WhatsApp are read almost immediately. Template messages (pre-approved by Meta) enable business-initiated outbound notifications to users who have opted in.

CRM Synchronization

Every WhatsApp conversation is a source of customer data. Phone number, intent, conversation history, lead score, support ticket content, all of this can be written to your CRM in real time. Contacts created from WhatsApp flows arrive in HubSpot, Salesforce, or Zoho already enriched with context, eliminating the manual data entry that slows every sales and support team.

Architecture Overview

WhatsApp API integration follows a webhook-driven architecture. When a customer sends a message, Meta POSTs the event to your webhook endpoint. Your backend processes the message, applies business logic, and responds by calling the WhatsApp Graph API. The key constraint: your webhook must respond with HTTP 200 within five seconds. All processing that takes longer must be handed off to an async queue.

whatsapp-architecture.mmd
flowchart LR
  Customer["Customer"] --> WA["WhatsApp"]
  WA --> Hook["Webhook\nPOST /webhook"]
  Hook --> BE["Backend API\nNode.js / FastAPI"]
  BE --> SIG["Signature\nValidation"]
  BE --> Queue["Message Queue\nRedis / BullMQ"]
  Queue --> AI["AI Engine\nOpenAI / Claude"]
  Queue --> CRM["CRM\nHubSpot / Salesforce"]
  AI --> Send["Send Message\nGraph API"]
  CRM --> Send
  Send --> WA
  BE --> DB["Database\nPostgreSQL"]
  BE --> MON["Monitoring\nSentry / Datadog"]

The webhook must return HTTP 200 within 5 seconds. If it does not, Meta retries the delivery. Move all processing, AI calls, CRM writes, database operations, to an async queue after acknowledging receipt.

Message Flow

  1. 1Customer sends a WhatsApp message to your business number
  2. 2Meta delivers the message as a POST request to your webhook URL
  3. 3Your backend validates the X-Hub-Signature-256 header using your App Secret
  4. 4Webhook handler returns HTTP 200 immediately and enqueues the message for processing
  5. 5Queue worker processes the message: calls AI, queries the database, checks session state
  6. 6Backend calls the Graph API to send the response back to the customer
  7. 7Conversation state, contact data, and logs are persisted to the database and CRM

Frontend Integration

The frontend for a WhatsApp integration is typically an internal dashboard, used by support agents to read conversation history, assign chats, and send replies. Customers interact entirely through WhatsApp; the frontend is for your team. Here are the key patterns for building that dashboard across popular stacks.

React, Conversation Dashboard

components/ConversationList.tsx
import { useState, useEffect } from 'react'

interface Conversation {
  id: string
  phone: string
  contactName: string
  lastMessage: string
  lastMessageAt: string
  status: 'open' | 'resolved' | 'pending'
  assignedTo?: string
}

export function ConversationList() {
  const [conversations, setConversations] = useState<Conversation[]>([])
  const [selected, setSelected] = useState<string | null>(null)

  useEffect(() => {
    const fetchConversations = async () => {
      const res = await fetch('/api/whatsapp/conversations')
      const data = await res.json()
      setConversations(data)
    }

    fetchConversations()
    // Poll for new conversations every 5 seconds
    const interval = setInterval(fetchConversations, 5000)
    return () => clearInterval(interval)
  }, [])

  return (
    <div className="flex h-screen">
      <aside className="w-80 border-r overflow-y-auto">
        {conversations.map((c) => (
          <button
            key={c.id}
            onClick={() => setSelected(c.id)}
            className="w-full p-4 text-left hover:bg-gray-50 border-b"
          >
            <div className="font-semibold">{c.contactName || c.phone}</div>
            <div className="text-sm text-gray-500 truncate">{c.lastMessage}</div>
          </button>
        ))}
      </aside>
      <main className="flex-1">
        {selected ? <ChatWindow conversationId={selected} /> : (
          <div className="flex items-center justify-center h-full text-gray-400">
            Select a conversation
          </div>
        )}
      </main>
    </div>
  )
}

Next.js, Agent Reply API Route

app/api/whatsapp/reply/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'

export async function POST(req: NextRequest) {
  const session = await getServerSession()
  if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

  const { phone, message, conversationId } = await req.json()

  if (!phone || !message) {
    return NextResponse.json({ error: 'Missing required fields' }, { status: 400 })
  }

  // Send via your backend WhatsApp service
  const res = await fetch(process.env.API_BASE_URL + '/whatsapp/send', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: 'Bearer ' + process.env.INTERNAL_API_KEY,
    },
    body: JSON.stringify({ phone, message, conversationId, agentId: session.user?.email }),
  })

  if (!res.ok) return NextResponse.json({ error: 'Failed to send' }, { status: 502 })

  return NextResponse.json({ success: true })
}

Vue.js, Real-time Chat with WebSocket

composables/useWhatsAppChat.ts
import { ref, onUnmounted } from 'vue'

export function useWhatsAppChat(conversationId: string) {
  const messages = ref<{ from: string; body: string; timestamp: string }[]>([])
  const connected = ref(false)

  const ws = new WebSocket(
    (import.meta.env.VITE_WS_URL || 'ws://localhost:3001') + '/whatsapp/' + conversationId
  )

  ws.onopen = () => { connected.value = true }
  ws.onclose = () => { connected.value = false }

  ws.onmessage = (event) => {
    const msg = JSON.parse(event.data)
    messages.value.push(msg)
  }

  const sendReply = async (text: string) => {
    await fetch('/api/whatsapp/reply', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ conversationId, message: text }),
    })
  }

  onUnmounted(() => ws.close())

  return { messages, connected, sendReply }
}

Angular

src/app/services/whatsapp.service.ts
import { Injectable } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { Observable } from 'rxjs'

interface Conversation { id: string; phone: string; lastMessage: string }

@Injectable({ providedIn: 'root' })
export class WhatsAppService {
  constructor(private http: HttpClient) {}

  getConversations(): Observable<Conversation[]> {
    return this.http.get<Conversation[]>('/api/whatsapp/conversations')
  }

  sendReply(conversationId: string, message: string): Observable<{ success: boolean }> {
    return this.http.post<{ success: boolean }>('/api/whatsapp/reply', { conversationId, message })
  }
}

Svelte

src/components/ReplyBox.svelte
<script lang="ts">
  export let conversationId: string
  let reply = ''
  let sending = false
  let sent = false

  async function send() {
    if (!reply.trim()) return
    sending = true
    await fetch('/api/whatsapp/reply', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ conversationId, message: reply }),
    })
    reply = ''
    sending = false
    sent = true
    setTimeout(() => (sent = false), 2000)
  }
</script>

<textarea bind:value={reply} placeholder="Type a reply..." />
<button on:click={send} disabled={sending}>
  {sending ? 'Sending...' : sent ? 'Sent ✓' : 'Send'}
</button>

Solid.js

src/components/ReplyBox.tsx
import { createSignal } from 'solid-js'

export function ReplyBox({ conversationId }: { conversationId: string }) {
  const [reply, setReply] = createSignal('')
  const [sending, setSending] = createSignal(false)

  const send = async () => {
    if (!reply().trim()) return
    setSending(true)
    await fetch('/api/whatsapp/reply', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ conversationId, message: reply() }),
    })
    setReply('')
    setSending(false)
  }

  return (
    <div>
      <textarea value={reply()} onInput={(e) => setReply(e.currentTarget.value)} />
      <button onClick={send} disabled={sending()}>
        {sending() ? 'Sending...' : 'Send'}
      </button>
    </div>
  )
}

Qwik

src/components/ReplyBox.tsx
import { component$, useSignal, $ } from '@builder.io/qwik'

export const ReplyBox = component$<{ conversationId: string }>(({ conversationId }) => {
  const reply = useSignal('')
  const sending = useSignal(false)

  const send = $(async () => {
    if (!reply.value.trim()) return
    sending.value = true
    await fetch('/api/whatsapp/reply', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ conversationId, message: reply.value }),
    })
    reply.value = ''
    sending.value = false
  })

  return (
    <div>
      <textarea value={reply.value} onInput$={(e) => (reply.value = (e.target as HTMLTextAreaElement).value)} />
      <button onClick$={send} disabled={sending.value}>
        {sending.value ? 'Sending...' : 'Send'}
      </button>
    </div>
  )
})

Astro

src/components/ReplyBox.astro
---
const { conversationId } = Astro.props
---
<div data-conversation-id={conversationId}>
  <textarea id="reply-text" placeholder="Type a reply..."></textarea>
  <button id="reply-send">Send</button>
</div>

<script>
  const container = document.querySelector('[data-conversation-id]') as HTMLElement
  const conversationId = container?.dataset.conversationId

  document.getElementById('reply-send')?.addEventListener('click', async () => {
    const textarea = document.getElementById('reply-text') as HTMLTextAreaElement
    const message = textarea.value.trim()
    if (!message) return
    await fetch('/api/whatsapp/reply', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ conversationId, message }),
    })
    textarea.value = ''
  })
</script>

Backend Integration

The backend is where all WhatsApp logic runs. It handles webhook verification, message parsing, business logic, and outbound message sending via the Graph API. Below are production-ready implementations across four stacks.

Node.js / Express, Webhook Handler

$npm install axios crypto-js
src/webhooks/whatsapp.ts
import crypto from 'crypto'
import axios from 'axios'
import type { Request, Response } from 'express'

const BASE_URL = 'https://graph.facebook.com/v19.0'

// GET, Meta sends this once to verify your endpoint
export function verifyWebhook(req: Request, res: Response) {
  const mode = req.query['hub.mode']
  const token = req.query['hub.verify_token']
  const challenge = req.query['hub.challenge']

  if (mode === 'subscribe' && token === process.env.WEBHOOK_VERIFY_TOKEN) {
    return res.status(200).send(challenge)
  }
  return res.status(403).send('Forbidden')
}

// POST, incoming messages and status updates
export async function handleWebhook(req: Request, res: Response) {
  // Always acknowledge immediately
  res.status(200).send('EVENT_RECEIVED')

  const body = req.body
  if (body.object !== 'whatsapp_business_account') return

  for (const entry of body.entry ?? []) {
    for (const change of entry.changes ?? []) {
      const value = change.value
      const phoneNumberId = value.metadata?.phone_number_id

      for (const message of value.messages ?? []) {
        await enqueueMessage({ message, phoneNumberId })
      }
    }
  }
}

// Send a plain text message
export async function sendTextMessage(to: string, text: string, phoneNumberId: string) {
  await axios.post(
    BASE_URL + '/' + phoneNumberId + '/messages',
    {
      messaging_product: 'whatsapp',
      recipient_type: 'individual',
      to,
      type: 'text',
      text: { preview_url: false, body: text },
    },
    { headers: { Authorization: 'Bearer ' + process.env.WHATSAPP_ACCESS_TOKEN } }
  )
}

// Send a pre-approved template message
export async function sendTemplate(to: string, template: string, params: string[]) {
  await axios.post(
    BASE_URL + '/' + process.env.PHONE_NUMBER_ID + '/messages',
    {
      messaging_product: 'whatsapp',
      to,
      type: 'template',
      template: {
        name: template,
        language: { code: 'en_US' },
        components: [{
          type: 'body',
          parameters: params.map((text) => ({ type: 'text', text })),
        }],
      },
    },
    { headers: { Authorization: 'Bearer ' + process.env.WHATSAPP_ACCESS_TOKEN } }
  )
}

NestJS

src/whatsapp/whatsapp.service.ts
import { Injectable, Logger } from '@nestjs/common'
import { HttpService } from '@nestjs/axios'
import { firstValueFrom } from 'rxjs'

@Injectable()
export class WhatsAppService {
  private readonly logger = new Logger(WhatsAppService.name)
  private readonly baseUrl = 'https://graph.facebook.com/v19.0'

  constructor(private readonly http: HttpService) {}

  async sendMessage(to: string, text: string): Promise<void> {
    const url = this.baseUrl + '/' + process.env.PHONE_NUMBER_ID + '/messages'
    try {
      await firstValueFrom(
        this.http.post(
          url,
          { messaging_product: 'whatsapp', to, type: 'text', text: { body: text } },
          { headers: { Authorization: 'Bearer ' + process.env.WHATSAPP_ACCESS_TOKEN } }
        )
      )
    } catch (error) {
      this.logger.error('Failed to send WhatsApp message', { to, error })
      throw error
    }
  }

  async sendTemplate(to: string, name: string, params: string[]): Promise<void> {
    const url = this.baseUrl + '/' + process.env.PHONE_NUMBER_ID + '/messages'
    await firstValueFrom(
      this.http.post(
        url,
        {
          messaging_product: 'whatsapp',
          to,
          type: 'template',
          template: {
            name,
            language: { code: 'en_US' },
            components: [{ type: 'body', parameters: params.map((t) => ({ type: 'text', text: t })) }],
          },
        },
        { headers: { Authorization: 'Bearer ' + process.env.WHATSAPP_ACCESS_TOKEN } }
      )
    )
  }
}

Python FastAPI

$pip install fastapi httpx python-dotenv
routers/whatsapp.py
import os, hashlib, hmac, httpx
from fastapi import APIRouter, Request, Response, HTTPException, BackgroundTasks

router = APIRouter()
BASE_URL = "https://graph.facebook.com/v19.0"

@router.get("/webhook")
async def verify(request: Request):
    p = dict(request.query_params)
    if p.get("hub.mode") == "subscribe" and p.get("hub.verify_token") == os.environ["WEBHOOK_VERIFY_TOKEN"]:
        return Response(content=p["hub.challenge"])
    raise HTTPException(status_code=403)

@router.post("/webhook")
async def handle(request: Request, background: BackgroundTasks):
    body = await request.json()
    # Respond immediately, process in background
    if body.get("object") == "whatsapp_business_account":
        for entry in body.get("entry", []):
            for change in entry.get("changes", []):
                for msg in change.get("value", {}).get("messages", []):
                    background.add_task(process_message, msg)
    return {"status": "ok"}

async def send_message(to: str, text: str):
    url = f"{BASE_URL}/{os.environ['PHONE_NUMBER_ID']}/messages"
    async with httpx.AsyncClient() as client:
        await client.post(
            url,
            json={"messaging_product": "whatsapp", "to": to, "type": "text", "text": {"body": text}},
            headers={"Authorization": "Bearer " + os.environ["WHATSAPP_ACCESS_TOKEN"]},
        )

Laravel

$composer require guzzlehttp/guzzle
app/Services/WhatsAppService.php
<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class WhatsAppService
{
    private string $baseUrl;
    private string $token;

    public function __construct()
    {
        $phoneId = config('whatsapp.phone_number_id');
        $this->baseUrl = "https://graph.facebook.com/v19.0/{$phoneId}";
        $this->token = config('whatsapp.access_token');
    }

    public function sendMessage(string $to, string $text): void
    {
        Http::withToken($this->token)
            ->post("{$this->baseUrl}/messages", [
                'messaging_product' => 'whatsapp',
                'to' => $to,
                'type' => 'text',
                'text' => ['body' => $text],
            ]);
    }

    public function sendTemplate(string $to, string $template, array $params = []): void
    {
        Http::withToken($this->token)
            ->post("{$this->baseUrl}/messages", [
                'messaging_product' => 'whatsapp',
                'to' => $to,
                'type' => 'template',
                'template' => [
                    'name' => $template,
                    'language' => ['code' => 'en_US'],
                    'components' => [[
                        'type' => 'body',
                        'parameters' => array_map(
                            fn($p) => ['type' => 'text', 'text' => $p],
                            $params
                        ),
                    ]],
                ],
            ]);
    }
}

Django

$pip install requests django
whatsapp/views.py
import os, json, hmac, hashlib, requests
from django.http import JsonResponse, HttpResponse
from django.views.decorators.csrf import csrf_exempt

BASE_URL = f"https://graph.facebook.com/v19.0/{os.environ['PHONE_NUMBER_ID']}"
HEADERS = {"Authorization": f"Bearer {os.environ['WHATSAPP_ACCESS_TOKEN']}"}

@csrf_exempt
def webhook(request):
    if request.method == 'GET':
        p = request.GET
        if p.get('hub.mode') == 'subscribe' and p.get('hub.verify_token') == os.environ['WEBHOOK_VERIFY_TOKEN']:
            return HttpResponse(p['hub.challenge'])
        return HttpResponse(status=403)

    body = json.loads(request.body)
    for entry in body.get('entry', []):
        for change in entry.get('changes', []):
            for msg in change.get('value', {}).get('messages', []):
                process_message(msg)
    return JsonResponse({'status': 'ok'})

def send_message(to: str, text: str):
    requests.post(f"{BASE_URL}/messages", headers=HEADERS, json={
        'messaging_product': 'whatsapp', 'to': to,
        'type': 'text', 'text': {'body': text},
    })

def process_message(msg: dict):
    if msg.get('type') == 'text':
        send_message(msg['from'], f"Thanks for your message: {msg['text']['body']}")

Go

$go get github.com/go-chi/chi/v5
handlers/whatsapp.go
package handlers

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
    "os"
)

var (
    baseURL = fmt.Sprintf("https://graph.facebook.com/v19.0/%s", os.Getenv("PHONE_NUMBER_ID"))
    token   = os.Getenv("WHATSAPP_ACCESS_TOKEN")
)

func Webhook(w http.ResponseWriter, r *http.Request) {
    if r.Method == http.MethodGet {
        q := r.URL.Query()
        if q.Get("hub.mode") == "subscribe" && q.Get("hub.verify_token") == os.Getenv("WEBHOOK_VERIFY_TOKEN") {
            w.Write([]byte(q.Get("hub.challenge")))
            return
        }
        http.Error(w, "Forbidden", http.StatusForbidden)
        return
    }

    var body map[string]any
    json.NewDecoder(r.Body).Decode(&body)
    // Process messages asynchronously
    go processWebhook(body)
    w.WriteHeader(http.StatusOK)
}

func SendMessage(to, text string) error {
    payload, _ := json.Marshal(map[string]any{
        "messaging_product": "whatsapp", "to": to,
        "type": "text", "text": map[string]string{"body": text},
    })
    req, _ := http.NewRequest(http.MethodPost, baseURL+"/messages", bytes.NewBuffer(payload))
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("Authorization", "Bearer "+token)
    _, err := http.DefaultClient.Do(req)
    return err
}

Spring Boot

WhatsAppController.java
@RestController
@RequestMapping("/api/whatsapp")
public class WhatsAppController {

    private final String baseUrl;
    private final String token;

    public WhatsAppController() {
        this.baseUrl = "https://graph.facebook.com/v19.0/" + System.getenv("PHONE_NUMBER_ID");
        this.token = System.getenv("WHATSAPP_ACCESS_TOKEN");
    }

    @GetMapping("/webhook")
    public ResponseEntity<String> verify(@RequestParam Map<String, String> params) {
        if ("subscribe".equals(params.get("hub.mode")) &&
            System.getenv("WEBHOOK_VERIFY_TOKEN").equals(params.get("hub.verify_token"))) {
            return ResponseEntity.ok(params.get("hub.challenge"));
        }
        return ResponseEntity.status(403).build();
    }

    @PostMapping("/webhook")
    public ResponseEntity<Map<String, String>> handle(@RequestBody Map<String, Object> body) {
        // Process async in production
        return ResponseEntity.ok(Map.of("status", "ok"));
    }

    public void sendMessage(String to, String text) {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.setBearerAuth(token);
        Map<String, Object> payload = Map.of(
            "messaging_product", "whatsapp", "to", to,
            "type", "text", "text", Map.of("body", text)
        );
        new RestTemplate().exchange(baseUrl + "/messages", HttpMethod.POST, new HttpEntity<>(payload, headers), Map.class);
    }
}

ASP.NET Core

WhatsAppController.cs
[ApiController]
[Route("api/whatsapp")]
public class WhatsAppController : ControllerBase
{
    private readonly HttpClient _http;
    private readonly string _baseUrl;

    public WhatsAppController(IHttpClientFactory factory)
    {
        _http = factory.CreateClient();
        _http.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("WHATSAPP_ACCESS_TOKEN"));
        _baseUrl = $"https://graph.facebook.com/v19.0/{Environment.GetEnvironmentVariable("PHONE_NUMBER_ID")}";
    }

    [HttpGet("webhook")]
    public IActionResult Verify([FromQuery(Name = "hub.mode")] string mode,
                                 [FromQuery(Name = "hub.verify_token")] string verifyToken,
                                 [FromQuery(Name = "hub.challenge")] string challenge)
    {
        if (mode == "subscribe" && verifyToken == Environment.GetEnvironmentVariable("WEBHOOK_VERIFY_TOKEN"))
            return Content(challenge);
        return Forbid();
    }

    [HttpPost("webhook")]
    public async Task<IActionResult> Handle([FromBody] JsonElement body)
    {
        // Parse and dispatch messages asynchronously
        return Ok(new { status = "ok" });
    }

    public async Task SendMessage(string to, string text)
    {
        var payload = new { messaging_product = "whatsapp", to, type = "text", text = new { body = text } };
        await _http.PostAsJsonAsync(_baseUrl + "/messages", payload);
    }
}

Rust

$# Cargo.toml: axum = "0.7", reqwest = { features = ["json"] }, serde_json = "1"
src/handlers/whatsapp.rs
use axum::{extract::Query, Json};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::collections::HashMap;

#[derive(Deserialize)]
pub struct VerifyQuery {
    #[serde(rename = "hub.mode")]
    pub mode: Option<String>,
    #[serde(rename = "hub.verify_token")]
    pub verify_token: Option<String>,
    #[serde(rename = "hub.challenge")]
    pub challenge: Option<String>,
}

pub async fn verify(Query(params): Query<VerifyQuery>) -> String {
    let expected = std::env::var("WEBHOOK_VERIFY_TOKEN").unwrap();
    if params.mode.as_deref() == Some("subscribe") && params.verify_token.as_deref() == Some(&expected) {
        return params.challenge.unwrap_or_default();
    }
    "Forbidden".to_string()
}

pub async fn send_whatsapp_message(client: &Client, to: &str, text: &str) {
    let phone_id = std::env::var("PHONE_NUMBER_ID").unwrap();
    let token = std::env::var("WHATSAPP_ACCESS_TOKEN").unwrap();
    let _ = client
        .post(format!("https://graph.facebook.com/v19.0/{phone_id}/messages"))
        .bearer_auth(&token)
        .json(&json!({"messaging_product": "whatsapp", "to": to, "type": "text", "text": {"body": text}}))
        .send().await;
}

Authentication

WhatsApp Business API authentication involves three components: a Meta App with the WhatsApp product added, a System User Access Token for API calls, and a Webhook Verify Token you define for endpoint validation. None of these should ever appear in client-side code or version control.

Meta App Setup

  1. 1Create a Meta App at developers.facebook.com, select Business type
  2. 2Add the WhatsApp product to your app
  3. 3In WhatsApp Settings, add and verify your business phone number
  4. 4Generate a System User and assign it to your App with admin permissions
  5. 5Create a permanent System User Access Token (not the temporary test token)
  6. 6Set your webhook URL and Verify Token in the WhatsApp Configuration tab
  7. 7Subscribe to the messages webhook field

Environment Variables

.env
# Meta / WhatsApp credentials, never commit these
WHATSAPP_ACCESS_TOKEN=EAAxxxxxxxxxxxxxx
WHATSAPP_APP_SECRET=xxxxxxxxxxxxxxxx
PHONE_NUMBER_ID=1234567890
WHATSAPP_BUSINESS_ACCOUNT_ID=0987654321

# Token you define, must match what you enter in the Meta dashboard
WEBHOOK_VERIFY_TOKEN=my-secret-verify-token-2026

# Optional: rate limiting and session management
REDIS_URL=redis://localhost:6379

Webhook Signature Validation

Every POST request Meta sends to your webhook includes an X-Hub-Signature-256 header, an HMAC-SHA256 hash of the request body using your App Secret as the key. You must validate this signature on every request. Skipping validation means any attacker who discovers your webhook URL can send fabricated events.

middleware/validateWebhook.ts
import crypto from 'crypto'
import type { Request, Response, NextFunction } from 'express'

export function validateWhatsAppWebhook(req: Request, res: Response, next: NextFunction) {
  const signature = req.headers['x-hub-signature-256'] as string

  if (!signature) {
    return res.status(401).json({ error: 'Missing signature header' })
  }

  const expected =
    'sha256=' +
    crypto
      .createHmac('sha256', process.env.WHATSAPP_APP_SECRET!)
      .update(JSON.stringify(req.body))
      .digest('hex')

  // Use timingSafeEqual to prevent timing attacks
  const sigBuffer = Buffer.from(signature)
  const expBuffer = Buffer.from(expected)

  if (sigBuffer.length !== expBuffer.length || !crypto.timingSafeEqual(sigBuffer, expBuffer)) {
    return res.status(401).json({ error: 'Invalid signature' })
  }

  next()
}

Access Token Rotation

System User tokens generated in Meta Business Manager do not expire, but you should rotate them after any suspected exposure, when team members with access depart, and as part of a regular annual security review. Store the token in a secrets manager (AWS Secrets Manager, HashiCorp Vault, Vercel Environment Variables), not in .env files on production servers.

Automation Examples

The highest-value WhatsApp integrations automate repetitive, high-volume processes. Here are three complete automation patterns with working code.

Lead Qualification Flow

When a new customer sends a first message, the bot collects key information through a guided conversation, scores the lead, and routes it to the CRM, all before a sales rep is involved.

automation/leadQualification.ts
const FLOW: Record<string, { question: string; nextStage: string }> = {
  START:   { question: 'Welcome! What is your name?', nextStage: 'NAME' },
  NAME:    { question: 'Great! What is your company name?', nextStage: 'COMPANY' },
  COMPANY: { question: 'What is your estimated budget?

1. Under $5k
2. $5k – $20k
3. Over $20k', nextStage: 'BUDGET' },
  BUDGET:  { question: 'What problem are you trying to solve?', nextStage: 'PROBLEM' },
  PROBLEM: { question: '', nextStage: 'COMPLETE' },
}

export async function handleLeadFlow(phone: string, message: string) {
  const session = await getSession(phone) ?? { stage: 'START', data: {} }
  const { stage, data } = session

  // Save the user's answer for the current stage
  const updatedData = { ...data, [stage.toLowerCase()]: message.trim() }

  const next = FLOW[stage]
  if (!next) return

  if (next.nextStage === 'COMPLETE') {
    const score = scoreLead(updatedData)
    await syncToHubSpot(phone, updatedData, score)
    await sendTextMessage(phone,
      'Thank you, ' + updatedData.name + '! Our team will reach out within 24 hours.'
    )
    await clearSession(phone)
  } else {
    await setSession(phone, { stage: next.nextStage, data: updatedData })
    await sendTextMessage(phone, FLOW[next.nextStage].question)
  }
}

function scoreLead(data: Record<string, string>): number {
  let score = 50
  if (data.budget === '3') score += 30   // Over $20k
  if (data.budget === '2') score += 15   // $5k–$20k
  if (data.company?.length > 3) score += 10
  if (data.problem?.length > 30) score += 10
  return Math.min(score, 100)
}

Appointment Booking

A customer sends a message to book a call. The bot checks real-time availability, presents options, confirms the selection, and sends a pre-approved template confirmation, all without human involvement.

automation/appointmentBooking.ts
export async function handleBookingFlow(phone: string, message: string) {
  const session = await getSession(phone)

  if (!session?.bookingStage) {
    const slots = await getAvailableSlots() // fetches from your calendar API
    const slotList = slots.map((s, i) => (i + 1) + '. ' + s.label).join('
')
    await sendTextMessage(phone, 'Available slots:

' + slotList + '

Reply with a number to book.')
    await setSession(phone, { bookingStage: 'SELECT_SLOT', slots })
    return
  }

  if (session.bookingStage === 'SELECT_SLOT') {
    const index = parseInt(message.trim(), 10) - 1
    const slot = session.slots?.[index]

    if (!slot) {
      await sendTextMessage(phone, 'Please reply with a number from the list.')
      return
    }

    const booking = await createCalendarEvent(phone, slot)

    // Send pre-approved confirmation template
    await sendTemplate(phone, 'booking_confirmation', [
      slot.label,
      booking.confirmationCode,
      booking.meetingLink,
    ])

    await clearSession(phone)
  }
}

AI Customer Support with Human Escalation

The AI assistant handles the conversation using your knowledge base. When the AI cannot answer confidently, or when the customer explicitly asks for a human, the conversation is escalated to a live agent.

automation/aiSupport.ts
import OpenAI from 'openai'

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })

export async function handleAISupport(phone: string, userMessage: string) {
  // Check if already escalated to human
  const session = await getSession(phone)
  if (session?.escalated) {
    await notifyAgent(phone, userMessage)
    return
  }

  const history = await getConversationHistory(phone)

  const completion = await openai.chat.completions.create({
    model: 'gpt-4o',
    messages: [
      {
        role: 'system',
        content:
          'You are a WhatsApp support agent. Answer concisely, under 200 words. ' +
          'If you cannot help or the user asks for a human, reply with exactly: ESCALATE',
      },
      ...history,
      { role: 'user', content: userMessage },
    ],
    max_tokens: 300,
  })

  const reply = completion.choices[0].message.content ?? ''

  if (reply.trim() === 'ESCALATE' || userMessage.toLowerCase().includes('human')) {
    await setSession(phone, { escalated: true })
    await sendTextMessage(phone, 'Connecting you with a team member. Average wait: 2 minutes.')
    await createSupportTicket(phone, history, userMessage)
    return
  }

  await sendTextMessage(phone, reply)
  await appendToHistory(phone, userMessage, reply)
}

CRM Integrations

HubSpot

Create or update HubSpot contacts from WhatsApp conversations using the HubSpot Node.js SDK. Phone number is the join key, search for an existing contact first to avoid duplicates.

crm/hubspot.ts
import { Client } from '@hubspot/api-client'

const hubspot = new Client({ accessToken: process.env.HUBSPOT_ACCESS_TOKEN })

export async function upsertHubSpotContact(phone: string, data: {
  name?: string
  company?: string
  leadScore?: number
}) {
  const search = await hubspot.crm.contacts.searchApi.doSearch({
    filterGroups: [{ filters: [{ propertyName: 'phone', operator: 'EQ', value: phone }] }],
    properties: ['phone', 'firstname', 'hs_lead_status'],
    limit: 1,
    after: 0,
    sorts: [],
  })

  const properties = {
    phone,
    firstname: data.name ?? '',
    company: data.company ?? '',
    hs_lead_status: 'NEW',
    whatsapp_lead: 'true',
    lead_score: String(data.leadScore ?? 0),
  }

  if (search.results.length > 0) {
    await hubspot.crm.contacts.basicApi.update(search.results[0].id, { properties })
  } else {
    await hubspot.crm.contacts.basicApi.create({ properties })
  }
}

Salesforce

Use the jsforce library to write WhatsApp leads into Salesforce as Lead records. The Phone field is the external identifier for deduplication.

crm/salesforce.ts
import jsforce from 'jsforce'

const conn = new jsforce.Connection({
  loginUrl: process.env.SF_LOGIN_URL,
})

async function getSalesforceConnection() {
  await conn.login(process.env.SF_USERNAME!, process.env.SF_PASSWORD! + process.env.SF_TOKEN!)
  return conn
}

export async function createSalesforceLead(phone: string, data: {
  name: string
  company: string
  description: string
}) {
  const sf = await getSalesforceConnection()

  await sf.sobject('Lead').upsert(
    {
      Phone: phone,
      FirstName: data.name.split(' ')[0] ?? data.name,
      LastName: data.name.split(' ').slice(1).join(' ') || 'Unknown',
      Company: data.company,
      Description: data.description,
      LeadSource: 'WhatsApp',
    },
    'Phone'
  )
}

Zoho CRM

Zoho's REST API uses OAuth 2.0 access tokens. Create leads via the /Leads endpoint and sync conversation notes as Activities linked to the contact record.

Custom CRM via Webhook

If you run a custom CRM or internal database, emit a structured webhook event from the WhatsApp backend whenever a conversation stage changes, lead captured, appointment booked, support resolved. The CRM subscribes to these events and handles its own data updates. This decouples the WhatsApp integration from the CRM implementation, making both easier to change independently.

AI Integrations

OpenAI

The pattern shown in the AI Support automation example above applies broadly. Use GPT-4o for complex conversational support, lead qualification interpretation, and document summarization. Use GPT-4o mini for high-volume, simple message classification and routing, it costs far less and responds faster.

Anthropic Claude

Claude can be substituted for OpenAI in any of the examples above by swapping the SDK. Claude performs particularly well on longer documents and structured extraction tasks, reading a support conversation and producing a structured ticket summary, or analyzing a customer inquiry and determining the correct routing category.

ai/claude.ts
import Anthropic from '@anthropic-ai/sdk'

const claude = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })

export async function summarizeConversation(messages: { from: string; body: string }[]) {
  const transcript = messages.map((m) => m.from + ': ' + m.body).join('
')

  const response = await claude.messages.create({
    model: 'claude-opus-4-8',
    max_tokens: 300,
    messages: [{
      role: 'user',
      content:
        'Summarize this WhatsApp support conversation as: issue (one sentence), ' +
        'urgency (low/medium/high), category, and suggested resolution.

' + transcript,
    }],
  })

  return response.content[0].type === 'text' ? response.content[0].text : ''
}

Store your FAQs, product documentation, and policy content in a vector database (Pinecone, pgvector, Weaviate). When a customer asks a question, embed it, retrieve the three most relevant knowledge base articles, and pass them to the AI as context before generating the response. This is the RAG pattern, it keeps your AI grounded in your actual content and prevents hallucination.

Security Considerations

Webhook Signature Validation

Always validate the X-Hub-Signature-256 header on every incoming POST request. Use crypto.timingSafeEqual for the comparison to prevent timing-based attacks. If your raw body is parsed by a framework before the middleware runs, you may need to configure body-parser to preserve the raw buffer for signing.

Access Control

Gate your support dashboard behind authentication. Agents should only see conversations assigned to their team. Implement role-based access control: agents can read and reply, supervisors can reassign and escalate, admins can configure the bot and access analytics. Log every action an agent takes in the dashboard with a timestamp and user ID.

Token Rotation and Secrets Management

Store WHATSAPP_ACCESS_TOKEN, WHATSAPP_APP_SECRET, and WEBHOOK_VERIFY_TOKEN in a secrets manager, never in code or .env files on production servers. Rotate the access token after any team access change or suspected exposure. Use separate Meta Apps for development, staging, and production environments so a compromised staging credential cannot affect production.

Data Protection

WhatsApp conversations contain personal data, phone numbers, names, support content, potentially financial or health information. Encrypt conversation data at rest in your database. Implement a data retention policy and purge conversations older than your policy requires. If you operate under GDPR, establish a lawful basis for processing and document it. Do not send conversation transcripts to third-party AI providers without confirming your privacy policy covers this.

Audit Logging

lib/whatsappLogger.ts
interface WhatsAppAuditLog {
  event: 'message_received' | 'message_sent' | 'template_sent' | 'escalation' | 'lead_created' | 'error'
  phone: string
  agentId?: string
  templateName?: string
  durationMs?: number
  errorCode?: string
}

export function auditLog(entry: WhatsAppAuditLog) {
  console.log(JSON.stringify({
    ...entry,
    service: 'whatsapp',
    timestamp: new Date().toISOString(),
    env: process.env.NODE_ENV,
  }))
}

// Usage
auditLog({ event: 'message_received', phone: '+1234567890' })
auditLog({ event: 'lead_created', phone: '+1234567890', durationMs: 342 })
auditLog({ event: 'escalation', phone: '+1234567890', agentId: 'agent@company.com' })

Monitoring and Observability

Sentry

Wrap your webhook handler and message processor in Sentry error tracking. Capture the phone number and message type as context so errors are immediately actionable. Set up alerts for error rate spikes, a sudden increase in webhook processing errors often signals an API change or rate limit issue.

Datadog and Grafana

Emit custom metrics for: messages received per minute, messages sent per minute, AI response latency, CRM sync success rate, escalation rate, and conversation resolution time. Track these in Datadog or Grafana and set alerts when any metric deviates significantly from its 7-day average. A spike in escalation rate often signals the bot is not handling a new question type correctly.

Structured Logging

Every webhook event, outbound message, AI call, CRM write, and error should produce a structured JSON log line. Include phone (hashed for privacy in non-debug environments), event type, duration, and outcome. This makes it possible to reconstruct the full timeline of any conversation for debugging, and to build dashboards from log aggregation tools like Datadog Logs, Loki, or AWS CloudWatch Insights.

Webhook Health Monitoring

Meta will disable your webhook subscription if it consistently returns non-200 responses or times out. Monitor your webhook endpoint's response time and error rate continuously. Set an alert if P95 response time exceeds 3 seconds (your budget before Meta starts retrying). Use a separate health check endpoint that verifies your database connection, Redis connection, and queue state.

Common Mistakes

Not Responding to Webhooks Within 5 Seconds

If your webhook does not return HTTP 200 within 5 seconds, Meta retries the delivery. If it times out repeatedly, Meta disables your webhook subscription and you stop receiving messages. Always acknowledge the webhook immediately and hand the message to an async queue for processing. The handler should do nothing except validate the signature and enqueue the job.

Skipping Webhook Signature Verification

Any attacker who discovers your webhook URL can POST fabricated events and trigger your automation, creating fake leads, triggering messages, or flooding your queue. Signature validation takes five lines of code and prevents this entirely. There is no acceptable reason to skip it.

Sending Non-Approved Templates

Business-initiated messages (outside the 24-hour customer conversation window) must use Meta-approved template messages. Attempting to send free-form text to users outside the 24-hour window returns an error and can result in the business number being flagged. Submit templates for approval before you need them, approval takes 24 to 48 hours.

Poor Rate Limit Handling

WhatsApp Cloud API allows up to 80 messages per second per phone number. Exceeding this returns a 429 error. Implement a rate limiter before your send function. Add exponential backoff on retries so a spike in message volume does not cause a cascade of failed sends and retries that worsens the overload.

No Session Management for Multi-Turn Conversations

Without session state, every incoming message is treated as a new conversation. Lead qualification flows, appointment booking, and support flows all require knowing where the user is in the conversation. Use Redis with TTL-based sessions (expire after 24 hours of inactivity) to track conversation state per phone number.

Production Deployment Checklist

Before going live with a WhatsApp Business API integration, verify each item.

  • ✓ WhatsApp Business Account verified and phone number approved by Meta
  • ✓ Meta App created with WhatsApp product, webhook URL, and verify token configured
  • ✓ Webhook signature validation implemented using X-Hub-Signature-256 and timingSafeEqual
  • ✓ WHATSAPP_ACCESS_TOKEN and WHATSAPP_APP_SECRET stored in a secrets manager
  • ✓ Webhook handler responds with HTTP 200 in under 500ms (all processing is async)
  • ✓ Message queue (Redis / BullMQ / SQS) active and processing jobs reliably
  • ✓ Retry logic with exponential backoff for failed Graph API sends
  • ✓ Rate limiter in place (max 80 messages/second per phone number)
  • ✓ 24-hour conversation window logic implemented, templates used for business-initiated messages
  • ✓ All outbound templates submitted and approved in Meta Business Manager
  • ✓ Redis session management active with TTL expiry for multi-turn flows
  • ✓ Opt-in mechanism implemented before sending any outbound messages
  • ✓ User opt-out (STOP keyword) handler removes contact from outbound lists
  • ✓ AI escalation to human agent implemented and tested
  • ✓ CRM integration tested with real phone numbers in staging
  • ✓ Sentry or equivalent error tracking active and capturing webhook errors
  • ✓ Structured audit logging for every inbound and outbound event
  • ✓ Datadog / Grafana dashboard showing message volume, latency, and error rate
  • ✓ Webhook health monitor with alert if P95 response exceeds 3 seconds
  • ✓ GDPR / privacy compliance reviewed, data retention policy documented
Nurture Technologies

NEED HELP WITH YOUR STACK?

Nurture Technologies builds and maintains production-quality software for startups and businesses. If engineering problems are slowing you down, our team can help.

Talk to our team →
FAQ

FREQUENTLY ASKED QUESTIONS

What is the WhatsApp Business API?+

The WhatsApp Business API (now called the WhatsApp Cloud API when hosted by Meta) is a programmatic interface that lets businesses send and receive WhatsApp messages at scale. Unlike the WhatsApp Business App, which is a mobile application for small businesses, the API has no graphical interface. It is accessed via HTTP endpoints and designed for developers building automated systems: chatbots, CRM integrations, notification pipelines, and multi-agent support dashboards. You access it by creating a Meta App and adding the WhatsApp product in the Meta for Developers portal.

How much does the WhatsApp Business API cost?+

WhatsApp charges per conversation, not per message. A conversation is a 24-hour window that opens when the first message is exchanged. Conversations are categorized as service (user-initiated), marketing, utility, or authentication. Service conversations are free in most regions, you pay nothing when a customer messages you first. Marketing and utility conversations (business-initiated) carry a per-conversation fee that varies by country, ranging from approximately $0.005 to $0.085 per conversation. Meta publishes a full pricing table by country on the Meta for Developers documentation site. There are no monthly subscription fees for using the Cloud API directly through Meta.

How do I get access to the WhatsApp Business API?+

Create a Meta Business Account at business.facebook.com, then create a Meta App at developers.facebook.com and select Business as the app type. Add the WhatsApp product to your app, add and verify your business phone number, and create a System User Access Token. That token authenticates all API calls. The verification process requires a valid business phone number and a Meta Business Account in good standing. The process typically takes one to three business days for the business verification step, after which your phone number is approved for production messaging.

Can the WhatsApp API send automated messages?+

Yes, with one important constraint. Within 24 hours of a customer sending you a message, you can reply with any content (text, images, documents, buttons). After the 24-hour window closes, you can only send pre-approved template messages to re-engage the customer. Template messages are submitted to Meta for approval (takes 24–48 hours) and must follow their content policies. Templates are appropriate for appointment reminders, order confirmations, shipping updates, and payment receipts. They are not appropriate for promotional content unless submitted under the Marketing template category.

What is the 24-hour conversation window?+

When a customer sends you a message on WhatsApp, a 24-hour window opens during which you can reply with any type of content. This window resets each time the customer sends a new message. When the window closes, because the customer has not messaged in 24 hours, you can only reach them using a pre-approved template message. Template messages re-open the conversation window if the customer replies. This policy exists to protect users from unsolicited business messages. Design your flows to stay within the conversation window where possible, and use templates only for genuinely expected outbound notifications.

Can I connect WhatsApp to Salesforce or HubSpot?+

Yes. Both CRM platforms have REST APIs that you can call from your WhatsApp backend whenever a conversation event occurs. For HubSpot, use the official @hubspot/api-client Node.js library to create or update Contact and Deal records. For Salesforce, use jsforce to upsert Lead or Contact records using the phone number as the external key. The pattern is: receive a WhatsApp webhook event, process it (AI scoring, session state, etc.), then write the result to the CRM. The WhatsApp backend and CRM communicate server-to-server, no native WhatsApp connector is needed in either platform.

How does WhatsApp webhook integration work?+

You register a public HTTPS endpoint (your webhook URL) and a secret verify token in the Meta App dashboard. Meta sends a GET request to verify the endpoint, your server must respond with the hub.challenge value from the query string. After verification, Meta delivers incoming WhatsApp messages as POST requests to your webhook. Each POST contains a JSON payload describing the message event. Your server must respond with HTTP 200 within 5 seconds. Validate the X-Hub-Signature-256 header on every POST using your App Secret to confirm the request came from Meta.

Can I integrate OpenAI or Claude with WhatsApp?+

Yes, and this is one of the most valuable patterns. Your webhook receives the customer's message, passes it to the OpenAI or Anthropic API along with your system prompt and conversation history, and sends the model's response back via the WhatsApp Graph API. Use GPT-4o or Claude Opus for complex support tasks and reasoning. Use GPT-4o mini for high-volume, simple classification and routing tasks. Combine AI responses with a knowledge base (RAG pattern) to keep answers grounded in your actual product and policy content. Always implement human escalation so customers are not stuck with an AI that cannot resolve their issue.

What are WhatsApp template messages and how do I use them?+

Template messages are pre-approved message formats used for business-initiated communication outside the 24-hour conversation window. They are created in the Meta Business Manager under WhatsApp Manager > Message Templates. Each template has a name, language, category (marketing/utility/authentication/service), and a body with optional variable placeholders like {{1}} and {{2}}. After submission, Meta reviews the template in 24–48 hours. Once approved, you send it via the API by specifying the template name, language, and the values for each placeholder. Common approved templates include order confirmations, appointment reminders, shipping notifications, and OTP codes.

How many messages can I send per second?+

WhatsApp Cloud API allows up to 80 messages per second per phone number at the standard tier. Higher throughput is available for enterprise accounts. Within those limits, you can send to multiple recipients in parallel. Exceeding the rate limit returns a 429 HTTP error. Implement a client-side rate limiter (track message count per second using a Redis sliding window counter) and add exponential backoff retry logic for 429 errors. For bulk notifications to large lists, distribute sends across time windows rather than bursting all at once.

How do I handle images, documents, and other media in WhatsApp API?+

Incoming media messages include a media ID and MIME type but not a direct download URL. To download the file, first call the Graph API to get a temporary download URL (GET /{media-id}), then download the binary from that URL using your access token. Store the file in your own storage (S3, GCS) and reference it from your database. For sending media, upload the file to the Media API first to get a media ID, then reference that ID in your message payload. Temporary download URLs expire after a few minutes, download and store files immediately when you receive a media message.

What is the difference between the WhatsApp Business App and the WhatsApp Business API?+

The WhatsApp Business App is a mobile application designed for small businesses. It runs on a single device, supports one user at a time, and has a graphical interface. It is free but limited, no automation, no CRM integration, no API access, and a limit of around 250 broadcast recipients. The WhatsApp Business API is an HTTP API designed for programmatic access. It supports unlimited agents through your own dashboard, full automation via webhooks and code, integration with any CRM or system, and scales to millions of messages. The API requires technical setup and carries per-conversation costs for outbound messages.

How do I handle opt-ins and opt-outs?+

Meta requires that users have explicitly opted in before you send them business-initiated messages. Acceptable opt-in mechanisms include a checkbox on a web form, a confirmation flow in the WhatsApp conversation itself, or a voice/SMS opt-in. Store the opt-in record with timestamp and source. For opt-outs, respond to any message containing STOP, UNSUBSCRIBE, or CANCEL by setting an opt-out flag on the contact record and immediately ceasing outbound messages to that number. Continuing to message opted-out contacts violates Meta's policy and can result in your business number being flagged or disabled.

What should I do if the WhatsApp API returns errors or is unavailable?+

Implement exponential backoff for transient errors (429 rate limits, 500 server errors): wait 1 second and retry, then 2 seconds, 4 seconds, up to a maximum of 32 seconds. For 400 errors (bad request), log the full error payload, Meta returns structured error codes that identify whether the issue is a template not found, invalid phone number, message too long, or policy violation. Monitor the Meta Status page (metastatus.com) for platform-wide outages. For critical notifications (appointment confirmations, payment receipts), implement a fallback channel (SMS or email) that activates when WhatsApp delivers a delivery failure status callback.

How do I test a WhatsApp integration without going to production?+

The Meta App dashboard provides up to five free test phone numbers in the WhatsApp Sandbox. You can add your personal WhatsApp number as a recipient during testing. Test numbers can send messages to added recipients without template approval requirements. For webhook testing, use a tool like ngrok to expose your local server to the internet, then register the ngrok URL as your webhook in the Meta dashboard. For automated testing, mock the Graph API calls in unit tests so they do not make real API requests. Run integration tests against the sandbox with a test Meta App and a dedicated staging phone number that is separate from production.