<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Diego Moreira</title>
    <description>The latest articles on DEV Community by Diego Moreira (@diegomoreira).</description>
    <link>https://dev.to/diegomoreira</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4069857%2F5ce3e272-b056-4b01-9bee-fc886f194944.jpeg</url>
      <title>DEV Community: Diego Moreira</title>
      <link>https://dev.to/diegomoreira</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/diegomoreira"/>
    <language>en</language>
    <item>
      <title>Shipping WhatsApp Cloud API to production: the gotchas the docs don't tell you</title>
      <dc:creator>Diego Moreira</dc:creator>
      <pubDate>Sun, 09 Aug 2026 13:29:24 +0000</pubDate>
      <link>https://dev.to/diegomoreira/shipping-whatsapp-cloud-api-to-production-the-gotchas-the-docs-dont-tell-you-47e6</link>
      <guid>https://dev.to/diegomoreira/shipping-whatsapp-cloud-api-to-production-the-gotchas-the-docs-dont-tell-you-47e6</guid>
      <description>&lt;p&gt;I recently built a proactive-notification channel on top of the &lt;strong&gt;WhatsApp Cloud API&lt;/strong&gt; for a small business — the kind of thing that sends "your X is ready" or "there's an outage at location Y" straight to a customer's WhatsApp, triggered by a backend system rather than a human typing.&lt;/p&gt;

&lt;p&gt;On paper it's a few API calls. In practice I lost hours to failures that returned &lt;strong&gt;HTTP 200 and looked successful&lt;/strong&gt;, to a UI button that was simply broken, and to rules Meta enforces but doesn't surface until you trip over them.&lt;/p&gt;

&lt;p&gt;This is the write-up I wish I'd found. Every ID, token, and number below is a placeholder — swap in your own.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Cloud API vs. the WhatsApp Business &lt;em&gt;app&lt;/em&gt; — pick the right tool first
&lt;/h2&gt;

&lt;p&gt;If your goal is &lt;strong&gt;proactive&lt;/strong&gt; messaging — notifying a customer who has &lt;em&gt;not&lt;/em&gt; messaged you first — the WhatsApp Business app on a phone will not do it. It can only reply inside an open conversation window.&lt;/p&gt;

&lt;p&gt;Proactive, system-triggered messages require:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;strong&gt;Cloud API&lt;/strong&gt; (or on-premise API), and&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;pre-approved message template&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A useful mental model that saved me a lot of confusion:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The Cloud API is a &lt;strong&gt;pipe, not a faucet.&lt;/strong&gt; There is no screen where someone logs in and types a message. It only accepts calls from a program. To actually operate it you either build an integration or plug in a third-party platform. Out of the box, all you can do is send via &lt;code&gt;curl&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Decide this on day one. It changes your whole architecture.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The silent killer: &lt;code&gt;accepted&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; mean &lt;code&gt;delivered&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This one cost me the most time, so it goes first.&lt;/p&gt;

&lt;p&gt;My send call returned a clean success:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"messaging_product"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"whatsapp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"messages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wamid.XXXX"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;HTTP 200. A valid &lt;code&gt;wamid&lt;/code&gt;. And &lt;strong&gt;the message never arrived&lt;/strong&gt; — not on my phone, not on a second test phone. No error anywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; the Meta &lt;em&gt;app&lt;/em&gt; was still in &lt;strong&gt;Development / Unpublished&lt;/strong&gt; mode. In that mode Meta &lt;strong&gt;accepts the API call and returns success but does not deliver the message.&lt;/strong&gt; The failure is 100% silent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; publish the app.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;developers.facebook.com → your app → Publish
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only prerequisite is a &lt;strong&gt;Privacy Policy URL&lt;/strong&gt; under &lt;em&gt;App settings → Basic&lt;/em&gt;. It has to resolve to a real page with real content or validation fails. Once published, the exact same request delivered on the first try.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; &lt;code&gt;accepted&lt;/code&gt; ≠ &lt;code&gt;delivered&lt;/code&gt;. Without webhooks configured you have no way to tell the difference. &lt;strong&gt;If the API accepts your message but nothing arrives, check the app mode before any other hypothesis.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  3. When the Meta UI lies, go straight to the Graph API
&lt;/h2&gt;

