Meta (Ads + WhatsApp)
Our growth stack. The meta-ads MCP server is by far the largest server in the
portfolio by tool count — prefer it over hand-rolled Graph calls for anything Claude
does at build time.
Environment
META_ACCESS_TOKEN= # SECRET — read
META_ADS_WRITE_TOKEN= # SECRET — write, kept separate deliberately
META_APP_SECRET= # SECRET
META_AD_ACCOUNT_ID= # act_XXXXXXXXX (the act_ prefix is required)
META_PAGE_ID=
META_PIXEL_ID=
META_API_VERSION= # v21.0 — PIN IT
META_GRAPH_VERSION=
META_WHATSAPP_TOKEN= # SECRET
META_WHATSAPP_PHONE_NUMBER_ID=
META_WHATSAPP_VERIFY_TOKEN= # SECRET — webhook handshake
Separate read and write tokens. Worth doing everywhere: the read token powers dashboards and can be scoped narrowly; the write token can create campaigns and spend money, and should be used from exactly one code path.
Pin META_API_VERSION. Meta deprecates versions on a ~2-year clock and changes
field availability between them without warning at the call site.
Hierarchy
Ad Account (act_…)
└── Campaign — objective, budget strategy
└── Ad Set — audience, placement, schedule, budget
└── Ad — creative + tracking
Create top-down; you cannot make an ad without an ad set. Every object starts in
PAUSED — always create paused, review, then activate. A misconfigured audience
that goes live immediately spends real money before anyone looks at it.
Insights
GET /v21.0/act_<ID>/insights
?fields=spend,impressions,clicks,ctr,cpc,actions
&level=ad
&time_range={"since":"2026-08-01","until":"2026-08-24"}
&time_increment=1
Notes that matter:
- Attribution windows change the numbers.
action_attribution_windowsdefaults differ from the Ads Manager UI, so an API number that "disagrees with the dashboard" is usually this. - Insights lag. Today's numbers are incomplete for up to 72 hours. Never chart today as final.
actionsis an array of typed objects, not a scalar. Purchases are the entry withaction_type: "purchase"(oromni_purchase— pick one and be consistent).
Catalogs
A product catalog feeds dynamic ads. The catalog is a separate object from the
ad account, with its own feeds, product sets and rules. Keep Supabase as the source of
truth and push a feed, rather than editing products in Meta directly — same principle
as Shopify inventory (../ekx-shopify/SKILL.md).
WhatsApp Business
await fetch(`https://graph.facebook.com/${V}/${process.env.META_WHATSAPP_PHONE_NUMBER_ID}/messages`, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.META_WHATSAPP_TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify({
messaging_product: "whatsapp",
to: phone, // E.164, no +, no spaces
type: "template",
template: { name: "order_ready", language: { code: "es" } },
}),
});
Three rules:
- Outside the 24-hour customer service window you can only send approved templates. Free-form text is rejected. Get templates approved before building the flow around them.
- Numbers are E.164 without
+. Colombian numbers:57+ 10 digits. - Webhook verification is a GET with
hub.verify_token— echohub.challengeback as plain text if it matchesMETA_WHATSAPP_VERIFY_TOKEN, or Meta never subscribes you.
shopper sells via WhatsApp with cash-on-delivery, so this is the primary commerce
channel there, not a notification sideline.
Gotchas
- Missing
act_prefix on the account id → confusing 400. - Creating unpaused spends money before review.
- Insights lag ≤72h.
- Attribution window mismatch vs the UI.
- Token expiry — long-lived tokens still expire (~60 days). Have a refresh path, or the dashboards die quietly.
- Unpinned API version.
- WhatsApp 24-hour window.