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ć
- Nowy lead z Meta trafia do lejka ze statusem Nowy. Reaguj szybko — czas reakcji realnie wpływa na konwersję.
- Lead ma wypełnione dane z formularza (imię, nazwisko, telefon, e-mail) oraz informację, z której kampanii / reklamy pochodzi.
- 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. - 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. Zwracahub.challenge, jeślihub.verify_token==META_VERIFY_TOKEN.POST— powiadomienialeadgen. Kolejność: rate limit → weryfikacja podpisuX-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.ts → mapLeadFieldData:
| Pole leada | Źródło w field_data |
|---|---|
email | nazwa zawiera email |
phone | nazwa zawiera phone |
first_name | first_name lub 1. człon full_name / name |
last_name | last_name lub reszta full_name / name |
product_interest | nazwa 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):
GET /me/accounts→ wszystkie strony (fallbackGET /me).GET /{page_id}/leadgen_forms→ wszystkie formularze strony.GET /{form_id}/leadsz filtremtime_created > now-90d(paginacja).- 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.ts → importMetaAdSpend(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ślnieDEFAULT_CITY). Podział per miasto wymagałby osobnych kont/kampanii i breakdownu — do rozważenia w przyszłości.
Wymagane zmienne środowiskowe (secrets)
| Zmienna | Opis |
|---|---|
META_APP_SECRET | Sekret aplikacji Meta — weryfikacja podpisu webhooka. |
META_VERIFY_TOKEN | Dowolny sekret ustawiony też w panelu Meta (handshake). |
META_PAGE_ACCESS_TOKEN | Page Access Token z uprawnieniem leads_retrieval. |
META_FORM_CITY_MAP | (opcjonalnie) JSON mapy form_id ➔ miasto. |
META_AD_ACCOUNT_ID | ID konta reklamowego (z lub bez prefiksu act_). |
META_ADS_ACCESS_TOKEN | Token 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)
- Aplikacja Meta z produktem Webhooks i Lead Ads.
- App Review na uprawnienie
leads_retrieval(orazads_readdla wydatków). - Webhook → obiekt Page, pole leadgen, callback:
https://<host>/api/public/meta-leads, verify token =META_VERIFY_TOKEN. - Subskrypcja strony (Page) do aplikacji.