&lt;p&gt;To activate a number you have to &lt;strong&gt;register&lt;/strong&gt; it. The dashboard has a "Register" button for this. In my case that button returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Registration failed. Please try again.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…every time, with no real cause. I burned hours ruling things out: the SIM had never had WhatsApp installed, the OTP was entered correctly, two-step verification was off, it wasn't a rate limit (same failure after 22h), and business verification isn't required to register.&lt;/p&gt;

&lt;p&gt;The button was simply &lt;strong&gt;bugged&lt;/strong&gt;. The identical operation via the Graph API worked on the first attempt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://graph.facebook.com/v21.0/&amp;lt;PHONE_NUMBER_ID&amp;gt;/register"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$WA_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"messaging_product":"whatsapp","pin":"&amp;lt;YOUR_6_DIGIT_PIN&amp;gt;"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response: &lt;code&gt;HTTP 200&lt;/code&gt;, &lt;code&gt;{"success": true}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; when Meta's UI throws a generic error, hit the Graph API directly. It either returns the &lt;em&gt;real&lt;/em&gt; error (far more actionable than "try again") or, as here, just works because the UI was the only broken part.&lt;/p&gt;

&lt;p&gt;The same applies to reading state. The developer dashboard often shows &lt;strong&gt;stale&lt;/strong&gt; status. The API is the source of truth:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"https://graph.facebook.com/v25.0/&amp;lt;PHONE_NUMBER_ID&amp;gt;?fields=status"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$WA_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# -&amp;gt; {"status":"CONNECTED","id":"&amp;lt;PHONE_NUMBER_ID&amp;gt;"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4. Template gotchas
&lt;/h2&gt;

