home / skills / openclaw / skills / whatsapp-cloud-api-reference

whatsapp-cloud-api-reference skill

/skills/romanbaz/whatsapp-cloud-api-reference

This skill helps you implement and diagnose WhatsApp Cloud API messaging by enforcing templates, 24-hour windows, and delivery tracking.

npx playbooks add skill openclaw/skills --skill whatsapp-cloud-api-reference

Review the files below or copy the command above to add this skill to your agents.

Files (2)
SKILL.md
32.6 KB
---
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 "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -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 "Authorization: Bearer YOUR_SYSTEM_USER_TOKEN"
```

### 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 "Authorization: Bearer YOUR_SYSTEM_USER_TOKEN"

# Verify the subscription exists
curl "https://graph.facebook.com/v21.0/WABA_ID/subscribed_apps" \
  -H "Authorization: Bearer YOUR_SYSTEM_USER_TOKEN"
```

**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()

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?** — `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

Overview

This skill is a compact reference for implementing and troubleshooting WhatsApp messaging via the Meta (WhatsApp) Cloud API. It explains required setup, message types, webhook patterns, delivery tracking, quality rating impacts, and common failure modes. Use it to implement reliable sending, handle errors, and keep your WABA healthy.

How this skill works

The skill inspects implementation steps and runtime flows: app registration, system user tokens, phone-number registration, webhook verification, sending templates and media, and delivery webhook processing. It summarizes constraints (24-hour window, template requirements, media URL rules), rate limits, API versioning, and quality-rating causes so you can diagnose failures and apply fixes quickly.

When to use it

  • Implement a new WhatsApp Cloud API integration from scratch
  • Debug message delivery failures, webhook retries, or token errors
  • Set up or verify webhook handling and immediate 200 responses
  • Prepare and send template messages when outside the 24-hour window
  • Troubleshoot WABA quality rating drops, registration errors, or template rejections

Best practices

  • Always use a permanent System User token and store API version in config
  • Never send free-form text as the first business message—use approved templates
  • Return HTTP 200 immediately on webhook POST and process payloads asynchronously
  • Validate phone numbers and opt-in status before sending to avoid bounces
  • Host media on HTTPS public URLs (no shorteners) and respect file size/format limits
  • Monitor quality_rating via the API and back off or clean lists if metrics degrade

Example use cases

  • Send transactional templates (order updates, OTPs) when a user hasn’t replied in 24 hours
  • Implement webhook consumer that matches message_id updates to delivery status in your DB
  • Troubleshoot registration error 133010 when a number is active on personal WhatsApp
  • Detect and respond to ‘failed’ statuses: log, notify, and apply retry/backoff
  • Prepare templates for approval following placeholder and wording constraints

FAQ

What must be used for the first message to a user?

A pre-approved template message; free-form text is allowed only after the user replies and within the 24-hour window.

Why am I getting delivery failures even after successful send response?

Check webhook status updates: failures can be due to unregistered numbers, network or format issues, or user device problems; inspect statuses in webhook payload and correlate message_id.

How should webhooks respond to Meta to avoid retries?

Return HTTP 200 immediately for POST requests and process the payload asynchronously; for GET verification return the challenge string raw when the verify token matches.