WhatsApp Messaging via Meta Cloud API
Overview
The Meta WhatsApp Cloud API is the official, fully hosted path for programmatic WhatsApp messaging. No server management needed. First 1,000 service conversations per month are free.
Key rules:
- - Your app can NEVER send a free-form text message first. The very first message to any user must always be a pre-approved template. Free-form text is only unlocked after the user replies, and only within the 24-hour window that reply opens.
- Business-initiated messages outside a 24h reply window must use a pre-approved template
- Phone numbers must be registered in your WABA before sending
- Always use a System User token — user tokens expire in 24 hours
Conversation flow:
App → user: MUST be a template (always, for first contact)
User → app: reply opens a 24-hour free-form window
App → user: free-form text allowed within that 24h window
[24h passes with no user reply]
App → user: MUST use a template again to re-engage
Setup Checklist
- 1. Create Meta Developer App — developers.facebook.com → Create App → Business type
- Add WhatsApp product to the app (gives temp test number + 5 test recipient slots)
- Create a permanent System User token:
- Meta Business Manager → Settings → System Users → Create Admin user
- Assign permissions:
whatsapp_business_messaging +
whatsapp_business_management
- Generate token — this never expires
- 4. Register real phone number — number cannot already be active on personal/business WhatsApp
- Set up webhook — needs public HTTPS URL (trusted CA cert, no self-signed), must respond in < 10s
Sending Messages
Text Message (Node.js)
CODEBLOCK1
Text Message (Python)
CODEBLOCK2
Quick test via curl
CODEBLOCK3
Template Message (required when > 24h since last user reply)
CODEBLOCK4
Media Messages (Image, Document, Audio, Video)
Image:
CODEBLOCK5
Document:
CODEBLOCK6
Audio:
CODEBLOCK7
Video:
CODEBLOCK8
Important constraints:
- - All media URLs must be publicly accessible HTTPS (http:// fails)
- Max file sizes: Image 16MB, Document 100MB, Audio 16MB, Video 16MB
- Supported formats: Images (JPEG, PNG), Documents (PDF), Audio (AAC, MP3, OGG, WAV), Video (MP4, 3GPP)
- Media must not require authentication
- URLs cannot use shorteners (bit.ly, tinyurl, etc.)
Webhook Handler (Express) — Correct Async Pattern
CODEBLOCK9
Message Status & Delivery Tracking
Webhook Payload: Delivery Status
Meta sends status updates via webhook when a message is delivered, read, or fails:
CODEBLOCK10
Status values:
- -
sent — Message reached Meta servers - INLINECODE3 — Message delivered to user's device
- INLINECODE4 — User opened the message
- INLINECODE5 — Delivery failed (permanent)
Tracking Message Delivery
CODEBLOCK11
WABA Quality Rating & Account Health
What is Quality Rating?
Your WhatsApp Business Account (WABA) has a quality rating that affects your sending ability:
| Rating | Impact | Recovery |
|---|
| GREEN | Full functionality, no restrictions | Maintain this (stay green) |
| YELLOW |
Slight rate limit reduction, monitor closely | Improve within 7 days or drops to RED |
|
RED | Severe restrictions, may lose messaging access | Contact Meta Support |
How to Check Quality Rating
CODEBLOCK12
What Causes Quality Rating to Drop?
- - High bounce rate — sending to invalid/inactive numbers
- Spam reports — users marking your messages as spam
- High failure rate — messages consistently failing to deliver
- User blocks — users blocking your number after messages
- Policy violations — sending prohibited content
How to Improve Quality Rating
- 1. Validate phone numbers before sending — use the WhatsApp contacts check (error 131026)
- Only message opted-in users — don't send unsolicited messages
- Keep template content transactional — avoid marketing spam
- Monitor quality metrics — check rating regularly via API
- Respect user preferences — remove users who opt out
- Don't retry failed numbers aggressively — wait before retrying same number
Group Messages
Note: WhatsApp Business API does NOT support group messaging directly. You can only send to individual recipients (1:1 conversations).
If you need group functionality:
- - Users must add your business number to a group manually
- Messages sent to the group are treated as individual 1:1 messages
- You cannot initiate group conversations programmatically
API Versioning Strategy
All examples use v21.0 (current as of February 2026). Meta deprecates API versions annually.
Checking Your Current Version
CODEBLOCK13
Version Update Strategy
CODEBLOCK14
What Changes Between Versions?
- - New message types or features added
- Deprecated fields removed
- Error codes may change slightly
- Response payload structure may change
Always test before upgrading — make requests against the new version in your dev environment first.
Message Constraints & Limits
Text Message Limits
| Constraint | Limit |
|---|
| Text body max length | 4,096 characters |
| Link preview |
Enabled by default, disable with
preview_url: false |
| Carriage returns / newlines | Supported (use
\n) |
Interactive Message Limits
| Type | Items | Character Limit |
|---|
| Button | 1-3 | Title: 20 chars |
| List |
1-10 | Title: 24 chars per row |
Media URL Requirements
- - Must be HTTPS only (http:// rejected)
- Must be publicly accessible (no authentication required)
- Must NOT use shorteners (bit.ly, tinyurl rejected)
- Must have correct Content-Type header
- Max file sizes:
- Image: 16 MB
- Audio: 16 MB
- Video: 16 MB
- Document: 100 MB
Rate Limits (Per WABA)
| Limit | Value |
|---|
| Default throughput | 80 messages/second |
| Burst capacity |
1,000 messages/second (request increase) |
| Requests per minute | 60 API calls/minute |
Template Approval Process
Template Submission Workflow
CODEBLOCK15
How Long Does Approval Take?
- - Typical: 24-72 hours
- Peak times (weekends, holidays): up to 7 days
- Fast-track: Available for high-volume WABAs (request in Meta Support)
Common Approval Issues
| Issue | Why Rejected |
|---|
| Variable format | Must use {{1}}, {{2}} format |
| Template starts/ends with variable |
Must have text before first variable |
|
URL shorteners | Use full domain URLs only |
|
Placeholder quality | Placeholder values must be realistic examples |
|
Sensitive data request | Never ask for SSN, card numbers, passwords |
|
Unclear purpose | Purpose field must clearly state intent |
|
Warm language in utility | Use formal wording; warmth triggers "marketing" category (costs more) |
|
Duplicate template | Name/wording too similar to existing template |
Check rejection reason in Meta Business Manager → Business Support → Rejected Template Messages.
Common Setup Mistakes
1. Phone Number Already on Personal WhatsApp
Symptom: Registration fails with error 133010, status stays PENDING
Cause: Phone number is already active on a personal WhatsApp account
Fix:
CODEBLOCK16
2. Mixing Phone Number ID with WABA ID
Symptom: API returns "Invalid parameters" for phone operations
How to tell them apart:
CODEBLOCK17
3. Token Validation Passes but Scopes Missing
Symptom: Token debug shows valid, but messaging fails with error 3/10
CODEBLOCK18
4. Webhook Returns JSON Instead of Raw Challenge
Symptom: Webhook verification fails silently in Meta dashboard
Wrong:
CODEBLOCK19
Correct:
CODEBLOCK20
5. WABA Not Subscribed to App
Symptom: Webhooks never arrive (silent failure since 2025 Meta UI change)
Fix:
CODEBLOCK21
6. Sending Template Too Early in Approval Process
Symptom: Error 132001 "Template Unavailable"
Cause: Template still in PENDING status, not yet APPROVED
Fix: Check status in Meta Business Manager → Message Templates → wait for APPROVED status
7. Phone Number Not in WABA
Symptom: Error 131009 when trying to send
How to verify:
CODEBLOCK22
8. Sending Free-Form Text as First Message
Symptom: API returns 200, message ID issued, but user never receives it
Root cause: Only templates allowed as first message to any number
Fix: Always use a template for first contact
Phone Number Status Reference
Check the status field via API. Each status blocks different operations:
| Status | Meaning | Action |
|---|
| PENDING | Number is registered but not verified | Set up 2FA (either manual or API), then run register call |
| REGISTERED |
Number is verified and ready | Check
code_verification_status — should be
VERIFIED |
|
FLAGGED | Account or number under review for policy violation | Contact Meta Support |
|
BANNED | Number permanently disabled | Contact Meta Support |
Error Code Reference
| Code | Name | Cause | Fix |
|---|
| 190 | Token Expired | User token (24h lifetime) used in production | Switch to System User token; debug at developers.facebook.com/tools/debug/accesstoken |
| 3 / 10 |
Permission Denied | Token missing required scopes | Regenerate System User token with
whatsapp_business_messaging +
whatsapp_business_management |
|
100 | Invalid Parameter | Misspelled field or wrong value | Check request body against API docs; verify phone number format (E.164, no
+) |
|
130429 | Rate Limit (MPS) | Exceeded 80 messages/sec default | Add send queue + exponential backoff (see below) |
|
131047 | 24h Window Expired | > 24h since customer last replied | Replace free-form text with a pre-approved template message |
|
131026 | Undeliverable | Recipient blocked you, no WhatsApp, or outdated app | Verify recipient number; confirm they have WhatsApp installed and accepted Meta terms |
|
131048 | Spam Rate Limit | Messages flagged as spam | Check Quality Rating in WhatsApp Manager; review message content and opt-in practices |
|
131056 | Pair Rate Limit | Too many messages to same recipient too fast | Wait before retrying the same number |
|
131009 | Invalid Parameter Value | Phone number not in WABA, or wrong parameter | Verify number is registered in your WABA under Phone Numbers |
|
131021 | Same Sender/Recipient |
from and
to are the same number | Use a different recipient |
|
131031 | Account Locked | Policy violation or wrong 2-step PIN | Contact Meta Support |
|
132001 | Template Unavailable | Wrong template name, wrong language code, or not yet approved | Check WhatsApp Manager → Message Templates for exact name, language, and status |
|
133010 | Phone Not Registered | Sender number not registered in Cloud API | Run the registration API call (see below) |
|
368 | Policy Violation | Account restricted | Contact Meta Support |
|
1 / 2 | API Service Error | Meta outage or server error | Check metastatus.com; retry with exponential backoff |
Fix: Token Problems (Error 190, 3, 10)
Diagnose first:
curl "https://graph.facebook.com/debug_token?input_token=YOUR_TOKEN&access_token=YOUR_APP_ID|YOUR_APP_SECRET"
Check
is_valid,
expires_at (0 = never expires), and
scopes in the response.
Fix — create a non-expiring System User token:
- 1. Meta Business Manager → Settings → System Users
- Create Admin system user
- Add
whatsapp_business_messaging + whatsapp_business_management permissions - Generate token — never set an expiry
Fix: Phone Number Not Registered (Error 133010)
Step 1 — check phone number status:
CODEBLOCK24
If status is PENDING, the number is waiting for verification. Continue below.
Step 2 — set up Two-Step Verification (choose ONE method):
Option A: Manual setup (easy)
- 1. Meta Business Manager → WhatsApp Settings → Phone Numbers → Your Number
- Click "Two-Step Verification" → set a PIN
Option B: API setup (easier for automation)
CODEBLOCK25
Step 3 — register the number with the PIN:
CODEBLOCK26
Step 4 — wait 5 minutes, then verify registration:
CODEBLOCK27
Look for status: REGISTERED and code_verification_status: VERIFIED.
Fix: Webhook Not Verifying
Diagnose in this order:
- 1. Not returning raw challenge — endpoint must return the
hub.challenge string value only, not JSON - Token mismatch —
hub.verify_token Meta sends must match exactly what you set in the dashboard (case-sensitive) - SSL issue — Meta requires a valid cert from a trusted CA; self-signed certs are rejected
- Timeout — your server must respond within 10 seconds
- WABA not subscribed to App — common silent failure since 2025 Meta UI change:
CODEBLOCK28
Local development — expose localhost with a tunnel:
# Using ngrok
ngrok http 3000
# Use the https:// URL ngrok provides as your webhook callback URL in Meta
Fix: Rate Limiting (Error 130429)
Default limit is 80 messages per second. Fix with a queue and exponential backoff:
CODEBLOCK30
To increase throughput beyond 80 MPS, apply in Meta Business Manager → WhatsApp → Phone Numbers → Request Increased Messaging Limit.
Fix: 24-Hour Window (Error 131047)
You cannot send free-form text to a user more than 24 hours after their last message. You must use a template.
CODEBLOCK31
Create and submit templates at: Meta Business Manager → WhatsApp → Message Templates.
Fix: Template Rejected (Error 132001 or rejection in WhatsApp Manager)
| Rejection Reason | Fix |
|---|
| Variable format wrong | Use {{1}}, {{2}} — double curly braces, sequential integers only |
| Template starts/ends with variable |
Add plain text before
{{1}} and after the last variable |
| Variables not sequential | Must be
{{1}},
{{2}} — no gaps allowed |
| URL shorteners used | Use full, unshortened URLs to your own domain |
| Language code mismatch | Match
language.code to the actual content language, e.g.
en_US,
pt_BR |
| Warm language in utility template | Use formal transactional wording; warm language causes auto-reclassification to marketing category |
| Sensitive data | Never request SSNs, full card numbers, or passwords |
| Duplicate of existing template | Change the wording — even minor variation is required |
| Purpose unclear | Each variable must have a descriptive example value in the template submission |
Where to find rejection reason:
Meta Business Manager → Business Support Home → Your WhatsApp Account → Rejected Template Messages → view policy issue.
Fix: Message Undeliverable (Error 131026)
Run this to check if a number has WhatsApp before sending:
CODEBLOCK32
Diagnose: "My message sends but the user never receives it" (first-contact trap)
This is the most common silent failure. The API returns success (messages[0].id) but the user receives nothing.
Root cause: You sent a free-form text message as the first outreach. Meta silently drops it.
How to tell: Check the message status webhook — the message will show failed with error 131047 or show sent but never delivered.
Rule: The very first message your app sends to any number must be a template. No exceptions.
CODEBLOCK33
Fix: Create and approve a template for every type of first-contact message you need to send. Submit templates at Meta Business Manager → WhatsApp → Message Templates.
Practical Patterns from Production
Phone Number Validation Before Sending
Always validate phone number format and WhatsApp registration before sending:
CODEBLOCK34
Structured Error Response Handling
CODEBLOCK35
Interactive Messages (Smart Buttons vs List)
CODEBLOCK36
Testing & Debugging Helper Scripts
Token & Phone Verification Script:
# Check if token is valid and phone number is accessible
import requests, os
from dotenv import load_dotenv
load_dotenv()
TOKEN = os.getenv('WHATSAPP_TOKEN')
PHONE_ID = os.getenv('PHONE_NUMBER_ID')
# Debug token
r = requests.get(f"https://graph.facebook.com/debug_token?input_token={TOKEN}&access_token={TOKEN}")
data = r.json()
if 'error' in data:
print(f"❌ Token Error: {data['error']['message']}")
else:
print(f"✅ Token Valid")
print(f" Expires: {data['data'].get('expires_at')} (0=never)")
print(f" Scopes: {data['data'].get('scopes')}")
# Check phone number
r = requests.get(
f"https://graph.facebook.com/v21.0/{PHONE_ID}",
headers={"Authorization": f"Bearer {TOKEN}"}
)
if r.status_code == 200:
print(f"✅ Phone Number Accessible")
print(f" Display: {r.json().get('display_phone_number')}")
print(f" Quality Rating: {r.json().get('quality_rating')}")
else:
print(f"❌ Phone Error: {r.json().get('error', {}).get('message')}")
Quick Debug Checklist
When a message fails, check in this order:
- 1. Token valid? — INLINECODE43
- Phone number format? — E.164, no
+, no spaces: INLINECODE45 - Number registered in WABA? — check WhatsApp Manager → Phone Numbers
- Number registered with Cloud API? — run registration call if error 133010
- 24h window? — if > 24h since last user reply, send a template instead
- Template approved? — WhatsApp Manager → Message Templates → check status and rejection reason
- Webhook subscribed? — verify WABA → App subscription via INLINECODE46
- Rate limited? — check Quality Rating in WhatsApp Manager; implement backoff + queue
通过Meta云API发送WhatsApp消息
概述
Meta WhatsApp云API是用于程序化WhatsApp消息传递的官方全托管路径。无需服务器管理。每月前1,000次服务对话免费。
关键规则:
- - 您的应用绝不能首先发送自由格式文本消息。 对任何用户的第一条消息必须始终是预先批准的模板。只有在用户回复后,并且在回复打开的24小时窗口内,才允许发送自由格式文本。
- 在24小时回复窗口之外的业务发起的消息必须使用预先批准的模板
- 发送前,电话号码必须在您的WABA中注册
- 始终使用系统用户令牌——用户令牌在24小时内过期
对话流程:
应用 → 用户:必须是模板(始终如此,用于首次联系)
用户 → 应用:回复打开一个24小时的自由格式窗口
应用 → 用户:在该24小时窗口内允许自由格式文本
[24小时过去,用户无回复]
应用 → 用户:必须再次使用模板才能重新接触
设置清单
- 1. 创建Meta开发者应用 — developers.facebook.com → 创建应用 → 业务类型
- 将WhatsApp产品添加到应用中(提供临时测试号码 + 5个测试接收者名额)
- 创建永久系统用户令牌:
- Meta商务管理平台 → 设置 → 系统用户 → 创建管理员用户
- 分配权限:whatsapp
businessmessaging + whatsapp
businessmanagement
- 生成令牌——此令牌永不过期
- 4. 注册真实电话号码 — 该号码不能已在个人/企业WhatsApp上激活
- 设置Webhook — 需要公共HTTPS URL(受信任的CA证书,非自签名),必须在10秒内响应
发送消息
文本消息 (Node.js)
javascript
// npm install axios
const axios = require(axios);
async function sendMessage(phoneNumber, text) {
// phoneNumber: E.164格式,不带+,例如 14155551234
const res = await axios.post(
https://graph.facebook.com/v21.0/${process.env.WAPHONENUMBER_ID}/messages,
{
messaging_product: whatsapp,
recipient_type: individual,
to: phoneNumber,
type: text,
text: { preview_url: false, body: text }
},
{ headers: { Authorization: Bearer ${process.env.WAACCESSTOKEN} } }
);
return res.data;
}
文本消息 (Python)
python
pip install requests
import requests, os
def send_message(phone: str, text: str) -> dict:
r = requests.post(
fhttps://graph.facebook.com/v21.0/{os.environ[WAPHONENUMBER_ID]}/messages,
headers={Authorization: fBearer {os.environ[WAACCESSTOKEN]}},
json={
messaging_product: whatsapp,
recipient_type: individual,
to: phone, # E.164格式,不带+
type: text,
text: {preview_url: False, body: text}
}
)
r.raiseforstatus()
return r.json()
通过curl快速测试
bash
curl -X POST https://graph.facebook.com/v21.0/YOURPHONEID/messages \
-H Authorization: Bearer YOURACCESSTOKEN \
-H Content-Type: application/json \
-d {messaging_product:whatsapp,to:14155551234,type:text,text:{body:Hello}}
模板消息(自上次用户回复超过24小时后必需)
javascript
const payload = {
messaging_product: whatsapp,
to: phoneNumber,
type: template,
template: {
name: hello_world, // 您已批准的模板名称
language: { code: en_US },
components: [{
type: body,
parameters: [
{ type: text, text: John }, // 填充 {{1}}
{ type: text, text: Order #4521 } // 填充 {{2}}
]
}]
}
};
媒体消息(图片、文档、音频、视频)
图片:
javascript
const payload = {
messaging_product: whatsapp,
to: phoneNumber,
type: image,
image: {
link: https://your-domain.com/image.jpg // 必须是可公开访问的HTTPS
}
};
文档:
javascript
const payload = {
messaging_product: whatsapp,
to: phoneNumber,
type: document,
document: {
link: https://your-domain.com/file.pdf,
caption: Invoice // 可选
}
};
音频:
javascript
const payload = {
messaging_product: whatsapp,
to: phoneNumber,
type: audio,
audio: {
link: https://your-domain.com/audio.mp3
}
};
视频:
javascript
const payload = {
messaging_product: whatsapp,
to: phoneNumber,
type: video,
video: {
link: https://your-domain.com/video.mp4,
caption: Demo video // 可选
}
};
重要限制:
- - 所有媒体URL必须是可公开访问的HTTPS(http://会失败)
- 最大文件大小:图片16MB,文档100MB,音频16MB,视频16MB
- 支持的格式:图片(JPEG,PNG),文档(PDF),音频(AAC,MP3,OGG,WAV),视频(MP4,3GPP)
- 媒体不得要求身份验证
- URL不能使用缩短器(bit.ly,tinyurl等)
Webhook处理程序(Express)——正确的异步模式
javascript
// GET — Meta调用此方法以验证您的端点
app.get(/webhook, (req, res) => {
const { hub.mode: mode, hub.verify_token: token, hub.challenge: challenge } = req.query;
if (mode === subscribe && token === process.env.VERIFY_TOKEN)
return res.status(200).send(challenge); // 仅原始字符串 — 不是JSON
res.sendStatus(403);
});
// POST — 关键:立即返回200,异步处理
app.post(/webhook, express.json(), (req, res) => {
// 立即返回200,这样Meta不会重试
res.sendStatus(200);
// 异步处理webhook负载(不要阻塞)
setImmediate(() => {
processWebhookAsync(req.body).catch(err => {
logger.error(Webhook处理失败:${err.message});
});
});
});
async function processWebhookAsync(body) {
body.entry?.forEach(entry =>
entry.changes?.forEach(change => {
const value = change.value;
// 传入消息
if (value.messages) {
value.messages.forEach(msg => {
console.log(来自${msg.from}的消息:${msg.text?.body});
handleMessage(msg);
});
}
// 投递状态
if (value.statuses) {
value.statuses.forEach(status => {
console.log(消息${status.id}状态:${status.status});
handleDeliveryStatus(status);
});
}
})
);
}
消息状态与投递跟踪
Webhook负载:投递状态
当消息被投递、阅读或失败时,Meta通过webhook发送状态更新:
json
{
object: whatsappbusinessaccount,
entry: [{
changes: [{
value: {
statuses: [{
id: wamid.xxx, // 来自您发送响应的消息ID
status: delivered, // sent | delivered | read | failed
timestamp: 1675262308,
recipient_id: 14155551234,
type: message
}]
}
}]
}]
}
状态值:
- - sent — 消息已到达Meta服务器
- delivered — 消息已投递到用户设备
- read — 用户已打开消息
- failed — 投递失败(永久性)
跟踪消息投递
javascript
// 发送时,存储消息ID
const sendResult = await sendMessage(phone, text);
const messageId = sendResult.messages[0].id;
// 记录以便webhook跟踪
db.messages.insert({
message_id: messageId,
recipient: phone,
sent_at: Date.now(),
status: sent,
body: text
});
// 当webhook到达并带有状态