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
    
    1. User clicks Connect Facebook → the UI fetches /api/facebook/connect and redirects to the returned Meta dialog URL.
    2. Meta redirects to {API_PUBLIC_URL}/v1/facebook/callback?code&state.
    3. The callback exchanges the code for a long‑lived user token, calls me/accounts, and upserts each page (facebook_page), fetching the linked instagram_business_account id when present.
    4. Each page is subscribed to leadgen,messages,messaging_postbacks via {pageId}/subscribed_apps, and its lead forms are synced into facebook_lead_form.
    5. 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, and igAccountId are refreshed.
    • messengerBotEnabled / igBotEnabled and 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. |