Facebook Lead Ads + Messenger
Connects Facebook Pages so the workspace receives lead‑ad submissions and Messenger messages, and (when the page has a linked IG business account) Instagram messages via the same page.
- API:
apps/api/src/facebook/(prefix/v1/facebook) - SDK:
@branofy/api/facebook - UI:
apps/premium/app/(dashboard)/settings/integrations/meta/page.tsx - Schema:
facebook_page,facebook_lead_form
See README.md for the shared OAuth model, signed state, env vars, and the general reconnection contract.
Endpoints
| Method | Path | Auth | Purpose |
| --- | --- | --- | --- |
| GET | /v1/facebook/connect | session/api‑key | Returns { url } — the Meta OAuth dialog URL (signed state). |
| GET | /v1/facebook/callback | public | OAuth redirect target. Exchanges code → long‑lived token → imports pages → subscribes webhooks → 302 to settings. |
| POST | /v1/facebook/connect-token | session/api‑key | Manual fallback: connect from a user token ({ userAccessToken }) or a page token ({ pageAccessToken }). Returns { pages, pageName }. |
| GET | /v1/facebook/status | session/api‑key | { connected, pages[] }. Each page's id doubles as its connection id. |
| GET | /v1/facebook/forms?connectionId=<pageId> | session/api‑key | { forms: [{ id, name, status, active }] }, optionally filtered to one page. |
| PATCH | /v1/facebook/forms | session/api‑key | { formId, active } — toggle whether a lead form's submissions are ingested. |
| PATCH | /v1/facebook/page-settings | session/api‑key | { connectionId, messengerBotEnabled?, igBotEnabled? } — per‑page bot toggles. |
| DELETE | /v1/facebook/status | session/api‑key | Body { pageId? } — disconnect one page, or all when omitted. |
| DELETE | /v1/facebook/status/:pageId | session/api‑key | Disconnect by path param (SDK form). |
| GET | /v1/facebook/webhook | public | Meta verification challenge (checks FACEBOOK_VERIFY_TOKEN). |
| POST | /v1/facebook/webhook | public (HMAC) | Lead/Messenger events; verifies x-hub-signature-256. |
Connect flow
sequenceDiagram
actor U as User (browser)
participant P as Premium /api proxy
participant A as NestJS API (/v1/facebook)
participant M as Meta Graph API
U->>P: GET /api/facebook/connect
P->>A: GET /v1/facebook/connect
A-->>U: { url } (dialog + signed state)
U->>M: redirect to OAuth dialog → consent
M-->>A: GET /v1/facebook/callback ?code & state
A->>A: verify state
A->>M: exchange code → long-lived user token
A->>M: GET me/accounts (pages + page tokens)
loop each page
A->>M: GET {page}?fields=instagram_business_account
A->>A: upsert facebook_page
A->>M: POST {page}/subscribed_apps (leadgen,messages)
A->>M: GET {page}/leadgen_forms → sync facebook_lead_form
end
A-->>U: 302 → /settings/integrations/meta?fb=connected&pages=N
- User clicks Connect Facebook → the UI fetches
/api/facebook/connectand redirects to the returned Meta dialog URL. - Meta redirects to
{API_PUBLIC_URL}/v1/facebook/callback?code&state. - The callback exchanges the code for a long‑lived user token, calls
me/accounts, and upserts each page (facebook_page), fetching the linkedinstagram_business_accountid when present. - Each page is subscribed to
leadgen,messages,messaging_postbacksvia{pageId}/subscribed_apps, and its lead forms are synced intofacebook_lead_form. - Redirect →
{APP}/settings/integrations/meta?fb=connected&pages=N.
OAuth scopes
public_profile, pages_show_list, pages_manage_metadata, pages_read_engagement, leads_retrieval, pages_messaging, instagram_basic, instagram_manage_messages, business_management.
Reconnecting
flowchart TD
R[connect / connect-token] --> F[fetch pages from token]
F --> C{facebook_page.id exists?}
C -- no --> I[INSERT page<br/>messengerBotEnabled=false<br/>igBotEnabled=false]
C -- yes --> U[UPDATE pageName, pageToken, igAccountId<br/>KEEP bot toggles & tracked forms]
I --> W[POST subscribed_apps<br/>leadgen, messages]
U --> W
W --> S[sync facebook_lead_form]
Re‑running connect (or Add More Pages) re‑imports and upserts by page id:
pageName,pageToken, andigAccountIdare refreshed.messengerBotEnabled/igBotEnabledand the set of tracked forms are preserved (only set on first insert).- Webhooks are re‑subscribed every time, so a page whose subscription lapsed is repaired by reconnecting.
Disconnect deletes the page row (and cascades its lead forms) and best‑effort unsubscribes the page app.
Webhook setup (provider side)
Set the Meta app webhook callback to {APP_PUBLIC_URL}/api/facebook/webhook with
verify token FACEBOOK_VERIFY_TOKEN. Subscribe the page to leadgen and
messages. Incoming POSTs are HMAC‑verified with the app secret before
processing; leadgen events fetch the lead and create a CRM contact if the
form is active.
Troubleshooting
| Symptom | Cause / fix |
| --- | --- |
| fb=error&reason=not_configured | No Meta App ID/Secret — set them in the Meta section or via FACEBOOK_CLIENT_*. |
| fb=error&reason=no_pages | The logging‑in user manages no pages, or didn't grant pages_show_list. |
| fb=error&reason=invalid_state | State expired (> 1h) or tampered — restart the flow. |
| Leads not arriving | Confirm the page is subscribed (reconnect re‑subscribes) and the specific lead form is toggled on. |