{
  "schemaVersion": "1.0",
  "item": {
    "slug": "whatsapp-cloud-api-reference",
    "name": "WhatsApp cloud api reference",
    "source": "tencent",
    "type": "skill",
    "category": "开发工具",
    "sourceUrl": "https://clawhub.ai/RomanBaz/whatsapp-cloud-api-reference",
    "canonicalUrl": "https://clawhub.ai/RomanBaz/whatsapp-cloud-api-reference",
    "targetPlatform": "OpenClaw"
  },
  "install": {
    "downloadMode": "redirect",
    "downloadUrl": "/downloads/whatsapp-cloud-api-reference",
    "sourceDownloadUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=whatsapp-cloud-api-reference",
    "sourcePlatform": "tencent",
    "targetPlatform": "OpenClaw",
    "installMethod": "Manual import",
    "extraction": "Extract archive",
    "prerequisites": [
      "OpenClaw"
    ],
    "packageFormat": "ZIP package",
    "includedAssets": [
      "SKILL.md"
    ],
    "primaryDoc": "SKILL.md",
    "quickSetup": [
      "Download the package from Yavira.",
      "Extract the archive and review SKILL.md first.",
      "Import or place the package into your OpenClaw setup."
    ],
    "agentAssist": {
      "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
      "steps": [
        "Download the package from Yavira.",
        "Extract it into a folder your agent can access.",
        "Paste one of the prompts below and point your agent at the extracted folder."
      ],
      "prompts": [
        {
          "label": "New install",
          "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Tell me what you changed and call out any manual steps you could not complete."
        },
        {
          "label": "Upgrade existing",
          "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Summarize what changed and any follow-up checks I should run."
        }
      ]
    },
    "sourceHealth": {
      "source": "tencent",
      "status": "healthy",
      "reason": "direct_download_ok",
      "recommendedAction": "download",
      "checkedAt": "2026-05-07T17:22:31.273Z",
      "expiresAt": "2026-05-14T17:22:31.273Z",
      "httpStatus": 200,
      "finalUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=afrexai-annual-report",
      "contentType": "application/zip",
      "probeMethod": "head",
      "details": {
        "probeUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=afrexai-annual-report",
        "contentDisposition": "attachment; filename=\"afrexai-annual-report-1.0.0.zip\"",
        "redirectLocation": null,
        "bodySnippet": null
      },
      "scope": "source",
      "summary": "Source download looks usable.",
      "detail": "Yavira can redirect you to the upstream package for this source.",
      "primaryActionLabel": "Download for OpenClaw",
      "primaryActionHref": "/downloads/whatsapp-cloud-api-reference"
    },
    "validation": {
      "installChecklist": [
        "Use the Yavira download entry.",
        "Review SKILL.md after the package is downloaded.",
        "Confirm the extracted package contains the expected setup assets."
      ],
      "postInstallChecks": [
        "Confirm the extracted package includes the expected docs or setup files.",
        "Validate the skill or prompts are available in your target agent workspace.",
        "Capture any manual follow-up steps the agent could not complete."
      ]
    },
    "downloadPageUrl": "https://openagent3.xyz/downloads/whatsapp-cloud-api-reference",
    "agentPageUrl": "https://openagent3.xyz/skills/whatsapp-cloud-api-reference/agent",
    "manifestUrl": "https://openagent3.xyz/skills/whatsapp-cloud-api-reference/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/whatsapp-cloud-api-reference/agent.md"
  },
  "agentAssist": {
    "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
    "steps": [
      "Download the package from Yavira.",
      "Extract it into a folder your agent can access.",
      "Paste one of the prompts below and point your agent at the extracted folder."
    ],
    "prompts": [
      {
        "label": "New install",
        "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Tell me what you changed and call out any manual steps you could not complete."
      },
      {
        "label": "Upgrade existing",
        "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Summarize what changed and any follow-up checks I should run."
      }
    ]
  },
  "documentation": {
    "source": "clawhub",
    "primaryDoc": "SKILL.md",
    "sections": [
      {
        "title": "Overview",
        "body": "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.\n\nKey rules:\n\nYour 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.\nBusiness-initiated messages outside a 24h reply window must use a pre-approved template\nPhone numbers must be registered in your WABA before sending\nAlways use a System User token — user tokens expire in 24 hours\n\nConversation flow:\n\nApp → user:  MUST be a template (always, for first contact)\nUser → app:  reply opens a 24-hour free-form window\nApp → user:  free-form text allowed within that 24h window\n  [24h passes with no user reply]\nApp → user:  MUST use a template again to re-engage"
      },
      {
        "title": "Setup Checklist",
        "body": "Create Meta Developer App — developers.facebook.com → Create App → Business type\nAdd WhatsApp product to the app (gives temp test number + 5 test recipient slots)\nCreate a permanent System User token:\n\nMeta Business Manager → Settings → System Users → Create Admin user\nAssign permissions: whatsapp_business_messaging + whatsapp_business_management\nGenerate token — this never expires\n\n\nRegister real phone number — number cannot already be active on personal/business WhatsApp\nSet up webhook — needs public HTTPS URL (trusted CA cert, no self-signed), must respond in < 10s"
      },
      {
        "title": "Text Message (Node.js)",
        "body": "// npm install axios\nconst axios = require('axios');\n\nasync function sendMessage(phoneNumber, text) {\n    // phoneNumber: E.164 without +, e.g. \"14155551234\"\n    const res = await axios.post(\n        `https://graph.facebook.com/v21.0/${process.env.WA_PHONE_NUMBER_ID}/messages`,\n        {\n            messaging_product: 'whatsapp',\n            recipient_type: 'individual',\n            to: phoneNumber,\n            type: 'text',\n            text: { preview_url: false, body: text }\n        },\n        { headers: { Authorization: `Bearer ${process.env.WA_ACCESS_TOKEN}` } }\n    );\n    return res.data;\n}"
      },
      {
        "title": "Text Message (Python)",
        "body": "# pip install requests\nimport requests, os\n\ndef send_message(phone: str, text: str) -> dict:\n    r = requests.post(\n        f\"https://graph.facebook.com/v21.0/{os.environ['WA_PHONE_NUMBER_ID']}/messages\",\n        headers={\"Authorization\": f\"Bearer {os.environ['WA_ACCESS_TOKEN']}\"},\n        json={\n            \"messaging_product\": \"whatsapp\",\n            \"recipient_type\": \"individual\",\n            \"to\": phone,        # E.164 without +\n            \"type\": \"text\",\n            \"text\": {\"preview_url\": False, \"body\": text}\n        }\n    )\n    r.raise_for_status()\n    return r.json()"
      },
      {
        "title": "Quick test via curl",
        "body": "curl -X POST \"https://graph.facebook.com/v21.0/YOUR_PHONE_ID/messages\" \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"messaging_product\":\"whatsapp\",\"to\":\"14155551234\",\"type\":\"text\",\"text\":{\"body\":\"Hello\"}}'"
      },
      {
        "title": "Template Message (required when > 24h since last user reply)",
        "body": "const payload = {\n    messaging_product: 'whatsapp',\n    to: phoneNumber,\n    type: 'template',\n    template: {\n        name: 'hello_world',           // your approved template name\n        language: { code: 'en_US' },\n        components: [{\n            type: 'body',\n            parameters: [\n                { type: 'text', text: 'John' },        // fills {{1}}\n                { type: 'text', text: 'Order #4521' }  // fills {{2}}\n            ]\n        }]\n    }\n};"
      },
      {
        "title": "Media Messages (Image, Document, Audio, Video)",
        "body": "Image:\n\nconst payload = {\n    messaging_product: 'whatsapp',\n    to: phoneNumber,\n    type: 'image',\n    image: {\n        link: 'https://your-domain.com/image.jpg'  // must be publicly accessible HTTPS\n    }\n};\n\nDocument:\n\nconst payload = {\n    messaging_product: 'whatsapp',\n    to: phoneNumber,\n    type: 'document',\n    document: {\n        link: 'https://your-domain.com/file.pdf',\n        caption: 'Invoice'  // optional\n    }\n};\n\nAudio:\n\nconst payload = {\n    messaging_product: 'whatsapp',\n    to: phoneNumber,\n    type: 'audio',\n    audio: {\n        link: 'https://your-domain.com/audio.mp3'\n    }\n};\n\nVideo:\n\nconst payload = {\n    messaging_product: 'whatsapp',\n    to: phoneNumber,\n    type: 'video',\n    video: {\n        link: 'https://your-domain.com/video.mp4',\n        caption: 'Demo video'  // optional\n    }\n};\n\nImportant constraints:\n\nAll media URLs must be publicly accessible HTTPS (http:// fails)\nMax file sizes: Image 16MB, Document 100MB, Audio 16MB, Video 16MB\nSupported formats: Images (JPEG, PNG), Documents (PDF), Audio (AAC, MP3, OGG, WAV), Video (MP4, 3GPP)\nMedia must not require authentication\nURLs cannot use shorteners (bit.ly, tinyurl, etc.)"
      },
      {
        "title": "Webhook Handler (Express) — Correct Async Pattern",
        "body": "// GET — Meta calls this to verify your endpoint\napp.get('/webhook', (req, res) => {\n    const { 'hub.mode': mode, 'hub.verify_token': token, 'hub.challenge': challenge } = req.query;\n    if (mode === 'subscribe' && token === process.env.VERIFY_TOKEN)\n        return res.status(200).send(challenge);  // raw string only — NOT JSON\n    res.sendStatus(403);\n});\n\n// POST — CRITICAL: return 200 IMMEDIATELY, process async\napp.post('/webhook', express.json(), (req, res) => {\n    // Return 200 immediately so Meta doesn't retry\n    res.sendStatus(200);\n\n    // Process webhook payload asynchronously (don't block)\n    setImmediate(() => {\n        processWebhookAsync(req.body).catch(err => {\n            logger.error(`Webhook processing failed: ${err.message}`);\n        });\n    });\n});\n\nasync function processWebhookAsync(body) {\n    body.entry?.forEach(entry =>\n        entry.changes?.forEach(change => {\n            const value = change.value;\n\n            // Incoming messages\n            if (value.messages) {\n                value.messages.forEach(msg => {\n                    console.log(`Message from ${msg.from}: ${msg.text?.body}`);\n                    handleMessage(msg);\n                });\n            }\n\n            // Delivery status\n            if (value.statuses) {\n                value.statuses.forEach(status => {\n                    console.log(`Message ${status.id} status: ${status.status}`);\n                    handleDeliveryStatus(status);\n                });\n            }\n        })\n    );\n}"
      },
      {
        "title": "Webhook Payload: Delivery Status",
        "body": "Meta sends status updates via webhook when a message is delivered, read, or fails:\n\n{\n  \"object\": \"whatsapp_business_account\",\n  \"entry\": [{\n    \"changes\": [{\n      \"value\": {\n        \"statuses\": [{\n          \"id\": \"wamid.xxx\",           // Message ID from your send response\n          \"status\": \"delivered\",       // \"sent\" | \"delivered\" | \"read\" | \"failed\"\n          \"timestamp\": \"1675262308\",\n          \"recipient_id\": \"14155551234\",\n          \"type\": \"message\"\n        }]\n      }\n    }]\n  }]\n}\n\nStatus values:\n\nsent — Message reached Meta servers\ndelivered — Message delivered to user's device\nread — User opened the message\nfailed — Delivery failed (permanent)"
      },
      {
        "title": "Tracking Message Delivery",
        "body": "// When you send, store the message ID\nconst sendResult = await sendMessage(phone, text);\nconst messageId = sendResult.messages[0].id;\n\n// Log it for webhook tracking\ndb.messages.insert({\n    message_id: messageId,\n    recipient: phone,\n    sent_at: Date.now(),\n    status: 'sent',\n    body: text\n});\n\n// When webhook arrives with status update, match by message_id\nfunction handleDeliveryStatus(statusUpdate) {\n    const { id, status, recipient_id } = statusUpdate;\n\n    // Update your database\n    db.messages.updateOne(\n        { message_id: id },\n        { status: status, updated_at: Date.now() }\n    );\n\n    // Handle delivery failures\n    if (status === 'failed') {\n        logger.error(`Message ${id} failed to deliver to ${recipient_id}`);\n        // Retry logic here\n    }\n}"
      },
      {
        "title": "What is Quality Rating?",
        "body": "Your WhatsApp Business Account (WABA) has a quality rating that affects your sending ability:\n\nRatingImpactRecoveryGREENFull functionality, no restrictionsMaintain this (stay green)YELLOWSlight rate limit reduction, monitor closelyImprove within 7 days or drops to REDREDSevere restrictions, may lose messaging accessContact Meta Support"
      },
      {
        "title": "How to Check Quality Rating",
        "body": "curl \"https://graph.facebook.com/v21.0/PHONE_NUMBER_ID?fields=quality_rating\" \\\n  -H \"Authorization: Bearer YOUR_TOKEN\"\n\n# Response: { \"quality_rating\": \"GREEN\" }"
      },
      {
        "title": "What Causes Quality Rating to Drop?",
        "body": "High bounce rate — sending to invalid/inactive numbers\nSpam reports — users marking your messages as spam\nHigh failure rate — messages consistently failing to deliver\nUser blocks — users blocking your number after messages\nPolicy violations — sending prohibited content"
      },
      {
        "title": "How to Improve Quality Rating",
        "body": "Validate phone numbers before sending — use the WhatsApp contacts check (error 131026)\nOnly message opted-in users — don't send unsolicited messages\nKeep template content transactional — avoid marketing spam\nMonitor quality metrics — check rating regularly via API\nRespect user preferences — remove users who opt out\nDon't retry failed numbers aggressively — wait before retrying same number"
      },
      {
        "title": "Group Messages",
        "body": "Note: WhatsApp Business API does NOT support group messaging directly. You can only send to individual recipients (1:1 conversations).\n\nIf you need group functionality:\n\nUsers must add your business number to a group manually\nMessages sent to the group are treated as individual 1:1 messages\nYou cannot initiate group conversations programmatically"
      },
      {
        "title": "API Versioning Strategy",
        "body": "All examples use v21.0 (current as of February 2026). Meta deprecates API versions annually."
      },
      {
        "title": "Checking Your Current Version",
        "body": "# List all available versions\ncurl \"https://graph.facebook.com/versions\" \\\n  -H \"Authorization: Bearer YOUR_TOKEN\"\n\n# Current version recommendations\n# - v21.0 (current, recommended)\n# - v20.0 (previous, will deprecate in 6 months)"
      },
      {
        "title": "Version Update Strategy",
        "body": "// Store version in config, not hardcoded\nconst API_VERSION = process.env.WHATSAPP_API_VERSION || 'v21.0';\n\nconst url = `https://graph.facebook.com/${API_VERSION}/${PHONE_NUMBER_ID}/messages`;\n\n// When Meta deprecates a version, update .env:\n// WHATSAPP_API_VERSION=v22.0"
      },
      {
        "title": "What Changes Between Versions?",
        "body": "New message types or features added\nDeprecated fields removed\nError codes may change slightly\nResponse payload structure may change\n\nAlways test before upgrading — make requests against the new version in your dev environment first."
      },
      {
        "title": "Text Message Limits",
        "body": "ConstraintLimitText body max length4,096 charactersLink previewEnabled by default, disable with preview_url: falseCarriage returns / newlinesSupported (use \\n)"
      },
      {
        "title": "Interactive Message Limits",
        "body": "TypeItemsCharacter LimitButton1-3Title: 20 charsList1-10Title: 24 chars per row"
      },
      {
        "title": "Media URL Requirements",
        "body": "Must be HTTPS only (http:// rejected)\nMust be publicly accessible (no authentication required)\nMust NOT use shorteners (bit.ly, tinyurl rejected)\nMust have correct Content-Type header\nMax file sizes:\n\nImage: 16 MB\nAudio: 16 MB\nVideo: 16 MB\nDocument: 100 MB"
      },
      {
        "title": "Rate Limits (Per WABA)",
        "body": "LimitValueDefault throughput80 messages/secondBurst capacity1,000 messages/second (request increase)Requests per minute60 API calls/minute"
      },
      {
        "title": "Template Submission Workflow",
        "body": "Create template in Meta Business Manager\n        ↓\nSubmit for review (human review by Meta)\n        ↓\nStatus: PENDING (24-72 hours typical)\n        ↓\nStatus: APPROVED (can now use in messages)\n    OR\nStatus: REJECTED (reason provided in dashboard)"
      },
      {
        "title": "How Long Does Approval Take?",
        "body": "Typical: 24-72 hours\nPeak times (weekends, holidays): up to 7 days\nFast-track: Available for high-volume WABAs (request in Meta Support)"
      },
      {
        "title": "Common Approval Issues",
        "body": "IssueWhy RejectedVariable formatMust use {{1}}, {{2}} formatTemplate starts/ends with variableMust have text before first variableURL shortenersUse full domain URLs onlyPlaceholder qualityPlaceholder values must be realistic examplesSensitive data requestNever ask for SSN, card numbers, passwordsUnclear purposePurpose field must clearly state intentWarm language in utilityUse formal wording; warmth triggers \"marketing\" category (costs more)Duplicate templateName/wording too similar to existing template\n\nCheck rejection reason in Meta Business Manager → Business Support → Rejected Template Messages."
      },
      {
        "title": "1. Phone Number Already on Personal WhatsApp",
        "body": "Symptom: Registration fails with error 133010, status stays PENDING\n\nCause: Phone number is already active on a personal WhatsApp account\n\nFix:\n\n1. Remove phone from personal WhatsApp (go to Settings → Devices → Remove phone)\n2. Wait 24 hours\n3. Re-run registration API call"
      },
      {
        "title": "2. Mixing Phone Number ID with WABA ID",
        "body": "Symptom: API returns \"Invalid parameters\" for phone operations\n\nHow to tell them apart:\n\nPHONE_NUMBER_ID: 120######## (11-12 digits, starts with 120)\nWABA_ID: ######### (9-10 digits, higher number)\n\n# Correct endpoint\nPOST /v21.0/PHONE_NUMBER_ID/messages  ✅\n\n# Wrong endpoint\nPOST /v21.0/WABA_ID/messages  ❌"
      },
      {
        "title": "3. Token Validation Passes but Scopes Missing",
        "body": "Symptom: Token debug shows valid, but messaging fails with error 3/10\n\n# Token is \"valid\" but missing scopes\ncurl \"https://graph.facebook.com/debug_token?input_token=TOKEN&access_token=TOKEN\"\n# Response: { \"is_valid\": true, \"scopes\": [\"manage_pages\"] }  ← NO whatsapp_business_messaging\n\n# Fix: Regenerate System User token with correct permissions"
      },
      {
        "title": "4. Webhook Returns JSON Instead of Raw Challenge",
        "body": "Symptom: Webhook verification fails silently in Meta dashboard\n\nWrong:\n\nreturn res.json({ challenge });  // ❌ returns JSON\n\nCorrect:\n\nreturn res.status(200).send(challenge);  // ✅ returns raw string"
      },
      {
        "title": "5. WABA Not Subscribed to App",
        "body": "Symptom: Webhooks never arrive (silent failure since 2025 Meta UI change)\n\nFix:\n\ncurl -X POST \\\n  \"https://graph.facebook.com/v21.0/WABA_ID/subscribed_apps\" \\\n  -H \"Authorization: Bearer YOUR_SYSTEM_USER_TOKEN\""
      },
      {
        "title": "6. Sending Template Too Early in Approval Process",
        "body": "Symptom: Error 132001 \"Template Unavailable\"\n\nCause: Template still in PENDING status, not yet APPROVED\n\nFix: Check status in Meta Business Manager → Message Templates → wait for APPROVED status"
      },
      {
        "title": "7. Phone Number Not in WABA",
        "body": "Symptom: Error 131009 when trying to send\n\nHow to verify:\n\n# Check which phone numbers are in your WABA\ncurl \"https://graph.facebook.com/v21.0/WABA_ID/phone_numbers?fields=id,display_phone_number\" \\\n  -H \"Authorization: Bearer YOUR_TOKEN\""
      },
      {
        "title": "8. Sending Free-Form Text as First Message",
        "body": "Symptom: API returns 200, message ID issued, but user never receives it\n\nRoot cause: Only templates allowed as first message to any number\n\nFix: Always use a template for first contact"
      },
      {
        "title": "Phone Number Status Reference",
        "body": "Check the status field via API. Each status blocks different operations:\n\nStatusMeaningActionPENDINGNumber is registered but not verifiedSet up 2FA (either manual or API), then run register callREGISTEREDNumber is verified and readyCheck code_verification_status — should be VERIFIEDFLAGGEDAccount or number under review for policy violationContact Meta SupportBANNEDNumber permanently disabledContact Meta Support"
      },
      {
        "title": "Error Code Reference",
        "body": "CodeNameCauseFix190Token ExpiredUser token (24h lifetime) used in productionSwitch to System User token; debug at developers.facebook.com/tools/debug/accesstoken3 / 10Permission DeniedToken missing required scopesRegenerate System User token with whatsapp_business_messaging + whatsapp_business_management100Invalid ParameterMisspelled field or wrong valueCheck request body against API docs; verify phone number format (E.164, no +)130429Rate Limit (MPS)Exceeded 80 messages/sec defaultAdd send queue + exponential backoff (see below)13104724h Window Expired> 24h since customer last repliedReplace free-form text with a pre-approved template message131026UndeliverableRecipient blocked you, no WhatsApp, or outdated appVerify recipient number; confirm they have WhatsApp installed and accepted Meta terms131048Spam Rate LimitMessages flagged as spamCheck Quality Rating in WhatsApp Manager; review message content and opt-in practices131056Pair Rate LimitToo many messages to same recipient too fastWait before retrying the same number131009Invalid Parameter ValuePhone number not in WABA, or wrong parameterVerify number is registered in your WABA under Phone Numbers131021Same Sender/Recipientfrom and to are the same numberUse a different recipient131031Account LockedPolicy violation or wrong 2-step PINContact Meta Support132001Template UnavailableWrong template name, wrong language code, or not yet approvedCheck WhatsApp Manager → Message Templates for exact name, language, and status133010Phone Not RegisteredSender number not registered in Cloud APIRun the registration API call (see below)368Policy ViolationAccount restrictedContact Meta Support1 / 2API Service ErrorMeta outage or server errorCheck metastatus.com; retry with exponential backoff"
      },
      {
        "title": "Fix: Token Problems (Error 190, 3, 10)",
        "body": "Diagnose first:\n\ncurl \"https://graph.facebook.com/debug_token?input_token=YOUR_TOKEN&access_token=YOUR_APP_ID|YOUR_APP_SECRET\"\n\nCheck is_valid, expires_at (0 = never expires), and scopes in the response.\n\nFix — create a non-expiring System User token:\n\nMeta Business Manager → Settings → System Users\nCreate Admin system user\nAdd whatsapp_business_messaging + whatsapp_business_management permissions\nGenerate token — never set an expiry"
      },
      {
        "title": "Fix: Phone Number Not Registered (Error 133010)",
        "body": "Step 1 — check phone number status:\n\ncurl \"https://graph.facebook.com/v21.0/PHONE_NUMBER_ID?fields=verified_name,code_verification_status,quality_rating,status\" \\\n  -H \"Authorization: Bearer YOUR_TOKEN\"\n\nIf status is PENDING, the number is waiting for verification. Continue below.\n\nStep 2 — set up Two-Step Verification (choose ONE method):\n\nOption A: Manual setup (easy)\n\nMeta Business Manager → WhatsApp Settings → Phone Numbers → Your Number\nClick \"Two-Step Verification\" → set a PIN\n\nOption B: API setup (easier for automation)\n\n# Set 2FA PIN via API\ncurl -X POST \\\n  \"https://graph.facebook.com/v21.0/PHONE_NUMBER_ID/two_step_verification\" \\\n  -H \"Authorization: Bearer YOUR_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"pin\": \"123456\"}'  # any 6-digit PIN you choose\n\nStep 3 — register the number with the PIN:\n\ncurl -X POST \\\n  \"https://graph.facebook.com/v21.0/PHONE_NUMBER_ID/register\" \\\n  -H \"Authorization: Bearer YOUR_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"messaging_product\": \"whatsapp\", \"pin\": \"123456\"}'  # same PIN from step 2\n\nStep 4 — wait 5 minutes, then verify registration:\n\ncurl \"https://graph.facebook.com/v21.0/PHONE_NUMBER_ID?fields=verified_name,code_verification_status,quality_rating,status\" \\\n  -H \"Authorization: Bearer YOUR_TOKEN\"\n\nLook for status: REGISTERED and code_verification_status: VERIFIED."
      },
      {
        "title": "Fix: Webhook Not Verifying",
        "body": "Diagnose in this order:\n\nNot returning raw challenge — endpoint must return the hub.challenge string value only, not JSON\nToken mismatch — hub.verify_token Meta sends must match exactly what you set in the dashboard (case-sensitive)\nSSL issue — Meta requires a valid cert from a trusted CA; self-signed certs are rejected\nTimeout — your server must respond within 10 seconds\nWABA not subscribed to App — common silent failure since 2025 Meta UI change:\n\n# Subscribe your WABA to your App (run once)\ncurl -X POST \\\n  \"https://graph.facebook.com/v21.0/WABA_ID/subscribed_apps\" \\\n  -H \"Authorization: Bearer YOUR_SYSTEM_USER_TOKEN\"\n\n# Verify the subscription exists\ncurl \"https://graph.facebook.com/v21.0/WABA_ID/subscribed_apps\" \\\n  -H \"Authorization: Bearer YOUR_SYSTEM_USER_TOKEN\"\n\nLocal development — expose localhost with a tunnel:\n\n# Using ngrok\nngrok http 3000\n# Use the https:// URL ngrok provides as your webhook callback URL in Meta"
      },
      {
        "title": "Fix: Rate Limiting (Error 130429)",
        "body": "Default limit is 80 messages per second. Fix with a queue and exponential backoff:\n\n// npm install limiter\nconst { RateLimiter } = require('limiter');\nconst limiter = new RateLimiter({ tokensPerInterval: 70, interval: 'second' });\n\nasync function sendWithRetry(phone, message, attempt = 0) {\n    await limiter.removeTokens(1);\n    try {\n        return await sendMessage(phone, message);\n    } catch (err) {\n        const code = err.response?.data?.error?.code;\n        if (code === 130429 && attempt < 5) {\n            const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s, 8s, 16s\n            await new Promise(r => setTimeout(r, delay));\n            return sendWithRetry(phone, message, attempt + 1);\n        }\n        throw err;\n    }\n}\n\nTo increase throughput beyond 80 MPS, apply in Meta Business Manager → WhatsApp → Phone Numbers → Request Increased Messaging Limit."
      },
      {
        "title": "Fix: 24-Hour Window (Error 131047)",
        "body": "You cannot send free-form text to a user more than 24 hours after their last message. You must use a template.\n\n// Instead of free-form text, send an approved template\nawait sendTemplate(phoneNumber, 'order_update', 'en_US', [\n    { type: 'text', text: 'John' },\n    { type: 'text', text: '#4521' }\n]);\n\nCreate and submit templates at: Meta Business Manager → WhatsApp → Message Templates."
      },
      {
        "title": "Fix: Template Rejected (Error 132001 or rejection in WhatsApp Manager)",
        "body": "Rejection ReasonFixVariable format wrongUse {{1}}, {{2}} — double curly braces, sequential integers onlyTemplate starts/ends with variableAdd plain text before {{1}} and after the last variableVariables not sequentialMust be {{1}}, {{2}} — no gaps allowedURL shorteners usedUse full, unshortened URLs to your own domainLanguage code mismatchMatch language.code to the actual content language, e.g. en_US, pt_BRWarm language in utility templateUse formal transactional wording; warm language causes auto-reclassification to marketing categorySensitive dataNever request SSNs, full card numbers, or passwordsDuplicate of existing templateChange the wording — even minor variation is requiredPurpose unclearEach variable must have a descriptive example value in the template submission\n\nWhere to find rejection reason:\nMeta Business Manager → Business Support Home → Your WhatsApp Account → Rejected Template Messages → view policy issue."
      },
      {
        "title": "Fix: Message Undeliverable (Error 131026)",
        "body": "Run this to check if a number has WhatsApp before sending:\n\ncurl \"https://graph.facebook.com/v21.0/PHONE_NUMBER_ID/contacts\" \\\n  -H \"Authorization: Bearer YOUR_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -X POST \\\n  -d '{\"messaging_product\": \"whatsapp\", \"contacts\": [\"+14155551234\"]}'\n# Response includes \"wa_id\" if the number has WhatsApp, empty if not"
      },
      {
        "title": "Diagnose: \"My message sends but the user never receives it\" (first-contact trap)",
        "body": "This is the most common silent failure. The API returns success (messages[0].id) but the user receives nothing.\n\nRoot cause: You sent a free-form text message as the first outreach. Meta silently drops it.\n\nHow to tell: Check the message status webhook — the message will show failed with error 131047 or show sent but never delivered.\n\nRule: The very first message your app sends to any number must be a template. No exceptions.\n\n❌ Wrong — app sends free-form text first:\n   POST /messages → type: \"text\", body: \"Hello John, your order is ready\"\n   → API may return 200 but message is silently dropped or returns 131047\n\n✅ Correct — app sends template first:\n   POST /messages → type: \"template\", name: \"order_ready\"\n   → User receives the message and can reply\n   → After user replies, free-form text is allowed for 24h\n\nFix: 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."
      },
      {
        "title": "Phone Number Validation Before Sending",
        "body": "Always validate phone number format and WhatsApp registration before sending:\n\ndef is_valid_phone_format(phone_digits: str) -> bool:\n    \"\"\"\n    E.164 format validation:\n      - 7-15 digits (not including +)\n      - Not all same digit (e.g., 0000000 is invalid)\n    \"\"\"\n    if not phone_digits or len(phone_digits) < 7 or len(phone_digits) > 15:\n        return False\n    if len(set(phone_digits)) == 1:  # all same digit\n        return False\n    return True\n\ndef is_registered_on_whatsapp(phone_digits: str, token: str, phone_id: str) -> bool:\n    \"\"\"\n    Check if number is a registered WhatsApp user.\n    Returns False only on definitive error 131026 (not on WhatsApp).\n    Returns True if successful, uncertain (auth error, etc.), or timeout.\n    \"\"\"\n    headers = {\n        \"Authorization\": f\"Bearer {token}\",\n        \"Content-Type\": \"application/json\"\n    }\n    data = {\n        \"messaging_product\": \"whatsapp\",\n        \"to\": phone_digits,\n        \"type\": \"text\",\n        \"text\": {\"body\": \"_\"}  # minimal text to check\n    }\n    try:\n        r = requests.post(\n            f\"https://graph.facebook.com/v21.0/{phone_id}/messages\",\n            headers=headers,\n            json=data,\n            timeout=10\n        )\n        if r.status_code == 200:\n            return True  # number is valid\n\n        error_code = r.json().get(\"error\", {}).get(\"code\")\n        if error_code == 131026:\n            return False  # NOT on WhatsApp\n\n        return True  # other errors — don't block\n    except:\n        return True  # network error — don't block"
      },
      {
        "title": "Structured Error Response Handling",
        "body": "def extract_error_code(response_json: dict) -> int:\n    \"\"\"Extract error code from Meta API response.\"\"\"\n    return (\n        response_json.get(\"error\", {}).get(\"code\")\n        or response_json.get(\"error\", {}).get(\"error_subcode\")\n    )\n\ndef send_with_error_handling(phone_id: str, recipient: str, message_body: str, token: str):\n    \"\"\"Send message and extract detailed error info.\"\"\"\n    url = f\"https://graph.facebook.com/v21.0/{phone_id}/messages\"\n    headers = {\n        \"Authorization\": f\"Bearer {token}\",\n        \"Content-Type\": \"application/json\"\n    }\n    data = {\n        \"messaging_product\": \"whatsapp\",\n        \"to\": recipient,\n        \"type\": \"text\",\n        \"text\": {\"body\": message_body}\n    }\n\n    try:\n        r = requests.post(url, headers=headers, json=data)\n        r.raise_for_status()\n        return {\"success\": True, \"message_id\": r.json().get(\"messages\")[0].get(\"id\")}\n    except requests.HTTPError as e:\n        error_code = extract_error_code(e.response.json())\n        error_msg = e.response.json().get(\"error\", {}).get(\"message\")\n        return {\n            \"success\": False,\n            \"error_code\": error_code,\n            \"error_message\": error_msg,\n            \"response_text\": e.response.text\n        }"
      },
      {
        "title": "Interactive Messages (Smart Buttons vs List)",
        "body": "def send_interactive_message(phone_id: str, recipient: str, text: str, options: list, token: str):\n    \"\"\"\n    Intelligently sends:\n      - Buttons (up to 3 options)\n      - List Menu (4-10 options)\n\n    Each option: {\"id\": \"unique_id\", \"title\": \"Text (max 20 chars)\"}\n    \"\"\"\n    url = f\"https://graph.facebook.com/v21.0/{phone_id}/messages\"\n    headers = {\n        \"Authorization\": f\"Bearer {token}\",\n        \"Content-Type\": \"application/json\"\n    }\n\n    if len(options) <= 3:\n        # Send as buttons\n        button_data = {\n            \"messaging_product\": \"whatsapp\",\n            \"to\": recipient,\n            \"type\": \"interactive\",\n            \"interactive\": {\n                \"type\": \"button\",\n                \"body\": {\"text\": text},\n                \"action\": {\n                    \"buttons\": [\n                        {\n                            \"type\": \"reply\",\n                            \"reply\": {\"id\": opt[\"id\"], \"title\": opt[\"title\"][:20]}\n                        }\n                        for opt in options\n                    ]\n                }\n            }\n        }\n        return requests.post(url, headers=headers, json=button_data)\n    else:\n        # Send as list menu\n        list_data = {\n            \"messaging_product\": \"whatsapp\",\n            \"to\": recipient,\n            \"type\": \"interactive\",\n            \"interactive\": {\n                \"type\": \"list\",\n                \"body\": {\"text\": text},\n                \"action\": {\n                    \"button\": \"See options\",\n                    \"sections\": [{\n                        \"title\": \"Available options\",\n                        \"rows\": [\n                            {\n                                \"id\": opt[\"id\"],\n                                \"title\": opt[\"title\"][:24]\n                            }\n                            for opt in options\n                        ]\n                    }]\n                }\n            }\n        }\n        return requests.post(url, headers=headers, json=list_data)"
      },
      {
        "title": "Testing & Debugging Helper Scripts",
        "body": "Token & Phone Verification Script:\n\n# Check if token is valid and phone number is accessible\nimport requests, os\nfrom dotenv import load_dotenv\nload_dotenv()\n\nTOKEN = os.getenv('WHATSAPP_TOKEN')\nPHONE_ID = os.getenv('PHONE_NUMBER_ID')\n\n# Debug token\nr = requests.get(f\"https://graph.facebook.com/debug_token?input_token={TOKEN}&access_token={TOKEN}\")\ndata = r.json()\nif 'error' in data:\n    print(f\"❌ Token Error: {data['error']['message']}\")\nelse:\n    print(f\"✅ Token Valid\")\n    print(f\"   Expires: {data['data'].get('expires_at')} (0=never)\")\n    print(f\"   Scopes: {data['data'].get('scopes')}\")\n\n# Check phone number\nr = requests.get(\n    f\"https://graph.facebook.com/v21.0/{PHONE_ID}\",\n    headers={\"Authorization\": f\"Bearer {TOKEN}\"}\n)\nif r.status_code == 200:\n    print(f\"✅ Phone Number Accessible\")\n    print(f\"   Display: {r.json().get('display_phone_number')}\")\n    print(f\"   Quality Rating: {r.json().get('quality_rating')}\")\nelse:\n    print(f\"❌ Phone Error: {r.json().get('error', {}).get('message')}\")"
      },
      {
        "title": "Quick Debug Checklist",
        "body": "When a message fails, check in this order:\n\nToken valid? — curl \"https://graph.facebook.com/debug_token?input_token=TOKEN&access_token=APP_ID|APP_SECRET\"\nPhone number format? — E.164, no +, no spaces: \"14155551234\"\nNumber registered in WABA? — check WhatsApp Manager → Phone Numbers\nNumber registered with Cloud API? — run registration call if error 133010\n24h window? — if > 24h since last user reply, send a template instead\nTemplate approved? — WhatsApp Manager → Message Templates → check status and rejection reason\nWebhook subscribed? — verify WABA → App subscription via GET /WABA_ID/subscribed_apps\nRate limited? — check Quality Rating in WhatsApp Manager; implement backoff + queue"
      }
    ],
    "body": "WhatsApp Messaging via Meta Cloud API\nOverview\n\nThe 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.\n\nKey rules:\n\nYour 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.\nBusiness-initiated messages outside a 24h reply window must use a pre-approved template\nPhone numbers must be registered in your WABA before sending\nAlways use a System User token — user tokens expire in 24 hours\n\nConversation flow:\n\nApp → user:  MUST be a template (always, for first contact)\nUser → app:  reply opens a 24-hour free-form window\nApp → user:  free-form text allowed within that 24h window\n  [24h passes with no user reply]\nApp → user:  MUST use a template again to re-engage\n\nSetup Checklist\nCreate Meta Developer App — developers.facebook.com → Create App → Business type\nAdd WhatsApp product to the app (gives temp test number + 5 test recipient slots)\nCreate a permanent System User token:\nMeta Business Manager → Settings → System Users → Create Admin user\nAssign permissions: whatsapp_business_messaging + whatsapp_business_management\nGenerate token — this never expires\nRegister real phone number — number cannot already be active on personal/business WhatsApp\nSet up webhook — needs public HTTPS URL (trusted CA cert, no self-signed), must respond in < 10s\nSending Messages\nText Message (Node.js)\n// npm install axios\nconst axios = require('axios');\n\nasync function sendMessage(phoneNumber, text) {\n    // phoneNumber: E.164 without +, e.g. \"14155551234\"\n    const res = await axios.post(\n        `https://graph.facebook.com/v21.0/${process.env.WA_PHONE_NUMBER_ID}/messages`,\n        {\n            messaging_product: 'whatsapp',\n            recipient_type: 'individual',\n            to: phoneNumber,\n            type: 'text',\n            text: { preview_url: false, body: text }\n        },\n        { headers: { Authorization: `Bearer ${process.env.WA_ACCESS_TOKEN}` } }\n    );\n    return res.data;\n}\n\nText Message (Python)\n# pip install requests\nimport requests, os\n\ndef send_message(phone: str, text: str) -> dict:\n    r = requests.post(\n        f\"https://graph.facebook.com/v21.0/{os.environ['WA_PHONE_NUMBER_ID']}/messages\",\n        headers={\"Authorization\": f\"Bearer {os.environ['WA_ACCESS_TOKEN']}\"},\n        json={\n            \"messaging_product\": \"whatsapp\",\n            \"recipient_type\": \"individual\",\n            \"to\": phone,        # E.164 without +\n            \"type\": \"text\",\n            \"text\": {\"preview_url\": False, \"body\": text}\n        }\n    )\n    r.raise_for_status()\n    return r.json()\n\nQuick test via curl\ncurl -X POST \"https://graph.facebook.com/v21.0/YOUR_PHONE_ID/messages\" \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"messaging_product\":\"whatsapp\",\"to\":\"14155551234\",\"type\":\"text\",\"text\":{\"body\":\"Hello\"}}'\n\nTemplate Message (required when > 24h since last user reply)\nconst payload = {\n    messaging_product: 'whatsapp',\n    to: phoneNumber,\n    type: 'template',\n    template: {\n        name: 'hello_world',           // your approved template name\n        language: { code: 'en_US' },\n        components: [{\n            type: 'body',\n            parameters: [\n                { type: 'text', text: 'John' },        // fills {{1}}\n                { type: 'text', text: 'Order #4521' }  // fills {{2}}\n            ]\n        }]\n    }\n};\n\nMedia Messages (Image, Document, Audio, Video)\n\nImage:\n\nconst payload = {\n    messaging_product: 'whatsapp',\n    to: phoneNumber,\n    type: 'image',\n    image: {\n        link: 'https://your-domain.com/image.jpg'  // must be publicly accessible HTTPS\n    }\n};\n\n\nDocument:\n\nconst payload = {\n    messaging_product: 'whatsapp',\n    to: phoneNumber,\n    type: 'document',\n    document: {\n        link: 'https://your-domain.com/file.pdf',\n        caption: 'Invoice'  // optional\n    }\n};\n\n\nAudio:\n\nconst payload = {\n    messaging_product: 'whatsapp',\n    to: phoneNumber,\n    type: 'audio',\n    audio: {\n        link: 'https://your-domain.com/audio.mp3'\n    }\n};\n\n\nVideo:\n\nconst payload = {\n    messaging_product: 'whatsapp',\n    to: phoneNumber,\n    type: 'video',\n    video: {\n        link: 'https://your-domain.com/video.mp4',\n        caption: 'Demo video'  // optional\n    }\n};\n\n\nImportant constraints:\n\nAll media URLs must be publicly accessible HTTPS (http:// fails)\nMax file sizes: Image 16MB, Document 100MB, Audio 16MB, Video 16MB\nSupported formats: Images (JPEG, PNG), Documents (PDF), Audio (AAC, MP3, OGG, WAV), Video (MP4, 3GPP)\nMedia must not require authentication\nURLs cannot use shorteners (bit.ly, tinyurl, etc.)\nWebhook Handler (Express) — Correct Async Pattern\n// GET — Meta calls this to verify your endpoint\napp.get('/webhook', (req, res) => {\n    const { 'hub.mode': mode, 'hub.verify_token': token, 'hub.challenge': challenge } = req.query;\n    if (mode === 'subscribe' && token === process.env.VERIFY_TOKEN)\n        return res.status(200).send(challenge);  // raw string only — NOT JSON\n    res.sendStatus(403);\n});\n\n// POST — CRITICAL: return 200 IMMEDIATELY, process async\napp.post('/webhook', express.json(), (req, res) => {\n    // Return 200 immediately so Meta doesn't retry\n    res.sendStatus(200);\n\n    // Process webhook payload asynchronously (don't block)\n    setImmediate(() => {\n        processWebhookAsync(req.body).catch(err => {\n            logger.error(`Webhook processing failed: ${err.message}`);\n        });\n    });\n});\n\nasync function processWebhookAsync(body) {\n    body.entry?.forEach(entry =>\n        entry.changes?.forEach(change => {\n            const value = change.value;\n\n            // Incoming messages\n            if (value.messages) {\n                value.messages.forEach(msg => {\n                    console.log(`Message from ${msg.from}: ${msg.text?.body}`);\n                    handleMessage(msg);\n                });\n            }\n\n            // Delivery status\n            if (value.statuses) {\n                value.statuses.forEach(status => {\n                    console.log(`Message ${status.id} status: ${status.status}`);\n                    handleDeliveryStatus(status);\n                });\n            }\n        })\n    );\n}\n\nMessage Status & Delivery Tracking\nWebhook Payload: Delivery Status\n\nMeta sends status updates via webhook when a message is delivered, read, or fails:\n\n{\n  \"object\": \"whatsapp_business_account\",\n  \"entry\": [{\n    \"changes\": [{\n      \"value\": {\n        \"statuses\": [{\n          \"id\": \"wamid.xxx\",           // Message ID from your send response\n          \"status\": \"delivered\",       // \"sent\" | \"delivered\" | \"read\" | \"failed\"\n          \"timestamp\": \"1675262308\",\n          \"recipient_id\": \"14155551234\",\n          \"type\": \"message\"\n        }]\n      }\n    }]\n  }]\n}\n\n\nStatus values:\n\nsent — Message reached Meta servers\ndelivered — Message delivered to user's device\nread — User opened the message\nfailed — Delivery failed (permanent)\nTracking Message Delivery\n// When you send, store the message ID\nconst sendResult = await sendMessage(phone, text);\nconst messageId = sendResult.messages[0].id;\n\n// Log it for webhook tracking\ndb.messages.insert({\n    message_id: messageId,\n    recipient: phone,\n    sent_at: Date.now(),\n    status: 'sent',\n    body: text\n});\n\n// When webhook arrives with status update, match by message_id\nfunction handleDeliveryStatus(statusUpdate) {\n    const { id, status, recipient_id } = statusUpdate;\n\n    // Update your database\n    db.messages.updateOne(\n        { message_id: id },\n        { status: status, updated_at: Date.now() }\n    );\n\n    // Handle delivery failures\n    if (status === 'failed') {\n        logger.error(`Message ${id} failed to deliver to ${recipient_id}`);\n        // Retry logic here\n    }\n}\n\nWABA Quality Rating & Account Health\nWhat is Quality Rating?\n\nYour WhatsApp Business Account (WABA) has a quality rating that affects your sending ability:\n\nRating\tImpact\tRecovery\nGREEN\tFull functionality, no restrictions\tMaintain this (stay green)\nYELLOW\tSlight rate limit reduction, monitor closely\tImprove within 7 days or drops to RED\nRED\tSevere restrictions, may lose messaging access\tContact Meta Support\nHow to Check Quality Rating\ncurl \"https://graph.facebook.com/v21.0/PHONE_NUMBER_ID?fields=quality_rating\" \\\n  -H \"Authorization: Bearer YOUR_TOKEN\"\n\n# Response: { \"quality_rating\": \"GREEN\" }\n\nWhat Causes Quality Rating to Drop?\nHigh bounce rate — sending to invalid/inactive numbers\nSpam reports — users marking your messages as spam\nHigh failure rate — messages consistently failing to deliver\nUser blocks — users blocking your number after messages\nPolicy violations — sending prohibited content\nHow to Improve Quality Rating\nValidate phone numbers before sending — use the WhatsApp contacts check (error 131026)\nOnly message opted-in users — don't send unsolicited messages\nKeep template content transactional — avoid marketing spam\nMonitor quality metrics — check rating regularly via API\nRespect user preferences — remove users who opt out\nDon't retry failed numbers aggressively — wait before retrying same number\nGroup Messages\n\nNote: WhatsApp Business API does NOT support group messaging directly. You can only send to individual recipients (1:1 conversations).\n\nIf you need group functionality:\n\nUsers must add your business number to a group manually\nMessages sent to the group are treated as individual 1:1 messages\nYou cannot initiate group conversations programmatically\nAPI Versioning Strategy\n\nAll examples use v21.0 (current as of February 2026). Meta deprecates API versions annually.\n\nChecking Your Current Version\n# List all available versions\ncurl \"https://graph.facebook.com/versions\" \\\n  -H \"Authorization: Bearer YOUR_TOKEN\"\n\n# Current version recommendations\n# - v21.0 (current, recommended)\n# - v20.0 (previous, will deprecate in 6 months)\n\nVersion Update Strategy\n// Store version in config, not hardcoded\nconst API_VERSION = process.env.WHATSAPP_API_VERSION || 'v21.0';\n\nconst url = `https://graph.facebook.com/${API_VERSION}/${PHONE_NUMBER_ID}/messages`;\n\n// When Meta deprecates a version, update .env:\n// WHATSAPP_API_VERSION=v22.0\n\nWhat Changes Between Versions?\nNew message types or features added\nDeprecated fields removed\nError codes may change slightly\nResponse payload structure may change\n\nAlways test before upgrading — make requests against the new version in your dev environment first.\n\nMessage Constraints & Limits\nText Message Limits\nConstraint\tLimit\nText body max length\t4,096 characters\nLink preview\tEnabled by default, disable with preview_url: false\nCarriage returns / newlines\tSupported (use \\n)\nInteractive Message Limits\nType\tItems\tCharacter Limit\nButton\t1-3\tTitle: 20 chars\nList\t1-10\tTitle: 24 chars per row\nMedia URL Requirements\nMust be HTTPS only (http:// rejected)\nMust be publicly accessible (no authentication required)\nMust NOT use shorteners (bit.ly, tinyurl rejected)\nMust have correct Content-Type header\nMax file sizes:\nImage: 16 MB\nAudio: 16 MB\nVideo: 16 MB\nDocument: 100 MB\nRate Limits (Per WABA)\nLimit\tValue\nDefault throughput\t80 messages/second\nBurst capacity\t1,000 messages/second (request increase)\nRequests per minute\t60 API calls/minute\nTemplate Approval Process\nTemplate Submission Workflow\nCreate template in Meta Business Manager\n        ↓\nSubmit for review (human review by Meta)\n        ↓\nStatus: PENDING (24-72 hours typical)\n        ↓\nStatus: APPROVED (can now use in messages)\n    OR\nStatus: REJECTED (reason provided in dashboard)\n\nHow Long Does Approval Take?\nTypical: 24-72 hours\nPeak times (weekends, holidays): up to 7 days\nFast-track: Available for high-volume WABAs (request in Meta Support)\nCommon Approval Issues\nIssue\tWhy Rejected\nVariable format\tMust use {{1}}, {{2}} format\nTemplate starts/ends with variable\tMust have text before first variable\nURL shorteners\tUse full domain URLs only\nPlaceholder quality\tPlaceholder values must be realistic examples\nSensitive data request\tNever ask for SSN, card numbers, passwords\nUnclear purpose\tPurpose field must clearly state intent\nWarm language in utility\tUse formal wording; warmth triggers \"marketing\" category (costs more)\nDuplicate template\tName/wording too similar to existing template\n\nCheck rejection reason in Meta Business Manager → Business Support → Rejected Template Messages.\n\nCommon Setup Mistakes\n1. Phone Number Already on Personal WhatsApp\n\nSymptom: Registration fails with error 133010, status stays PENDING\n\nCause: Phone number is already active on a personal WhatsApp account\n\nFix:\n\n1. Remove phone from personal WhatsApp (go to Settings → Devices → Remove phone)\n2. Wait 24 hours\n3. Re-run registration API call\n\n2. Mixing Phone Number ID with WABA ID\n\nSymptom: API returns \"Invalid parameters\" for phone operations\n\nHow to tell them apart:\n\nPHONE_NUMBER_ID: 120######## (11-12 digits, starts with 120)\nWABA_ID: ######### (9-10 digits, higher number)\n\n# Correct endpoint\nPOST /v21.0/PHONE_NUMBER_ID/messages  ✅\n\n# Wrong endpoint\nPOST /v21.0/WABA_ID/messages  ❌\n\n3. Token Validation Passes but Scopes Missing\n\nSymptom: Token debug shows valid, but messaging fails with error 3/10\n\n# Token is \"valid\" but missing scopes\ncurl \"https://graph.facebook.com/debug_token?input_token=TOKEN&access_token=TOKEN\"\n# Response: { \"is_valid\": true, \"scopes\": [\"manage_pages\"] }  ← NO whatsapp_business_messaging\n\n# Fix: Regenerate System User token with correct permissions\n\n4. Webhook Returns JSON Instead of Raw Challenge\n\nSymptom: Webhook verification fails silently in Meta dashboard\n\nWrong:\n\nreturn res.json({ challenge });  // ❌ returns JSON\n\n\nCorrect:\n\nreturn res.status(200).send(challenge);  // ✅ returns raw string\n\n5. WABA Not Subscribed to App\n\nSymptom: Webhooks never arrive (silent failure since 2025 Meta UI change)\n\nFix:\n\ncurl -X POST \\\n  \"https://graph.facebook.com/v21.0/WABA_ID/subscribed_apps\" \\\n  -H \"Authorization: Bearer YOUR_SYSTEM_USER_TOKEN\"\n\n6. Sending Template Too Early in Approval Process\n\nSymptom: Error 132001 \"Template Unavailable\"\n\nCause: Template still in PENDING status, not yet APPROVED\n\nFix: Check status in Meta Business Manager → Message Templates → wait for APPROVED status\n\n7. Phone Number Not in WABA\n\nSymptom: Error 131009 when trying to send\n\nHow to verify:\n\n# Check which phone numbers are in your WABA\ncurl \"https://graph.facebook.com/v21.0/WABA_ID/phone_numbers?fields=id,display_phone_number\" \\\n  -H \"Authorization: Bearer YOUR_TOKEN\"\n\n8. Sending Free-Form Text as First Message\n\nSymptom: API returns 200, message ID issued, but user never receives it\n\nRoot cause: Only templates allowed as first message to any number\n\nFix: Always use a template for first contact\n\nPhone Number Status Reference\n\nCheck the status field via API. Each status blocks different operations:\n\nStatus\tMeaning\tAction\nPENDING\tNumber is registered but not verified\tSet up 2FA (either manual or API), then run register call\nREGISTERED\tNumber is verified and ready\tCheck code_verification_status — should be VERIFIED\nFLAGGED\tAccount or number under review for policy violation\tContact Meta Support\nBANNED\tNumber permanently disabled\tContact Meta Support\nError Code Reference\nCode\tName\tCause\tFix\n190\tToken Expired\tUser token (24h lifetime) used in production\tSwitch to System User token; debug at developers.facebook.com/tools/debug/accesstoken\n3 / 10\tPermission Denied\tToken missing required scopes\tRegenerate System User token with whatsapp_business_messaging + whatsapp_business_management\n100\tInvalid Parameter\tMisspelled field or wrong value\tCheck request body against API docs; verify phone number format (E.164, no +)\n130429\tRate Limit (MPS)\tExceeded 80 messages/sec default\tAdd send queue + exponential backoff (see below)\n131047\t24h Window Expired\t> 24h since customer last replied\tReplace free-form text with a pre-approved template message\n131026\tUndeliverable\tRecipient blocked you, no WhatsApp, or outdated app\tVerify recipient number; confirm they have WhatsApp installed and accepted Meta terms\n131048\tSpam Rate Limit\tMessages flagged as spam\tCheck Quality Rating in WhatsApp Manager; review message content and opt-in practices\n131056\tPair Rate Limit\tToo many messages to same recipient too fast\tWait before retrying the same number\n131009\tInvalid Parameter Value\tPhone number not in WABA, or wrong parameter\tVerify number is registered in your WABA under Phone Numbers\n131021\tSame Sender/Recipient\tfrom and to are the same number\tUse a different recipient\n131031\tAccount Locked\tPolicy violation or wrong 2-step PIN\tContact Meta Support\n132001\tTemplate Unavailable\tWrong template name, wrong language code, or not yet approved\tCheck WhatsApp Manager → Message Templates for exact name, language, and status\n133010\tPhone Not Registered\tSender number not registered in Cloud API\tRun the registration API call (see below)\n368\tPolicy Violation\tAccount restricted\tContact Meta Support\n1 / 2\tAPI Service Error\tMeta outage or server error\tCheck metastatus.com; retry with exponential backoff\nFix: Token Problems (Error 190, 3, 10)\n\nDiagnose first:\n\ncurl \"https://graph.facebook.com/debug_token?input_token=YOUR_TOKEN&access_token=YOUR_APP_ID|YOUR_APP_SECRET\"\n\n\nCheck is_valid, expires_at (0 = never expires), and scopes in the response.\n\nFix — create a non-expiring System User token:\n\nMeta Business Manager → Settings → System Users\nCreate Admin system user\nAdd whatsapp_business_messaging + whatsapp_business_management permissions\nGenerate token — never set an expiry\nFix: Phone Number Not Registered (Error 133010)\n\nStep 1 — check phone number status:\n\ncurl \"https://graph.facebook.com/v21.0/PHONE_NUMBER_ID?fields=verified_name,code_verification_status,quality_rating,status\" \\\n  -H \"Authorization: Bearer YOUR_TOKEN\"\n\n\nIf status is PENDING, the number is waiting for verification. Continue below.\n\nStep 2 — set up Two-Step Verification (choose ONE method):\n\nOption A: Manual setup (easy)\n\nMeta Business Manager → WhatsApp Settings → Phone Numbers → Your Number\nClick \"Two-Step Verification\" → set a PIN\n\nOption B: API setup (easier for automation)\n\n# Set 2FA PIN via API\ncurl -X POST \\\n  \"https://graph.facebook.com/v21.0/PHONE_NUMBER_ID/two_step_verification\" \\\n  -H \"Authorization: Bearer YOUR_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"pin\": \"123456\"}'  # any 6-digit PIN you choose\n\n\nStep 3 — register the number with the PIN:\n\ncurl -X POST \\\n  \"https://graph.facebook.com/v21.0/PHONE_NUMBER_ID/register\" \\\n  -H \"Authorization: Bearer YOUR_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"messaging_product\": \"whatsapp\", \"pin\": \"123456\"}'  # same PIN from step 2\n\n\nStep 4 — wait 5 minutes, then verify registration:\n\ncurl \"https://graph.facebook.com/v21.0/PHONE_NUMBER_ID?fields=verified_name,code_verification_status,quality_rating,status\" \\\n  -H \"Authorization: Bearer YOUR_TOKEN\"\n\n\nLook for status: REGISTERED and code_verification_status: VERIFIED.\n\nFix: Webhook Not Verifying\n\nDiagnose in this order:\n\nNot returning raw challenge — endpoint must return the hub.challenge string value only, not JSON\nToken mismatch — hub.verify_token Meta sends must match exactly what you set in the dashboard (case-sensitive)\nSSL issue — Meta requires a valid cert from a trusted CA; self-signed certs are rejected\nTimeout — your server must respond within 10 seconds\nWABA not subscribed to App — common silent failure since 2025 Meta UI change:\n# Subscribe your WABA to your App (run once)\ncurl -X POST \\\n  \"https://graph.facebook.com/v21.0/WABA_ID/subscribed_apps\" \\\n  -H \"Authorization: Bearer YOUR_SYSTEM_USER_TOKEN\"\n\n# Verify the subscription exists\ncurl \"https://graph.facebook.com/v21.0/WABA_ID/subscribed_apps\" \\\n  -H \"Authorization: Bearer YOUR_SYSTEM_USER_TOKEN\"\n\n\nLocal development — expose localhost with a tunnel:\n\n# Using ngrok\nngrok http 3000\n# Use the https:// URL ngrok provides as your webhook callback URL in Meta\n\nFix: Rate Limiting (Error 130429)\n\nDefault limit is 80 messages per second. Fix with a queue and exponential backoff:\n\n// npm install limiter\nconst { RateLimiter } = require('limiter');\nconst limiter = new RateLimiter({ tokensPerInterval: 70, interval: 'second' });\n\nasync function sendWithRetry(phone, message, attempt = 0) {\n    await limiter.removeTokens(1);\n    try {\n        return await sendMessage(phone, message);\n    } catch (err) {\n        const code = err.response?.data?.error?.code;\n        if (code === 130429 && attempt < 5) {\n            const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s, 8s, 16s\n            await new Promise(r => setTimeout(r, delay));\n            return sendWithRetry(phone, message, attempt + 1);\n        }\n        throw err;\n    }\n}\n\n\nTo increase throughput beyond 80 MPS, apply in Meta Business Manager → WhatsApp → Phone Numbers → Request Increased Messaging Limit.\n\nFix: 24-Hour Window (Error 131047)\n\nYou cannot send free-form text to a user more than 24 hours after their last message. You must use a template.\n\n// Instead of free-form text, send an approved template\nawait sendTemplate(phoneNumber, 'order_update', 'en_US', [\n    { type: 'text', text: 'John' },\n    { type: 'text', text: '#4521' }\n]);\n\n\nCreate and submit templates at: Meta Business Manager → WhatsApp → Message Templates.\n\nFix: Template Rejected (Error 132001 or rejection in WhatsApp Manager)\nRejection Reason\tFix\nVariable format wrong\tUse {{1}}, {{2}} — double curly braces, sequential integers only\nTemplate starts/ends with variable\tAdd plain text before {{1}} and after the last variable\nVariables not sequential\tMust be {{1}}, {{2}} — no gaps allowed\nURL shorteners used\tUse full, unshortened URLs to your own domain\nLanguage code mismatch\tMatch language.code to the actual content language, e.g. en_US, pt_BR\nWarm language in utility template\tUse formal transactional wording; warm language causes auto-reclassification to marketing category\nSensitive data\tNever request SSNs, full card numbers, or passwords\nDuplicate of existing template\tChange the wording — even minor variation is required\nPurpose unclear\tEach variable must have a descriptive example value in the template submission\n\nWhere to find rejection reason: Meta Business Manager → Business Support Home → Your WhatsApp Account → Rejected Template Messages → view policy issue.\n\nFix: Message Undeliverable (Error 131026)\n\nRun this to check if a number has WhatsApp before sending:\n\ncurl \"https://graph.facebook.com/v21.0/PHONE_NUMBER_ID/contacts\" \\\n  -H \"Authorization: Bearer YOUR_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -X POST \\\n  -d '{\"messaging_product\": \"whatsapp\", \"contacts\": [\"+14155551234\"]}'\n# Response includes \"wa_id\" if the number has WhatsApp, empty if not\n\nDiagnose: \"My message sends but the user never receives it\" (first-contact trap)\n\nThis is the most common silent failure. The API returns success (messages[0].id) but the user receives nothing.\n\nRoot cause: You sent a free-form text message as the first outreach. Meta silently drops it.\n\nHow to tell: Check the message status webhook — the message will show failed with error 131047 or show sent but never delivered.\n\nRule: The very first message your app sends to any number must be a template. No exceptions.\n\n❌ Wrong — app sends free-form text first:\n   POST /messages → type: \"text\", body: \"Hello John, your order is ready\"\n   → API may return 200 but message is silently dropped or returns 131047\n\n✅ Correct — app sends template first:\n   POST /messages → type: \"template\", name: \"order_ready\"\n   → User receives the message and can reply\n   → After user replies, free-form text is allowed for 24h\n\n\nFix: 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.\n\nPractical Patterns from Production\nPhone Number Validation Before Sending\n\nAlways validate phone number format and WhatsApp registration before sending:\n\ndef is_valid_phone_format(phone_digits: str) -> bool:\n    \"\"\"\n    E.164 format validation:\n      - 7-15 digits (not including +)\n      - Not all same digit (e.g., 0000000 is invalid)\n    \"\"\"\n    if not phone_digits or len(phone_digits) < 7 or len(phone_digits) > 15:\n        return False\n    if len(set(phone_digits)) == 1:  # all same digit\n        return False\n    return True\n\ndef is_registered_on_whatsapp(phone_digits: str, token: str, phone_id: str) -> bool:\n    \"\"\"\n    Check if number is a registered WhatsApp user.\n    Returns False only on definitive error 131026 (not on WhatsApp).\n    Returns True if successful, uncertain (auth error, etc.), or timeout.\n    \"\"\"\n    headers = {\n        \"Authorization\": f\"Bearer {token}\",\n        \"Content-Type\": \"application/json\"\n    }\n    data = {\n        \"messaging_product\": \"whatsapp\",\n        \"to\": phone_digits,\n        \"type\": \"text\",\n        \"text\": {\"body\": \"_\"}  # minimal text to check\n    }\n    try:\n        r = requests.post(\n            f\"https://graph.facebook.com/v21.0/{phone_id}/messages\",\n            headers=headers,\n            json=data,\n            timeout=10\n        )\n        if r.status_code == 200:\n            return True  # number is valid\n\n        error_code = r.json().get(\"error\", {}).get(\"code\")\n        if error_code == 131026:\n            return False  # NOT on WhatsApp\n\n        return True  # other errors — don't block\n    except:\n        return True  # network error — don't block\n\nStructured Error Response Handling\ndef extract_error_code(response_json: dict) -> int:\n    \"\"\"Extract error code from Meta API response.\"\"\"\n    return (\n        response_json.get(\"error\", {}).get(\"code\")\n        or response_json.get(\"error\", {}).get(\"error_subcode\")\n    )\n\ndef send_with_error_handling(phone_id: str, recipient: str, message_body: str, token: str):\n    \"\"\"Send message and extract detailed error info.\"\"\"\n    url = f\"https://graph.facebook.com/v21.0/{phone_id}/messages\"\n    headers = {\n        \"Authorization\": f\"Bearer {token}\",\n        \"Content-Type\": \"application/json\"\n    }\n    data = {\n        \"messaging_product\": \"whatsapp\",\n        \"to\": recipient,\n        \"type\": \"text\",\n        \"text\": {\"body\": message_body}\n    }\n\n    try:\n        r = requests.post(url, headers=headers, json=data)\n        r.raise_for_status()\n        return {\"success\": True, \"message_id\": r.json().get(\"messages\")[0].get(\"id\")}\n    except requests.HTTPError as e:\n        error_code = extract_error_code(e.response.json())\n        error_msg = e.response.json().get(\"error\", {}).get(\"message\")\n        return {\n            \"success\": False,\n            \"error_code\": error_code,\n            \"error_message\": error_msg,\n            \"response_text\": e.response.text\n        }\n\nInteractive Messages (Smart Buttons vs List)\ndef send_interactive_message(phone_id: str, recipient: str, text: str, options: list, token: str):\n    \"\"\"\n    Intelligently sends:\n      - Buttons (up to 3 options)\n      - List Menu (4-10 options)\n\n    Each option: {\"id\": \"unique_id\", \"title\": \"Text (max 20 chars)\"}\n    \"\"\"\n    url = f\"https://graph.facebook.com/v21.0/{phone_id}/messages\"\n    headers = {\n        \"Authorization\": f\"Bearer {token}\",\n        \"Content-Type\": \"application/json\"\n    }\n\n    if len(options) <= 3:\n        # Send as buttons\n        button_data = {\n            \"messaging_product\": \"whatsapp\",\n            \"to\": recipient,\n            \"type\": \"interactive\",\n            \"interactive\": {\n                \"type\": \"button\",\n                \"body\": {\"text\": text},\n                \"action\": {\n                    \"buttons\": [\n                        {\n                            \"type\": \"reply\",\n                            \"reply\": {\"id\": opt[\"id\"], \"title\": opt[\"title\"][:20]}\n                        }\n                        for opt in options\n                    ]\n                }\n            }\n        }\n        return requests.post(url, headers=headers, json=button_data)\n    else:\n        # Send as list menu\n        list_data = {\n            \"messaging_product\": \"whatsapp\",\n            \"to\": recipient,\n            \"type\": \"interactive\",\n            \"interactive\": {\n                \"type\": \"list\",\n                \"body\": {\"text\": text},\n                \"action\": {\n                    \"button\": \"See options\",\n                    \"sections\": [{\n                        \"title\": \"Available options\",\n                        \"rows\": [\n                            {\n                                \"id\": opt[\"id\"],\n                                \"title\": opt[\"title\"][:24]\n                            }\n                            for opt in options\n                        ]\n                    }]\n                }\n            }\n        }\n        return requests.post(url, headers=headers, json=list_data)\n\nTesting & Debugging Helper Scripts\n\nToken & Phone Verification Script:\n\n# Check if token is valid and phone number is accessible\nimport requests, os\nfrom dotenv import load_dotenv\nload_dotenv()\n\nTOKEN = os.getenv('WHATSAPP_TOKEN')\nPHONE_ID = os.getenv('PHONE_NUMBER_ID')\n\n# Debug token\nr = requests.get(f\"https://graph.facebook.com/debug_token?input_token={TOKEN}&access_token={TOKEN}\")\ndata = r.json()\nif 'error' in data:\n    print(f\"❌ Token Error: {data['error']['message']}\")\nelse:\n    print(f\"✅ Token Valid\")\n    print(f\"   Expires: {data['data'].get('expires_at')} (0=never)\")\n    print(f\"   Scopes: {data['data'].get('scopes')}\")\n\n# Check phone number\nr = requests.get(\n    f\"https://graph.facebook.com/v21.0/{PHONE_ID}\",\n    headers={\"Authorization\": f\"Bearer {TOKEN}\"}\n)\nif r.status_code == 200:\n    print(f\"✅ Phone Number Accessible\")\n    print(f\"   Display: {r.json().get('display_phone_number')}\")\n    print(f\"   Quality Rating: {r.json().get('quality_rating')}\")\nelse:\n    print(f\"❌ Phone Error: {r.json().get('error', {}).get('message')}\")\n\nQuick Debug Checklist\n\nWhen a message fails, check in this order:\n\nToken valid? — curl \"https://graph.facebook.com/debug_token?input_token=TOKEN&access_token=APP_ID|APP_SECRET\"\nPhone number format? — E.164, no +, no spaces: \"14155551234\"\nNumber registered in WABA? — check WhatsApp Manager → Phone Numbers\nNumber registered with Cloud API? — run registration call if error 133010\n24h window? — if > 24h since last user reply, send a template instead\nTemplate approved? — WhatsApp Manager → Message Templates → check status and rejection reason\nWebhook subscribed? — verify WABA → App subscription via GET /WABA_ID/subscribed_apps\nRate limited? — check Quality Rating in WhatsApp Manager; implement backoff + queue"
  },
  "trust": {
    "sourceLabel": "tencent",
    "provenanceUrl": "https://clawhub.ai/RomanBaz/whatsapp-cloud-api-reference",
    "publisherUrl": "https://clawhub.ai/RomanBaz/whatsapp-cloud-api-reference",
    "owner": "RomanBaz",
    "version": "1.0.0",
    "license": null,
    "verificationStatus": "Indexed source record"
  },
  "links": {
    "detailUrl": "https://openagent3.xyz/skills/whatsapp-cloud-api-reference",
    "downloadUrl": "https://openagent3.xyz/downloads/whatsapp-cloud-api-reference",
    "agentUrl": "https://openagent3.xyz/skills/whatsapp-cloud-api-reference/agent",
    "manifestUrl": "https://openagent3.xyz/skills/whatsapp-cloud-api-reference/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/whatsapp-cloud-api-reference/agent.md"
  }
}