Przejdź do głównej zawartości

Integracja Meta Lead Ads (Leady z Facebooka / Instagrama)

Automatyczne zasilanie modułu CRM / Leady danymi z formularzy kontaktowych Meta (Facebook & Instagram Lead Ads) oraz import wydatków reklamowych do statystyk ROI / CAC.

👤 Instrukcja dla pracownika (Marketing / Biuro)

Po co to jest

Gdy klient wypełni formularz kontaktowy pod reklamą na Facebooku lub Instagramie, jego zgłoszenie automatycznie pojawia się w zakładce Dashboard ➔ Statystyki ➔ CRM / Leady z kanałem pozyskania Meta — bez ręcznego przepisywania.

Dodatkowo raz na dobę system pobiera z konta reklamowego Meta wydatki na reklamę i wpisuje je do budżetu marketingowego kanału Meta, dzięki czemu wskaźniki CAC (koszt pozyskania klienta) i ROI liczą się na realnych, aktualnych kwotach.

Jak z tego korzystać

  1. Nowy lead z Meta trafia do lejka ze statusem Nowy. Reaguj szybko — czas reakcji realnie wpływa na konwersję.
  2. Lead ma wypełnione dane z formularza (imię, nazwisko, telefon, e-mail) oraz informację, z której kampanii / reklamy pochodzi.
  3. Miasto przypisywane jest automatycznie na podstawie formularza (patrz sekcja techniczna — mapowanie form_id ➔ miasto). Jeśli lead trafi do złej lokalizacji, popraw pole miasta ręcznie w karcie leada.
  4. Dalej prowadzisz leada standardowo przez lejek: Nowy ➔ Kontakt ➔ Trening próbny ➔ Zapisany.

Czego pracownik NIE musi robić

  • Nie trzeba ręcznie wpisywać budżetu Meta — jest importowany automatycznie.
  • Nie trzeba przepisywać zgłoszeń z Facebooka — pojawiają się same.

🛠️ Dokumentacja techniczna

Architektura przepływu

Endpoint webhooka

app/api/public/meta-leads/route.ts

  • GET — handshake weryfikacyjny Meta. Zwraca hub.challenge, jeśli hub.verify_token == META_VERIFY_TOKEN.
  • POST — powiadomienia leadgen. Kolejność: rate limit → weryfikacja podpisu X-Hub-Signature-256 → parsowanie → pobranie leada z Graph API → upsert.
  • Na błędy przejściowe zwraca 200, aby Meta nie retry'owała agresywnie.

Idempotencja

Kolumna lead.meta_lead_id ma partycjonowany UNIQUE index (idx_lead_meta_lead_id ... WHERE meta_lead_id IS NOT NULL). Upsert używa ON CONFLICT (meta_lead_id) DO NOTHING, więc ponowne dostarczenie tego samego zdarzenia nie tworzy duplikatu ani nie nadpisuje ręcznych zmian.

Mapowanie pól formularza

lib/integrations/meta.tsmapLeadFieldData:

Pole leadaŹródło w field_data
emailnazwa zawiera email
phonenazwa zawiera phone
first_namefirst_name lub 1. człon full_name / name
last_namelast_name lub reszta full_name / name
product_interestnazwa zawiera interest/product/zajęc/program
miasto (override)nazwa zawiera city/miasto/lokaliz

Mapowanie miasta

resolveCityForForm(formId) odczytuje mapę z env META_FORM_CITY_MAP (JSON: {"<form_id>":"Opole","<form_id2>":"Wrocław"}). Jeśli w formularzu jest pytanie o miasto, jego wartość ma pierwszeństwo. W ostateczności używany jest DEFAULT_CITY.

Backfill historycznych leadów

Webhook dostarcza tylko nowe zgłoszenia (od momentu subskrypcji). Leady wysłane wcześniej trzeba dociągnąć jednorazowo. Służy do tego przycisk „Import z Meta" w widoku CRM (LeadsManager), który wywołuje akcję runMetaLeadBackfill (lib/actions/meta-backfill.ts, dostęp: admin/office).

Przebieg (backfillMetaLeads w lib/integrations/meta-ingest.ts):

  1. GET /me/accounts → wszystkie strony (fallback GET /me).
  2. GET /{page_id}/leadgen_forms → wszystkie formularze strony.
  3. GET /{form_id}/leads z filtrem time_created > now-90d (paginacja).
  4. Każdy lead przez ten sam idempotentny upsertMetaLead.

Backfill jest bezpieczny do wielokrotnego uruchomienia (dzięki ON CONFLICT meta_lead_id) i można go puścić także po starcie webhooka.

Ograniczenie Meta: leady są dostępne przez API zwykle do ~90 dni wstecz; starsze mogą być już niedostępne niezależnie od parametru sinceDays.

Import wydatków (CAC / ROI)

lib/actions/meta-spend.tsimportMetaAdSpend(env), wołany z crona 0 6 * * * w worker.ts (klucz meta_ad_spend_import, sterowany przez tabelę konfiguracji cronów). Pobiera spend z GET /act_{id}/insights za bieżący miesiąc (strefa Europe/Warsaw) i robi upsert do marketing_budget (channel='meta'). Statystyki ROI w lib/actions/analytics/marketing.ts konsumują tę wartość bez zmian.

Ograniczenie: insights są na poziomie konta reklamowego, więc wydatek trafia do jednego miasta (META_SPEND_CITY, domyślnie DEFAULT_CITY). Podział per miasto wymagałby osobnych kont/kampanii i breakdownu — do rozważenia w przyszłości.

Wymagane zmienne środowiskowe (secrets)

ZmiennaOpis
META_APP_SECRETSekret aplikacji Meta — weryfikacja podpisu webhooka.
META_VERIFY_TOKENDowolny sekret ustawiony też w panelu Meta (handshake).
META_PAGE_ACCESS_TOKENPage Access Token z uprawnieniem leads_retrieval.
META_FORM_CITY_MAP(opcjonalnie) JSON mapy form_id ➔ miasto.
META_AD_ACCOUNT_IDID konta reklamowego (z lub bez prefiksu act_).
META_ADS_ACCESS_TOKENToken z uprawnieniem ads_read (fallback: page token).
META_SPEND_CITY(opcjonalnie) miasto, do którego trafia wydatek Meta.

Sekrety ustawiamy przez wrangler secret put <NAZWA>.

Konfiguracja po stronie Meta (jednorazowo)

  1. Aplikacja Meta z produktem Webhooks i Lead Ads.
  2. App Review na uprawnienie leads_retrieval (oraz ads_read dla wydatków).
  3. Webhook → obiekt Page, pole leadgen, callback: https://<host>/api/public/meta-leads, verify token = META_VERIFY_TOKEN.
  4. Subskrypcja strony (Page) do aplikacji.