&lt;p&gt;Templates are where a surprising number of rules hide.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Category = money.&lt;/strong&gt; Choose &lt;strong&gt;Utility&lt;/strong&gt;, not &lt;strong&gt;Marketing&lt;/strong&gt;, whenever the message is transactional (status, alert, confirmation). In some markets Marketing costs on the order of &lt;strong&gt;~12× more&lt;/strong&gt; per message. Miscategorizing is a silent budget leak.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;hello_world&lt;/code&gt; only sends from the &lt;em&gt;test&lt;/em&gt; number.&lt;/strong&gt; You cannot validate your production pipe with the sample template. From a real number you get:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  (#131058) Hello World templates can only be sent from the Public Test Numbers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plan for it: you can't do an end-to-end send from your real number until &lt;em&gt;your own&lt;/em&gt; template is approved. (A failed &lt;code&gt;hello_world&lt;/code&gt; attempt still proves token validity, sender acceptance, active billing, and API reachability — the infra is fine, you're just waiting on the template.)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep v1 dead simple.&lt;/strong&gt; No header, no footer, no buttons approves faster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Buttons don't render on WhatsApp Web.&lt;/strong&gt; A template with buttons can show &lt;code&gt;This message couldn't load&lt;/code&gt; on desktop. Always verify on a &lt;strong&gt;phone&lt;/strong&gt;, not Web.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Editing or resending a pending template restarts the review queue.&lt;/strong&gt; Submit once, then wait. Approval is usually well under 48h; past that, contact support instead of poking it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch the message validity period (TTL).&lt;/strong&gt; The form defaults to a short validity (as little as &lt;strong&gt;10 minutes&lt;/strong&gt;) when you don't set a custom one. For a time-sensitive alert that's fine; for something useful for hours, a phone that was briefly off will &lt;strong&gt;silently expire&lt;/strong&gt; the message. It's editable later — but review it before you operate at volume.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use UTF-8 for non-English languages (accents included) — WhatsApp is UTF-8 end to end.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Phone-number gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;E.164 only.&lt;/strong&gt; Country code + area code + number, &lt;strong&gt;digits only&lt;/strong&gt;. No leading &lt;code&gt;0&lt;/code&gt;, no carrier-selection prefix. &lt;code&gt;55&lt;/code&gt; + area + number for Brazil, and nothing else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Brazilian 9th digit gets normalized away.&lt;/strong&gt; WhatsApp strips the mobile &lt;code&gt;9&lt;/code&gt; in the &lt;code&gt;wa_id&lt;/code&gt; (e.g. &lt;code&gt;55 61 9XXXX-XXXX&lt;/code&gt; → &lt;code&gt;55 61 XXXX-XXXX&lt;/code&gt;). This is &lt;strong&gt;normal normalization, not a bug.&lt;/strong&gt; Don't "fix" it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You don't need the recipient in your contacts.&lt;/strong&gt; WhatsApp delivers from unknown business numbers normally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A number registered on the Cloud API is &lt;em&gt;consumed&lt;/em&gt; by the API.&lt;/strong&gt; It can only send/receive through the API — it can no longer be used in the regular WhatsApp or WhatsApp Business app. &lt;strong&gt;Installing the app on that SIM breaks the registration.&lt;/strong&gt; Pick a dedicated number and leave the app off it.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  6. Account setup that doesn't get you banned
&lt;/h2&gt;

&lt;p&gt;The single most expensive mistake I saw was structural, not technical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A brand-new Facebook account, created from scratch and &lt;em&gt;immediately&lt;/em&gt; used to spin up business assets, is a red flag to Meta.&lt;/strong&gt; That pattern — fresh identity, instant business, sometimes over a VPN — looks like a disposable/fraud account, and it can get &lt;strong&gt;permanently restricted with no appeal&lt;/strong&gt;, taking every asset attached to it down with it (portfolio, app, number registration).&lt;/p&gt;

&lt;p&gt;What works instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use an &lt;strong&gt;established, verified Business account&lt;/strong&gt; as the administrator. An admin account holding &lt;strong&gt;multiple client portfolios&lt;/strong&gt; is the normal agency/studio pattern — legitimate, expected use.&lt;/li&gt;
&lt;li&gt;Create a &lt;strong&gt;separate business portfolio per client&lt;/strong&gt;. This isolates &lt;strong&gt;risk&lt;/strong&gt; (a quality complaint on one channel won't drag down another) and &lt;strong&gt;billing&lt;/strong&gt; (each client's spend stays in its own place).&lt;/li&gt;
&lt;li&gt;Don't rush. Don't do it over a VPN. Don't reuse an email already tied to a flagged asset.&lt;/li&gt;
&lt;li&gt;Business verification is &lt;strong&gt;optional&lt;/strong&gt; to register a number and to deliver — but it raises your throughput tier (e.g. from 250 to 1,000+ business-initiated conversations per 24h), so do it before you scale.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reputation of the &lt;strong&gt;admin&lt;/strong&gt; account is what carries you. Protect it.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Tokens: temporary vs. permanent, and token ≠ app secret
&lt;/h2&gt;

&lt;p&gt;Two traps here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Temporary vs. System User token.&lt;/strong&gt; The token you generate in the dashboard's "Try it out" is short-lived (~24h) — fine for testing, useless for production. For a real integration create a &lt;strong&gt;System User&lt;/strong&gt; with a &lt;strong&gt;non-expiring&lt;/strong&gt; token, scoped to &lt;em&gt;only&lt;/em&gt; the assets it needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Business Settings → Users → System users → Add
→ assign the app + the WABA (Full access)
→ Generate token with:
   whatsapp_business_messaging
   whatsapp_business_management
   whatsapp_business_manage_events
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When generating, opt in to &lt;strong&gt;only&lt;/strong&gt; the WhatsApp account you're working on. If your admin account manages several clients' WABAs, do &lt;strong&gt;not&lt;/strong&gt; tick the others.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token ≠ App Secret.&lt;/strong&gt; These are different credentials for different jobs, and a webhook consumer usually needs &lt;strong&gt;both&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;strong&gt;token&lt;/strong&gt; (&lt;code&gt;EAA…&lt;/code&gt;) authenticates &lt;strong&gt;outbound&lt;/strong&gt; message sends;&lt;/li&gt;
&lt;li&gt;the &lt;strong&gt;App Secret&lt;/strong&gt; validates the &lt;code&gt;X-Hub-Signature-256&lt;/code&gt; on &lt;strong&gt;inbound&lt;/strong&gt; webhooks. A fail-closed webhook receiver won't even boot without it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hygiene:&lt;/strong&gt; keep tokens in an environment variable so they never land in a screenshot or shell history:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;WA_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'...'&lt;/span&gt;
curl ... &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$WA_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A non-expiring token is a permanent liability if leaked — a leak is valid forever until someone revokes it (&lt;em&gt;Business Settings → Users → System users → Revoke tokens&lt;/em&gt;). Don't transmit it over chat; if you must, delete the message from both ends once it's deployed.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Without webhooks, your channel is blind and deaf
&lt;/h2&gt;

&lt;p&gt;Sending is only half of it. Until you configure webhooks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you &lt;strong&gt;can't tell if the message was delivered&lt;/strong&gt; (you only get &lt;code&gt;accepted&lt;/code&gt;), and&lt;/li&gt;
&lt;li&gt;if the recipient &lt;strong&gt;replies&lt;/strong&gt;, that reply is &lt;strong&gt;lost&lt;/strong&gt; — nobody sees it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Configure it under &lt;em&gt;your app → Webhooks&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Callback URL&lt;/strong&gt;: your endpoint, already live and answering the &lt;code&gt;GET&lt;/code&gt; verification handshake with the agreed &lt;strong&gt;verify token&lt;/strong&gt; &lt;em&gt;before&lt;/em&gt; you register it — Meta rejects the callback otherwise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subscribe to &lt;code&gt;messages&lt;/code&gt;&lt;/strong&gt; — that field covers both inbound messages and delivery-status events.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Treat webhooks as part of v1, not a nice-to-have.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Billing: there is no spend cap
&lt;/h2&gt;

&lt;p&gt;Coming from Meta &lt;em&gt;ads&lt;/em&gt; (which have budget limits), I assumed the Cloud API did too. &lt;strong&gt;It does not.&lt;/strong&gt; Billing is pure usage-based; the only ceiling Meta enforces is the &lt;strong&gt;volume tier&lt;/strong&gt;, not spend.&lt;/p&gt;

&lt;p&gt;Your only real financial guardrail is the &lt;strong&gt;limit on the card&lt;/strong&gt; attached to the WABA. Set one. As a reference point, at roughly &lt;strong&gt;US$0.005/message&lt;/strong&gt; a modest monthly card limit covers thousands of notifications — the limit mostly protects you against a bug that loops and sends in a runaway.&lt;/p&gt;

&lt;p&gt;If your billing currency ends up as USD (in some countries the local currency isn't offered in this flow), remember the charge lands as an &lt;strong&gt;international purchase&lt;/strong&gt; — factor in FX spread and any local tax when you reconcile costs. Reconcile against the &lt;strong&gt;card statement in your local currency&lt;/strong&gt;, not the dollar figure Meta shows.&lt;/p&gt;




&lt;h2&gt;
  
  
  The condensed checklist
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ ] Cloud API (not the app) if you need proactive messaging
[ ] App PUBLISHED (Development mode accepts but never delivers)
[ ] Number registered — via Graph API if the UI button fails
[ ] Number is DEDICATED — never install the WhatsApp app on that SIM
[ ] Template = Utility (not Marketing), no buttons in v1, TTL reviewed
[ ] Established admin account + one portfolio per client
[ ] System User token (non-expiring), scoped to one WABA
[ ] App Secret wired for webhook signature validation
[ ] Webhooks subscribed to `messages` (delivery + replies)
[ ] Card spend limit set (Meta has no budget cap)
[ ] Business verification done before scaling past the base tier
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Why I wrote this
&lt;/h2&gt;

&lt;p&gt;Most of these aren't in the official docs — they're the kind of thing you only learn by shipping. If it saved you an afternoon, it did its job.&lt;/p&gt;

&lt;p&gt;I build mobile apps (React Native) and WhatsApp/Meta API integrations for small and mid-size businesses, with a background in health-sector software. If you're wiring up a WhatsApp channel and something's returning success but not working, that section 2 is almost always the answer.&lt;/p&gt;

</description>
      <category>whatsapp</category>
      <category>api</category>
      <category>meta</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
