Skill: romanbaz/whatsapp-cloud-api-reference (version 1.0.0)

--- name: whatsapp-cloud-api-reference description: Use when implementing WhatsApp messaging via Meta Cloud API, or diagnosing failures like message not delivered, template rejected, webhook issues, phone not registered, token errors, rate limiting, 24-hour window violations, quality rating drops, or setup mistakes on the WhatsApp Business API. --- # 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 2. **Add WhatsApp product** to the app (gives temp test number + 5 test recipient slots) 3. **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 5. **Set up webhook** — needs public HTTPS URL (trusted CA cert, no self-signed), must respond in < 10s --- ## Sending Messages ### Text Message (Node.js) ```javascript // npm install axios const axios = require('axios'); async function sendMessage(phoneNumber, text) { // phoneNumber: E.164 without +, e.g. "14155551234" const res = await axios.post( `https://graph.facebook.com/v21.0/${process.env.WA_PHONE_NUMBER_ID}/messages`, { messaging_product: 'whatsapp', recipient_type: 'individual', to: phoneNumber, type: 'text', text: { preview_url: false, body: text } }, { headers: { Authorization: `Bearer ${process.env.WA_ACCESS_TOKEN}` } } ); return res.data; } ``` ### Text Message (Python) ```python # pip install requests import requests, os def send_message(phone: str, text: str) -> dict: r = requests.post( f"https://graph.facebook.com/v21.0/{os.environ['WA_PHONE_NUMBER_ID']}/messages", headers={"Authorization": f"Bearer {os.environ['WA_ACCESS_TOKEN']}"}, json={ "messaging_product": "whatsapp", "recipient_type": "individual", "to": phone, # E.164 without + "type": "text", "text": {"preview_url": False, "body": text} } ) r.raise_for_status() return r.json() ``` ### Quick test via curl ```bash curl -X POST "https://graph.facebook.com/v21.0/YOUR_PHONE_ID/messages" \ -H "[REDACTED_SECRET]" \ -H "Content-Type: application/json" \ -d '{"messaging_product":"whatsapp","to":"14155551234","type":"text","text":{"body":"Hello"}}' ``` ### Template Message (required when > 24h since last user reply) ```javascript const payload = { messaging_product: 'whatsapp', to: phoneNumber, type: 'template', template: { name: 'hello_world', // your approved template name language: { code: 'en_US' }, components: [{ type: 'body', parameters: [ { type: 'text', text: 'John' }, // fills {{1}} { type: 'text', text: 'Order #4521' } // fills {{2}} ] }] } }; ``` ### Media Messages (Image, Document, Audio, Video) **Image:** ```javascript const payload = { messaging_product: 'whatsapp', to: phoneNumber, type: 'image', image: { link: 'https://your-domain.com/image.jpg' // must be publicly accessible HTTPS } }; ``` **Document:** ```javascript const payload = { messaging_product: 'whatsapp', to: phoneNumber, type: 'document', document: { link: 'https://your-domain.com/file.pdf', caption: 'Invoice' // optional } }; ``` **Audio:** ```javascript const payload = { messaging_product: 'whatsapp', to: phoneNumber, type: 'audio', audio: { link: 'https://your-domain.com/audio.mp3' } }; ``` **Video:** ```javascript const payload = { messaging_product: 'whatsapp', to: phoneNumber, type: 'video', video: { link: 'https://your-domain.com/video.mp4', caption: 'Demo video' // optional } }; ``` **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 ```javascript // GET — Meta calls this to verify your endpoint 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); // raw string only — NOT JSON res.sendStatus(403); }); // POST — CRITICAL: return 200 IMMEDIATELY, process async app.post('/webhook', express.json(), (req, res) => { // Return 200 immediately so Meta doesn't retry res.sendStatus(200); // Process webhook payload asynchronously (don't block) setImmediate(() => { processWebhookAsync(req.body).catch(err => { logger.error(`Webhook processing failed: ${err.message}`); }); }); }); async function processWebhookAsync(body) { body.entry?.forEach(entry => entry.changes?.forEach(change => { const value = change.value; // Incoming messages if (value.messages) { value.messages.forEach(msg => { console.log(`Message from ${msg.from}: ${msg.text?.body}`); handleMessage(msg); }); } // Delivery status if (value.statuses) { value.statuses.forEach(status => { console.log(`Message ${status.id} status: ${status.status}`); handleDeliveryStatus(status); }); } }) ); } ``` --- ## Message Status & Delivery Tracking ### Webhook Payload: Delivery Status Meta sends status updates via webhook when a message is delivered, read, or fails: ```json { "object": "whatsapp_business_account", "entry": [{ "changes": [{ "value": { "statuses": [{ "id": "wamid.xxx", // Message ID from your send response "status": "delivered", // "sent" | "delivered" | "read" | "failed" "timestamp": "1675262308", "recipient_id": "14155551234", "type": "message" }] } }] }] } ``` **Status values:** - `sent` — Message reached Meta servers - `delivered` — Message delivered to user's device - `read` — User opened the message - `failed` — Delivery failed (permanent) ### Tracking Message Delivery ```javascript // When you send, store the message ID const sendResult = await sendMessage(phone, text); const messageId = sendResult.messages[0].id; // Log it for webhook tracking db.messages.insert({ message_id: messageId, recipient: phone, sent_at: Date.now(), status: 'sent', body: text }); // When webhook arrives with status update, match by message_id function handleDeliveryStatus(statusUpdate) { const { id, status, recipient_id } = statusUpdate; // Update your database db.messages.updateOne( { message_id: id }, { status: status, updated_at: Date.now() } ); // Handle delivery failures if (status === 'failed') { logger.error(`Message ${id} failed to deliver to ${recipient_id}`); // Retry logic here } } ``` --- ## 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 ```bash curl "https://graph.facebook.com/v21.0/PHONE_NUMBER_ID?fields=quality_rating" \ -H "Authorization: Bearer YOUR_TOKEN" # Response: { "quality_rating": "GREEN" } ``` ### 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) 2. **Only message opted-in users** — don't send unsolicited messages 3. **Keep template content transactional** — avoid marketing spam 4. **Monitor quality metrics** — check rating regularly via API 5. **Respect user preferences** — remove users who opt out 6. **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 ```bash # List all available versions curl "https://graph.facebook.com/versions" \ -H "Authorization: Bearer YOUR_TOKEN" # Current version recommendations # - v21.0 (current, recommended) # - v20.0 (previous, will deprecate in 6 months) ``` ### Version Update Strategy ```javascript // Store version in config, not hardcoded const API_VERSION = process.env.WHATSAPP_API_VERSION || 'v21.0'; const url = `https://graph.facebook.com/${API_VERSION}/${PHONE_NUMBER_ID}/messages`; // When Meta deprecates a version, update .env: // WHATSAPP_API_VERSION=v22.0 ``` ### 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 ``` Create template in Meta Business Manager ↓ Submit for review (human review by Meta) ↓ Status: PENDING (24-72 hours typical) ↓ Status: APPROVED (can now use in messages) OR Status: REJECTED (reason provided in dashboard) ``` ### 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:** ``` 1. Remove phone from personal WhatsApp (go to Settings → Devices → Remove phone) 2. Wait 24 hours 3. Re-run registration API call ``` ### 2. Mixing Phone Number ID with WABA ID **Symptom:** API returns "Invalid parameters" for phone operations **How to tell them apart:** ``` PHONE_NUMBER_ID: 120######## (11-12 digits, starts with 120) WABA_ID: ######### (9-10 digits, higher number) # Correct endpoint POST /v21.0/PHONE_NUMBER_ID/messages ✅ # Wrong endpoint POST /v21.0/WABA_ID/messages ❌ ``` ### 3. Token Validation Passes but Scopes Missing **Symptom:** Token debug shows valid, but messaging fails with error 3/10 ```bash # Token is "valid" but missing scopes curl "https://graph.facebook.com/debug_token?input_token=TOKEN&access_token=TOKEN" # Response: { "is_valid": true, "scopes": ["manage_pages"] } ← NO whatsapp_business_messaging # Fix: Regenerate System User token with correct permissions ``` ### 4. Webhook Returns JSON Instead of Raw Challenge **Symptom:** Webhook verification fails silently in Meta dashboard **Wrong:** ```javascript return res.json({ challenge }); // ❌ returns JSON ``` **Correct:** ```javascript return res.status(200).send(challenge); // ✅ returns raw string ``` ### 5. WABA Not Subscribed to App **Symptom:** Webhooks never arrive (silent failure since 2025 Meta UI change) **Fix:** ```bash curl -X POST \ "https://graph.facebook.com/v21.0/WABA_ID/subscribed_apps" \ -H "[REDACTED_SECRET]" ``` ### 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:** ```bash # Check which phone numbers are in your WABA curl "https://graph.facebook.com/v21.0/WABA_ID/phone_numbers?fields=id,display_phone_number" \ -H "Authorization: Bearer YOUR_TOKEN" ``` ### 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:** ```bash 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 2. Create Admin system user 3. Add `whatsapp_business_messaging` + `whatsapp_business_management` permissions 4. Generate token — never set an expiry --- ### Fix: Phone Number Not Registered (Error 133010) **Step 1 — check phone number status:** ```bash curl "https://graph.facebook.com/v21.0/PHONE_NUMBER_ID?fields=verified_name,code_verification_status,quality_rating,status" \ -H "Authorization: Bearer YOUR_TOKEN" ``` 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 2. Click "Two-Step Verification" → set a PIN **Option B: API setup (easier for automation)** ```bash # Set 2FA PIN via API curl -X POST \ "https://graph.facebook.com/v21.0/PHONE_NUMBER_ID/two_step_verification" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{"pin": "123456"}' # any 6-digit PIN you choose ``` **Step 3 — register the number with the PIN:** ```bash curl -X POST \ "https://graph.facebook.com/v21.0/PHONE_NUMBER_ID/register" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{"messaging_product": "whatsapp", "pin": "123456"}' # same PIN from step 2 ``` **Step 4 — wait 5 minutes, then verify registration:** ```bash curl "https://graph.facebook.com/v21.0/PHONE_NUMBER_ID?fields=verified_name,code_verification_status,quality_rating,status" \ -H "Authorization: Bearer YOUR_TOKEN" ``` 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 2. **Token mismatch** — `hub.verify_token` Meta sends must match exactly what you set in the dashboard (case-sensitive) 3. **SSL issue** — Meta requires a valid cert from a trusted CA; self-signed certs are rejected 4. **Timeout** — your server must respond within 10 seconds 5. **WABA not subscribed to App** — common silent failure since 2025 Meta UI change: ```bash # Subscribe your WABA to your App (run once) curl -X POST \ "https://graph.facebook.com/v21.0/WABA_ID/subscribed_apps" \ -H "[REDACTED_SECRET]" # Verify the subscription exists curl "https://graph.facebook.com/v21.0/WABA_ID/subscribed_apps" \ -H "[REDACTED_SECRET]" ``` **Local development** — expose localhost with a tunnel: ```bash # 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: ```javascript // npm install limiter const { RateLimiter } = require('limiter'); const limiter = new RateLimiter({ tokensPerInterval: 70, interval: 'second' }); async function sendWithRetry(phone, message, attempt = 0) { await limiter.removeTokens(1); try { return await sendMessage(phone, message); } catch (err) { const code = err.response?.data?.error?.code; if (code === 130429 && attempt < 5) { const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s, 8s, 16s await new Promise(r => setTimeout(r, delay)); return sendWithRetry(phone, message, attempt + 1); } throw err; } } ``` 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. ```javascript // Instead of free-form text, send an approved template await sendTemplate(phoneNumber, 'order_update', 'en_US', [ { type: 'text', text: 'John' }, { type: 'text', text: '#4521' } ]); ``` 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: ```bash curl "https://graph.facebook.com/v21.0/PHONE_NUMBER_ID/contacts" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -X POST \ -d '{"messaging_product": "whatsapp", "contacts": ["+14155551234"]}' # Response includes "wa_id" if the number has WhatsApp, empty if not ``` --- ### 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. ``` ❌ Wrong — app sends free-form text first: POST /messages → type: "text", body: "Hello John, your order is ready" → API may return 200 but message is silently dropped or returns 131047 ✅ Correct — app sends template first: POST /messages → type: "template", name: "order_ready" → User receives the message and can reply → After user replies, free-form text is allowed for 24h ``` **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: ```python def is_valid_phone_format(phone_digits: str) -> bool: """ E.164 format validation: - 7-15 digits (not including +) - Not all same digit (e.g., 0000000 is invalid) """ if not phone_digits or len(phone_digits) < 7 or len(phone_digits) > 15: return False if len(set(phone_digits)) == 1: # all same digit return False return True def is_registered_on_whatsapp(phone_digits: str, token: str, phone_id: str) -> bool: """ Check if number is a registered WhatsApp user. Returns False only on definitive error 131026 (not on WhatsApp). Returns True if successful, uncertain (auth error, etc.), or timeout. """ headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json" } data = { "messaging_product": "whatsapp", "to": phone_digits, "type": "text", "text": {"body": "_"} # minimal text to check } try: r = requests.post( f"https://graph.facebook.com/v21.0/{phone_id}/messages", headers=headers, json=data, timeout=10 ) if r.status_code == 200: return True # number is valid error_code = r.json().get("error", {}).get("code") if error_code == 131026: return False # NOT on WhatsApp return True # other errors — don't block except: return True # network error — don't block ``` ### Structured Error Response Handling ```python def extract_error_code(response_json: dict) -> int: """Extract error code from Meta API response.""" return ( response_json.get("error", {}).get("code") or response_json.get("error", {}).get("error_subcode") ) def send_with_error_handling(phone_id: str, recipient: str, message_body: str, token: str): """Send message and extract detailed error info.""" url = f"https://graph.facebook.com/v21.0/{phone_id}/messages" headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json" } data = { "messaging_product": "whatsapp", "to": recipient, "type": "text", "text": {"body": message_body} } try: r = requests.post(url, headers=headers, json=data) r.raise_for_status() return {"success": True, "message_id": r.json().get("messages")[0].get("id")} except requests.HTTPError as e: error_code = extract_error_code(e.response.json()) error_msg = e.response.json().get("error", {}).get("message") return { "success": False, "error_code": error_code, "error_message": error_msg, "response_text": e.response.text } ``` ### Interactive Messages (Smart Buttons vs List) ```python def send_interactive_message(phone_id: str, recipient: str, text: str, options: list, token: str): """ Intelligently sends: - Buttons (up to 3 options) - List Menu (4-10 options) Each option: {"id": "unique_id", "title": "Text (max 20 chars)"} """ url = f"https://graph.facebook.com/v21.0/{phone_id}/messages" headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json" } if len(options) <= 3: # Send as buttons button_data = { "messaging_product": "whatsapp", "to": recipient, "type": "interactive", "interactive": { "type": "button", "body": {"text": text}, "action": { "buttons": [ { "type": "reply", "reply": {"id": opt["id"], "title": opt["title"][:20]} } for opt in options ] } } } return requests.post(url, headers=headers, json=button_data) else: # Send as list menu list_data = { "messaging_product": "whatsapp", "to": recipient, "type": "interactive", "interactive": { "type": "list", "body": {"text": text}, "action": { "button": "See options", "sections": [{ "title": "Available options", "rows": [ { "id": opt["id"], "title": opt["title"][:24] } for opt in options ] }] } } } return requests.post(url, headers=headers, json=list_data) ``` ### Testing & Debugging Helper Scripts **Token & Phone Verification Script:** ```python # Check if token is valid and phone number is accessible import requests, os from dotenv import load_dotenv load_dotenv() [REDACTED_SECRET]'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?** — `curl "https://graph.facebook.com/debug_token?input_token=TOKEN&access_token=APP_ID|APP_SECRET"` 2. **Phone number format?** — E.164, no `+`, no spaces: `"14155551234"` 3. **Number registered in WABA?** — check WhatsApp Manager → Phone Numbers 4. **Number registered with Cloud API?** — run registration call if error 133010 5. **24h window?** — if > 24h since last user reply, send a template instead 6. **Template approved?** — WhatsApp Manager → Message Templates → check status and rejection reason 7. **Webhook subscribed?** — verify WABA → App subscription via `GET /WABA_ID/subscribed_apps` 8. **Rate limited?** — check Quality Rating in WhatsApp Manager; implement backoff + queue


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)