Meta & WhatsApp integration setup (Lead Ads + Cloud API)
How to configure the one central Taikan Meta app that powers both lead-ingestion channels:
- Meta Lead Ads — Facebook/Instagram lead forms → leads (Facebook Login for Business).
- WhatsApp Cloud — Click-to-WhatsApp ads → leads (Embedded Signup + Cloud API).
For what the pipeline does (flows, data model, code), see the feature doc
docs/features/leads-crm/lead-ingestion.md.
This runbook is the dashboard + env side: the Meta App Dashboard config, the
review/verification gates, and the env vars the API reads.
Status: WhatsApp is pending Meta Business Verification + App Review — the connect flow is built but a real number can’t go live until that clears. Lead Ads is live. Steps below that depend on review are marked ⏳ pending review.
The stable contract (what the code expects)
Everything the API needs is read from env (apps/api/src/lead-ingestion/meta-config.service.ts)
and these two webhook endpoints. One app, one app secret, both products — the
X-Hub-Signature-256 HMAC uses META_APP_SECRET for Lead Ads and WhatsApp.
| Endpoint | Object / field | Verify token | Handler |
|---|---|---|---|
GET/POST /webhooks/meta/leadgen | page / leadgen | META_VERIFY_TOKEN | meta-lead-webhook.controller.ts |
GET/POST /webhooks/whatsapp | whatsapp_business_account / messages | WHATSAPP_VERIFY_TOKEN (→ META_VERIFY_TOKEN) | whatsapp-webhook.controller.ts |
The GET is Meta’s subscribe handshake (echoes hub.challenge when the token matches);
the POST is the event delivery (HMAC-verified). Both must be publicly reachable over
HTTPS before Meta will let you save the webhook — for local dev, tunnel the API
(cloudflared tunnel --url http://localhost:3001 or ngrok) and use the tunnel URL.
1. The central Meta app
Meta App Dashboard → your Taikan app (one app serves every org).
- App ID / secret → env
META_APP_ID,META_APP_SECRET. (Settings → Basic.) - Add the two products / use cases:
- Facebook Login for Business (Lead Ads connect).
- WhatsApp (Cloud API).
- Business Verification (Settings → Basic → Verification) — required before either product leaves dev mode. Shared across both products; do it once.
2. Webhooks
App Dashboard → Webhooks (or each product’s Configuration → Webhooks).
| Field | Value |
|---|---|
| Lead Ads — Object | Page, subscribe field leadgen |
| Lead Ads — Callback URL | https://<api-host>/webhooks/meta/leadgen |
| Lead Ads — Verify token | the value you set as META_VERIFY_TOKEN |
| WhatsApp — Object | WhatsApp Business Account, subscribe field messages |
| WhatsApp — Callback URL | https://<api-host>/webhooks/whatsapp |
| WhatsApp — Verify token | WHATSAPP_VERIFY_TOKEN (or META_VERIFY_TOKEN if you didn’t set the WA-specific one) |
Pick any high-entropy strings for the verify tokens (openssl rand -hex 32); they only
have to match what the API echoes. Set the env vars first and redeploy — Meta calls
the GET immediately on save and the handshake fails if the API doesn’t know the token yet.
Per-account subscription (which Page / which WABA actually sends events) is done by the connect flow at runtime, not here:
subscribePageToLeadgen(Lead Ads) andsubscribeWabaToWebhooks(WhatsApp). This step only registers the app-level callback.
3. Lead Ads — Facebook Login for Business
The coach-facing connect is an OAuth redirect (…/integrations/meta/connect-url →
Facebook → …/integrations/meta/callback).
- Facebook Login for Business → Configurations → Create configuration.
- Type: System User access token (so the stored token can call the Graph API
server-to-server for
leadgenretrieval). - Assets/permissions the config requests (see §5).
- Copy the Configuration ID → env
META_LOGIN_CONFIG_ID.
- Type: System User access token (so the stored token can call the Graph API
server-to-server for
- Valid OAuth redirect URI (Login settings) = your
META_OAUTH_REDIRECT_URI, e.g.https://<api-host>/integrations/meta/callback. Must match env exactly. - The connect flow then lists the coach’s Pages and, on finalize, subscribes each chosen
Page to
leadgenand stores one activeintegration_connectionsrow per Page.
4. WhatsApp — Embedded Signup + Cloud API
The coach-facing connect is the Embedded Signup popup (FB JS SDK), not a redirect.
- WhatsApp → Embedded Signup → create/!configure a signup flow → copy its
Configuration ID → env
WHATSAPP_EMBEDDED_SIGNUP_CONFIG_ID. This is the only value the frontend popup needs (served via…/integrations/whatsapp/config). - Test number (WhatsApp → API Setup): Meta gives every app a free test number. Use it
to exercise
/webhooks/whatsappend-to-end before review clears. Inbound is free. - Per-org go-live ⏳ pending review: when a coach connects, the finalize step
(
whatsapp-connect.service.ts) exchanges the code, subscribes the WABA tomessages, and registers the number (registerWhatsAppNumber, idempotent). The coach’s number must be a real number that can receive the verification code (no VoIP), and its display name needs Meta approval before it can send.
5. App Review + Business Verification ⏳
Both products run in dev mode (only app admins/testers/the test number work) until App Review approves the advanced-access permissions. Submit once Business Verification is green.
| Product | Permissions to request | Why |
|---|---|---|
| Lead Ads | leads_retrieval, pages_show_list, pages_read_engagement, pages_manage_metadata, business_management | Read a Page’s lead forms + retrieve submitted leads + subscribe the Page webhook. |
whatsapp_business_management, whatsapp_business_messaging | Manage the WABA/number + receive inbound messages. |
The Lead Ads permission set is bundled into the Login for Business configuration (§3) — review it there. App Review needs a screencast of the real connect flow working, which is why the frontend connect cards (Settings → Integrations) had to ship first. Record: coach clicks Connect → authorizes → the connection appears active.
Restriction emails on a fresh test WABA (“breach of acceptable use”) are a common false positive on unverified test accounts — scoped to that test WABA, not your portfolio, and they don’t affect the live path. Appeal via Business Support Home and complete Business Verification.
6. Env summary
API env (apps/api/.env locally; Railway → @taikan/api in prod — the agent cannot
write prod env). All optional: when unset the pipeline fails safe (no connect, no
webhook routing) rather than erroring.
| Var | Used for |
|---|---|
META_APP_ID | App identity (OAuth + Embedded Signup). |
META_APP_SECRET | Shared webhook HMAC (both channels) + token exchange. |
META_VERIFY_TOKEN | Lead Ads webhook handshake. |
META_LOGIN_CONFIG_ID | Facebook Login for Business configuration. |
META_OAUTH_REDIRECT_URI | Lead Ads OAuth callback (must match the app’s allowed URI). |
WHATSAPP_VERIFY_TOKEN | WhatsApp webhook handshake (falls back to META_VERIFY_TOKEN). |
WHATSAPP_EMBEDDED_SIGNUP_CONFIG_ID | The Embedded Signup popup configuration. |
FRONTEND_URL | Where the Lead Ads OAuth callback redirects back to. |
PAYMENT_CREDENTIALS_ENCRYPTION_KEY | Encrypts stored access tokens at rest (reused). |
isConfigured (Lead Ads webhook) needs app id + secret + a verify token; isConnectConfigured
also needs the login config id + redirect uri. See meta-config.service.ts.
7. Prove it
- Webhook handshake — in the App Dashboard, hit Verify and Save on each webhook.
A green save means the
GEThandshake passed (token + reachable HTTPS). - Lead Ads — Meta’s Lead Ads Testing Tool (developers.facebook.com/tools/lead-ads-testing) → pick the connected Page + form → Create lead → a lead appears in Taikan within seconds.
- WhatsApp — from the test number’s API Setup page, send yourself a message, or message
the test number from a CTWA test ad. Only messages with a CTWA
referralcreate a lead (organic inbound is dropped by design).
8. Troubleshooting
| Symptom | Cause / fix |
|---|---|
| Webhook won’t save (“URL couldn’t be validated”) | The GET handshake failed — API not publicly reachable over HTTPS, or the verify token env ≠ what you typed. Set env + redeploy, then retry. |
| Webhook saves but no leads arrive | App-level callback is set but the Page/WABA isn’t subscribed — that happens in the connect flow (finalize). Confirm an active integration_connections row exists for the page_id/phone_number_id. |
401/403 on the POST | HMAC mismatch — the app sending events isn’t signing with META_APP_SECRET, or the secret rotated. |
| Lead Ads connect returns “no pages” | The coach granted the app no Pages, or the Login config doesn’t request pages_show_list. |
| WhatsApp connect popup does nothing | WHATSAPP_EMBEDDED_SIGNUP_CONFIG_ID unset → …/integrations/whatsapp/config returns nothing actionable; the card shows “not configured”. |
| Number registered but can’t send | Display name not yet approved, or it’s a VoIP number (rejected). |
| Everything 403s after going live | App still in dev mode — App Review not approved for the advanced permissions yet. |
Related
docs/features/leads-crm/lead-ingestion.md— the pipeline (flows, data model, code map).runbooks/inbox-email-inbound.md— the email ingestion channel (separateconversationsinbox; Cloudflare, not Meta). Ships with thefeat/lead-pipelines-inboxbranch.env-setup.md— full local env.- Meta: Lead Ads webhooks · WhatsApp Embedded Signup · CTWA .