16 czerwca 2026
Webhook Shopify jako siatka bezpieczeństwa konwersji — deduplikacja w sGTM
Część zakupów nigdy nie trafia do Twojego trackingu. Ad-blocker, ITP, błąd JS, zerwane łącze na mobile — transakcja się dzieje, ale dataLayer + web GTM + server GTM jej nie widzą. Płacisz za kliknięcie, które sprzedało, a Google Ads i Meta nigdy się o tym nie dowiadują. Algorytm licytuje dalej na niepełnych danych.
Shopify wysyła webhook zamówienia prosto z backendu, niezależnie od tego, co zrobiła przeglądarka. To druga, niezależna ścieżka dowozu tej samej konwersji. Webhook Shopify do trackingu konwersji łata dziurę po stronie, której front-end nie kontroluje — pod warunkiem, że domkniesz dwa problemy: webhook nie niesie danych atrybucji, i nie wolno mu policzyć konwersji, którą online już policzył.
To czwarty wariant wzorca, który rozkładam w tej serii (sGTM + Stape Store jako mini-CDP). Trzy poprzednie wzbogacały event danymi z magazynu. Ten robi coś innego: jest siatką bezpieczeństwa pod całą resztą.
Dwa problemy, które trzeba rozwiązać naraz
Webhook sam w sobie jest prosty — Shopify go po prostu wysyła. Trudność jest w dwóch rzeczach, które dochodzą razem z nim.
Pierwszy: webhook nie zna gclid, fbc ani fbp. Te identyfikatory żyją w przeglądarce, a webhook leci z serwera sklepu. Bez nich konwersja dociera do Meta i Google Ads ze słabym dopasowaniem, czyli połowicznie bezużyteczna. Trzeba je odtworzyć z profilu zapisanego wcześniej, gdy użytkownik był jeszcze online.
Drugi: skoro są teraz dwie ścieżki dla tej samej konwersji, nie może wpaść dwa razy. Większość zakupów przejdzie normalnie (web → server GTM) i webhookiem. Gdyby webhook odpalał tagi bezwarunkowo, podwoiłbyś każdą konwersję w raportach reklamowych. Potrzebny jest rejestr „co już policzyłem” i sprawdzenie przed wysłaniem.
Pierwszy problem to wzbogacenie. Drugi to deduplikacja. UC4 to oba naraz.
Architektura — dwie ścieżki, dwa klucze
Całość spinają dwa niezależne klucze w Stape Store i jeden warunek czasowy.
fbc/fbp/gclid
tracked: yes
tracked?
fbc/fbp/gclid
cart_token i status pod transaction_id. Webhook przychodzi z minutowym opóźnieniem, sprawdza status i odpala tagi tylko dla transakcji, której online nie zdążył policzyć.Dwa klucze, bo pełnią dwie różne role. cart_token indeksuje dane atrybucji — był obecny, gdy fbc/fbp/gclid lądowały w magazynie, a transaction_id jeszcze nie istniał. transaction_id indeksuje status „policzone / nie” — to po nim webhook rozpoznaje duplikat. Pomylenie ich albo użycie jednego do obu zadań rozsypuje mechanizm.
Skąd webhook bierze atrybucję: capture, gdy user jest online
Zanim webhook w ogóle przyjdzie, dane atrybucji muszą już leżeć w Store. Zbieram je przy checkout_started — to najbliższy zakupowi moment, w którym cart_token jest już stały. Shopify Web Pixel wysyła je na serwer:
analytics.subscribe("checkout_started", async (event) => {
const payload = {
event: "store_cookies",
cart_token: init.data.cart.id,
fbp: await browser.cookie.get("_fbp"),
fbc: await browser.cookie.get("_fbc"),
gclid: await browser.cookie.get("gclid")
};
fetch("https://<subdomena-stape>/data", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
keepalive: true
});
});
Web Pixel działa w sandboxie, dlatego browser.cookie.get(), nie document.cookie. I tu jest pułapka, którą łatwo przeoczyć: _fbc i _fbp ustawia sam Pixel Meta, ale gclid nikt za Ciebie nie zapisze. Google nie ma odpowiednika base code’u, który trzymałby gclid w cookie gotowym do odczytu. Musisz go złapać samodzielnie — zmienna URL po parametrze gclid plus tag w web GTM, który zapisuje go do cookie na okno atrybucji (domyślnie 90 dni), z warunkiem if (gclid), żeby kolejne organiczne wejście nie nadpisało wartości pustką.
Ten tag podłącz pod trigger „Wszystkie strony”, nie „Consent Initialization”. To dwie różne warstwy, które nazwy mylą. Consent Initialization jest dla skryptów ustawiających domyślny stan zgody — odpala się bezwarunkowo, zanim zgoda jest znana. Zapis gclid to mechanizm ad-trackingowy, więc ma być odwrotnie: zależny od zgody. Daj zwykły Page View plus warunek ad_storage = granted w ustawieniach samego tagu. Bez Kroku capture cała ścieżka serwerowa wygląda na poprawną i nigdy nie wysyła gclid — cisza identyczna jak przy rozjeździe typów niżej.
Na serwerze tag Stape Store Writer zapisuje trzy ad ID pod kluczem cart_token. Payload store_cookies ma własny schemat, więc zmienne Event Data czytają fbc/fbp/gclid wprost z niego.
Deduplikacja przez wyścig z opóźnieniem
To jest serce UC4. Online’owy purchase — ten, który przeszedł normalnie — zapisuje pod swoim transaction_id jeden dokument: tracked: yes. Żadnych danych, tylko flaga. To rejestr „policzyłem już tę transakcję”.
Webhook po stronie triggera robi tylko jedno sprawdzenie: odpala tagi, gdy Lookup po transaction_id nie zwraca yes. Transakcja już zarejestrowana online → webhook milczy. Nie zarejestrowana → webhook ją donosi.
Cały mechanizm stoi na kolejności dwóch żądań, a ta z natury jest nieprzewidywalna: online’owy purchase i webhook mogą dotrzeć w dowolnej sekwencji. Gdyby webhook wyprzedził zapis statusu, sprawdziłby pusty rejestr i odpalił tag dla transakcji, którą online policzy ułamek sekundy później. Duplikat.
Dlatego webhook dostaje celowe opóźnienie. Power-up Request Delay w Stape (URL webhooka z prefiksem /delay/1/data) wstrzymuje go o minutę. Tyle wystarcza, żeby online’owy zapis tracked: yes zdążył powstać, zanim webhook sięgnie do Store. Opóźnienie idzie na ścieżkę webhooka, nigdy na online’owy purchase — odwrotna konfiguracja psuje całą sekwencję.
Warunek krytyczny: typ transaction_id musi się zgadzać
To najważniejsze znalezisko z tej weryfikacji, i Stape ostrzega o nim wprost. transaction_id dociera do sGTM dwiema drogami: online’owym purchase (gdzie schema ecommerce zwykle niesie go jako string) i webhookiem Shopify (gdzie identyfikator zamówienia bywa liczbą JSON). Stape Store traktuje Document ID jako klucz dokładny. "450789469" i 450789469 to dla magazynu dwa różne dokumenty.
Konsekwencja jest gorsza niż we wcześniejszych use case’ach. Tam rozjazd klucza oznaczał brak wzbogacenia — tag wychodził, tylko bez dodatkowych danych. Tutaj Lookup statusu nigdy nie trafi na dokument zapisany online, bo szuka pod innym typem klucza. Webhook zawsze uzna transakcję za niepoliczoną i odpali tag — nawet gdy online ją zarejestrował. Efekt: duplikat konwersji na każdej transakcji, nie brak danych.
Lekarstwo jest jednolinijkowe: jedna zmienna rzutująca transaction_id na string, użyta jako Document ID w obu miejscach — przy zapisie statusu i przy jego odczycie. Dwa niezależne wywołania w czasie, jedna wspólna zmienna. To wystarczy, żeby klucz zapisu równał się kluczowi odczytu.
Odtworzenie atrybucji i wysyłka
Gdy webhook przejdzie warunek (transakcja niepoliczona), trzy zmienne Lookup wyciągają fbc/fbp/gclid z dokumentu pod cart_token — nie pod transaction_id, bo to cart_token indeksuje dane atrybucji. Stąd lecą do tagów:
- Meta CAPI —
fbcmapuje się na Click ID,fbpna Browser ID. To mapowanie łatwo odwrócić w głowie; zostaw je dokładnie tak. - Google Ads — tag z nadpisanym
gclidze zmiennej Lookup. Dla konwersji zgłaszanej z opóźnieniem architektonicznie czystszy jest dedykowany szablon Offline Conversion Tracking, ale standardowy tag zgclidteż zadziała.
Payload webhooka ma strukturę zgodną z GA4 (items, value, currency, user_data), więc generyczne zmienne Event Data zbudowane już dla online’owego purchase — email, first_name, reszta PII — działają na webhooku bez duplikowania.
I jeszcze najprostszy do przeoczenia bloker: w gotowej konfiguracji tag Meta dla webhooka bywa wstrzymany w GTM. Cała reszta łańcucha może być poprawna, a tag i tak się nie odpali. Sprawdź status, zanim ruszysz z testem end-to-end.
Weryfikacja
- Tagi aktywne. Tag Meta i Google Ads webhooka nie są wstrzymane. Bez tego żaden test niczego nie pokaże.
gclidsię zapisuje. Wejdź z?gclid=test123, sprawdź cookie w DevTools. Wejdź ponownie bez parametru — cookie ma zostać, nie zniknąć.- Typ klucza statusu. Na online’owym
purchaseotwórz dokument w Store i potwierdź, żetransaction_idjest tam jakostring(w cudzysłowie). To bezpośrednia weryfikacja ryzyka z duplikatem. - Webhook bez duplikatu. Wyślij webhook dla transakcji już policzonej online → tag webhookowy nie odpala się.
- Webhook jako fallback. Wyślij webhook dla
transaction_id, które nie przeszło online → tag odpala się, afbc/fbp/gclidw Meta i Google Ads są odtworzone z dokumentu podcart_token. - Opóźnienie. Request Delay aktywny i ustawiony na ścieżce webhooka, nie na online’owym
purchase.
Prywatność — zanim trafi na produkcję
Use Case 4 utrwala w Store dwa rodzaje danych: identyfikatory reklamowe (fbc/fbp/gclid) i PII z payloadu transakcji.
- Zapis cookie
gclid, capturestore_cookiesi rejestr online’owegopurchasewarunkuj zgodą marketingową — dlagclidto opisana wyżej reguła All Pages plus warunek zgody na tagu. - Ustaw TTL na dokumentach. Stan opcji TTL w panelu Stape bywał różny między wersjami — zweryfikuj aktualny bezpośrednio przed wdrożeniem, nie zakładaj.
- Łączenie
gclid/fbc/fbpz konkretną transakcją przez webhook opisz w polityce prywatności. To świadome wiązanie identyfikatorów reklamowych z zakupem.
To czwarty wariant tego samego wzorca (sGTM + magazyn jako mini-CDP), tym razem jako siatka bezpieczeństwa zamiast wzbogacania. Poprzednie: wzbogacanie AddToCart danymi PII i przywracanie fbc/fbp dla zakupu offline. Szerszy kontekst odzyskiwania sygnałów po stronie serwera opisuję w case study o server-side taggingu.