WhatsApp Business API: Quick Start
Send your first WhatsApp message in under 15 minutes.
Meta's official setup documentation is scattered across dashboards, policy pages, and changelogs — most developers spend 2–3 hours on a setup that should take 15 minutes. This guide isolates the critical path.
Who this is for: Backend engineers shipping production WhatsApp integrations who need first-message success without trial-and-error. REST and basic auth concepts assumed.
API version: v24.0 (Feb 2026) — check Meta's changelog
All code tested: Python 3.12.3 · Flask 3.0 · requests 2.31.0 · February 2026
Choose your path first
Read this before creating anything. Phone numbers cannot be transferred between WhatsApp accounts once registered. The wrong choice means starting over.
Are you sending to real users?
│
├─ No ──→ Test mode — 5 minutes, no verification needed
│
└─ Yes ──→ Production — 1–3 business days, verification required
| Test mode | Production | |
|---|---|---|
| Use when | Building, prototyping, CI | Sending to real users |
| Token type | Temporary (expires in 24h) | System User (never expires) |
| Recipients | Pre-approved list only (up to 5) | Any WhatsApp number |
| Templates required | No | Yes, outside the 24-hour window |
| Business verification | Not required | Required before going live |
| Setup time | ~5 minutes | 1–3 business days |
Irreversible
Once a phone number is registered to a WhatsApp Business Account, it cannot be transferred to a different account. Use a dedicated number — never your personal number.
Common pitfalls — read before you start
These are the errors developers hit most in the first 30 minutes. Reading this table once prevents most of them.
| Pitfall | Symptom | Fix |
|---|---|---|
| Using WABA ID instead of Phone Number ID | Error 100 — "object does not exist" | Use the 15-digit Phone Number ID, not the 16-digit WABA ID |
Bearer prefix missing |
Error 190 — token invalid | Header must be Authorization: Bearer YOUR_TOKEN |
| Recipient not in test allowlist | Error 131030 | Add recipient in dashboard → Phone Numbers → Send and receive messages |
| Phone number in wrong format | Error 100 or silent failure | E.164 format: + + country code + number, no spaces or dashes |
| Messaging after the 24-hour window | 200 OK but message never arrives |
Use an approved template for first contact or after 24 hours |
| Short-lived token in production | Error 190 after ~24h | Switch to a System User token — it does not expire |
Quick Start — 15 minutes
Step 1: Create your app and get credentials
- Go to developers.facebook.com → My Apps → Create App
- Select Business type
- Add the WhatsApp product to the app
- Open WhatsApp → API Setup in the left sidebar
From the API Setup page, copy these three values — you need all of them:
| Value | Where to find it | Used for |
|---|---|---|
| Phone Number ID | API Setup → "From" dropdown | All API calls — not the WABA ID |
| Access Token | API Setup → "Temporary access token" | Auth header in test mode |
| Test phone number | API Setup → "From" field | Your sending number |
The API Setup page shows both a Phone Number ID (15 digits) and a WABA ID (16 digits). Use the Phone Number ID in all API calls — using the WABA ID returns Error 100.
Step 2: Add a test recipient
Test mode only delivers to numbers you explicitly pre-approve — up to five.
- Dashboard → WhatsApp → API Setup → To field → Manage phone number list
- Add the recipient's phone number in E.164 format (
+15551234567) - The recipient receives a WhatsApp verification code and must confirm
Step 3: Send your first message
Replace the placeholder values, then run:
curl -X POST "https://graph.facebook.com/v24.0/PHONE_NUMBER_ID/messages" \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": "+15551234567",
"type": "text",
"text": {
"body": "Hello from WhatsApp Cloud API"
}
}'
Success — HTTP 200:
{
"messaging_product": "whatsapp",
"contacts": [{ "wa_id": "15551234567" }],
"messages": [{ "id": "wamid.HBgN..." }]
}
messages[0].id confirms Meta's servers accepted the request. The message should appear on the recipient's WhatsApp within seconds.
It didn't work? Check the Common pitfalls table above, or jump to Error reference.
✅ Quick Start complete
You've successfully sent a WhatsApp message via the API.
Here's the state of your system right now:
- You have a working test environment connected to Meta's sandbox
- Your token is temporary — it expires in 24 hours and is only valid in test mode
- Your sending is limited to the recipients you approved in Step 2
- No business verification, templates, or webhooks are required to continue in test mode
What to read next depends on what you're building.
The sections below are not required to continue in test mode. Come back to them when you're ready.
Messaging context
The two most common sources of silent failures in WhatsApp integrations. Read before you build anything that sends messages to users.
The 24-hour window
Outside the 24-hour window, the API returns 200 OK with a message ID. The message is never delivered. No error is thrown. This is by design — Meta does not surface this as an API error.
The rule: Free-form messages are only allowed within 24 hours of the user's last inbound message to you. For all other cases — first contact, re-engagement, scheduled sends — use an approved template.
Rule of thumb: If you initiate contact, always use a template. If the user messaged first within the last 24 hours, free-form is allowed. When in doubt, use a template — they work in both states.
Templates
Templates are required for first contact and all out-of-window sends. Meta provides a pre-approved hello_world template for testing.
Send a template:
curl -X POST "https://graph.facebook.com/v24.0/PHONE_NUMBER_ID/messages" \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": "+15551234567",
"type": "template",
"template": {
"name": "hello_world",
"language": { "code": "en_US" }
}
}'
With variables — for dynamic content like names or order numbers:
{
"type": "template",
"template": {
"name": "order_shipped",
"language": { "code": "en_US" },
"components": [{
"type": "body",
"parameters": [
{ "type": "text", "text": "Maria" },
{ "type": "text", "text": "ORDER-9981" }
]
}]
}
}
Create a template: Dashboard → WhatsApp → Message Templates → Create Template. Choose a category, write your content, submit. Approval typically takes minutes to a few hours.
Template categories
Template category affects review time, delivery rates, and cost. Getting this wrong is the leading cause of Error 368 (temporary block).
| Category | Use for | Examples |
|---|---|---|
| Authentication | OTPs, login verification | "Your code is 123456" |
| Utility | Transactional, opt-in responses | Order confirmation, shipping update |
| Marketing | Promotions, offers, announcements | "New sale — 20% off this week" |
Marketing ≠ Utility
Submitting marketing content as Utility is the single most common cause of Error 368. If your template promotes a product, discount, or offer — it is Marketing, regardless of how you frame it.
Webhooks
Webhooks deliver inbound messages, delivery receipts, and read status to your server. They are optional in test mode and required in production.
Dashboard configuration:
- WhatsApp → Configuration → Webhooks → Edit
- Callback URL — must be publicly reachable HTTPS. For local development, use ngrok:
ngrok http 5000 - Verify token — any string you choose, used to confirm endpoint ownership
- Subscribe to the messages field → Save
Verify endpoint ownership
When you save the webhook, Meta sends a GET to confirm your server controls the URL:
# Python 3.9+ | Flask 3.0 | Tested: Python 3.12.3
import os
from flask import Flask, request
app = Flask(__name__)
VERIFY_TOKEN = os.environ.get("WHATSAPP_VERIFY_TOKEN", "your_verify_token")
@app.route("/webhook", methods=["GET"])
def verify_webhook():
"""Handle Meta's webhook verification challenge."""
mode = request.args.get("hub.mode")
token = request.args.get("hub.verify_token")
challenge = request.args.get("hub.challenge")
if mode == "subscribe" and token == VERIFY_TOKEN:
return challenge, 200
return "Forbidden", 403
Receive messages with signature verification
Always verify the X-Hub-Signature-256 header. Any client can POST to a public URL — requests without a valid signature should be rejected immediately.
# Python 3.9+ | Flask 3.0 | Tested: Python 3.12.3
import hmac
import hashlib
import os
from flask import Flask, request
app = Flask(__name__)
APP_SECRET = os.environ["WHATSAPP_APP_SECRET"]
@app.route("/webhook", methods=["POST"])
def receive_webhook():
"""Receive and verify incoming WhatsApp webhook events."""
signature = request.headers.get("X-Hub-Signature-256", "")
expected = "sha256=" + hmac.new(
APP_SECRET.encode(), request.data, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected):
return "Forbidden", 403
payload = request.get_json(silent=True) or {}
for entry in payload.get("entry", []):
for change in entry.get("changes", []):
value = change.get("value", {})
for msg in value.get("messages", []):
sender = msg.get("from")
body = msg.get("text", {}).get("body", "")
print(f"Message from {sender}: {body}")
for status in value.get("statuses", []):
print(f"Status for {status['id']}: {status['status']}")
return "OK", 200
Production readiness
Complete every item in this section before sending to any number outside your test allowlist.
Business verification
Required before sending to any number outside your test allowlist. This is the step most teams underestimate — start it before you begin building.
- Business Manager → Business Settings → Business Info → Start Verification
- Submit business documentation — typically legal registration or a utility bill (varies by country)
- Allow 1–3 business days for approval
Verification cannot be expedited and blocks all production sends until complete.
Production credentials
Test mode tokens expire after 24 hours. Production requires a non-expiring System User token.
- Business Manager → Business Settings → System Users → Add
- Set role to Admin
- Add Assets → select your WhatsApp Business Account
- Generate New Token → select both permissions:
whatsapp_business_messagingwhatsapp_business_management- Copy the token — it is shown only once
- Store it in your environment — never in source code:
export WHATSAPP_TOKEN="EAAG..."
export WHATSAPP_PHONE_ID="123456789012345"
export WHATSAPP_APP_SECRET="your_app_secret"
Never hardcode credentials
A leaked System User token gives unrestricted API access to your WhatsApp account. Use environment variables, AWS Secrets Manager, HashiCorp Vault, or equivalent. Never commit secrets to source control.
Production-ready sending
The curl commands above are fine for exploration. Production code needs connection pooling, automatic retries on transient failures, and explicit timeouts — without these, a single slow request can block a thread indefinitely and a transient 5xx will surface as an unhandled exception.
# Python 3.9+ | Tested: Python 3.12.3
import os
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
_session = requests.Session()
_session.mount(
"https://",
HTTPAdapter(
max_retries=Retry(
total=3,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["POST"],
)
),
)
GRAPH_API_BASE = "https://graph.facebook.com/v24.0"
def send_text_message(to: str, body: str) -> dict:
"""
Sends a WhatsApp text message with connection pooling and retry logic.
Args:
to: Recipient phone in E.164 format (e.g. "+15551234567").
body: Message text.
Returns:
API response dict — check messages[0].id for the message ID.
Raises:
requests.exceptions.HTTPError: On 4xx/5xx after retries exhausted.
requests.exceptions.Timeout: If the request exceeds 10 seconds.
"""
response = _session.post(
f"{GRAPH_API_BASE}/{os.environ['WHATSAPP_PHONE_ID']}/messages",
headers={"Authorization": f"Bearer {os.environ['WHATSAPP_TOKEN']}"},
json={
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": to,
"type": "text",
"text": {"body": body},
},
timeout=10,
)
response.raise_for_status()
return response.json()
Production checklist
This checklist does not cover consent capture UX, CRM integration, or message sequencing logic.
- Business verification approved
- Production phone number registered — dedicated number, not personal
- Payment method added to Meta Business account
- System User token generated with
whatsapp_business_messagingandwhatsapp_business_management - Token and secrets stored as environment variables — not in source code
- Required message templates approved with correct categories
- Webhook live with HTTPS and signature verification enabled
- Retry logic implemented for HTTP 429 and 5xx responses
- Quality rating monitored in Meta Business Suite → Insights
- Opt-in mechanism in place for all recipients
- Opt-out path included in all outbound message sequences
Error reference
The errors below are the ones developers hit most often in this guide's flow. For root cause analysis, subcodes, and prevention patterns, see the Graph API Error Reference.
Error 131030 — Recipient not in allowed list
When it happens: Test mode only.
Fix: Dashboard → Phone Numbers → select your test number → Send and receive messages → add the number. The recipient must confirm via a WhatsApp verification message.
Error 190 — Invalid access token
Cause: Token expired, missing Bearer prefix, or generated for a different app.
# Confirm the header format
Authorization: Bearer YOUR_TOKEN
# Inspect the token
curl "https://graph.facebook.com/debug_token?\
input_token=YOUR_TOKEN&access_token=APP_ID|APP_SECRET"
For production: switch to a System User token. See Production credentials. For subcodes and prevention, see the Graph API Error Reference.
Error 100 — Invalid parameter
Cause: Wrong ID type or a required field is missing. Most common: WABA ID where Phone Number ID is required.
# Get your Phone Number ID from your WABA
curl "https://graph.facebook.com/v24.0/WABA_ID/phone_numbers?fields=id,display_phone_number" \
-H "Authorization: Bearer TOKEN"
For subcodes and prevention, see the Graph API Error Reference.
Error 131047 — Messaging window expired
Cause: 24-hour window closed. The API returns 200 OK — message is silently dropped.
If your logs show 200 OK but users report non-delivery, assume a closed window before debugging anything else.
Fix: Switch to an approved template. See Templates. See also The 24-hour window.
Error 368 — Temporary block
Cause: Policy violation — missing opt-in, miscategorized template, or sustained spam reports.
Fix: Check quality rating in Meta Business Suite → WhatsApp Manager → Insights → Quality Rating. Block lifts in 24–48 hours. For prevention patterns, see the Graph API Error Reference.
Next steps
| What you want to do | Where to go |
|---|---|
| Send images, documents, video | Media messages |
| Add buttons and lists | Interactive messages |
| Template approval and variable syntax | Message templates |
| Rate limits and throughput tiers | Messaging limits |
| Debug API errors in depth | Graph API Error Reference |
All code tested: Python 3.12.3 · Flask 3.0 · requests 2.31.0 · WhatsApp Cloud API v24.0 · February 2026
Not affiliated with Meta Platforms, Inc.
Last updated: February 2026