31 sierpnia 2026 r. OpenAI udostępniło samoobsługowego Menedżera reklam na rynkach europejskich, w tym w Polsce. Zanim uruchomię pierwszą kampanię, chciałem mieć pomiar, któremu ufam, więc zacząłem od własnej strony.

Wdrożenie zajęło jeden dzień i dwie rundy audytu. Opisuję je w całości: co zmieniłem, co się posypało, jak to sprawdziłem i czego nadal nie wiem. Cały pomiar prowadzę na własnym koncie w Menedżerze reklam OpenAI.

Zaznaczam od razu, czego w tym tekście nie ma. Nie ma wyników kampanii, bo kampania nie jest jeszcze uruchomiona. Nie ma też zrzutów z panelu ani liczników zdarzeń, i to jest decyzja, którą tłumaczę w sekcji 12. To jest opis wdrożenia pomiaru i tego, co udało mi się o nim potwierdzić.

Schemat pomiaru ChatGPT Ads: wysłanie formularza tworzy jeden identyfikator zdarzenia, który idzie dwoma kanałami - pikselem w przeglądarce na endpoint sdk events bez uwierzytelnienia i Conversions API z serwera z nagłówkiem Authorization Bearer - a oba trafiają do tego samego źródła danych, gdzie strumień pokazuje je jako pixel_sdk i server_to_server
Jak zdarzenie trafia do OpenAI dwoma drogami. Wspólny identyfikator jest warunkiem deduplikacji, a nazwa pola różni się między kanałami.

01 / TL;DR Najważniejsze wnioski

Zdarzenia objęte integracją kieruję do obu kanałów: piksela w przeglądarce i Conversions API z serwera, ze wspólnym identyfikatorem. Odbiór obu kanałów potwierdziłem w kontrolowanym teście na page_viewed i contents_viewed.

Stan na 3 września 2026. Sześć rzeczy, które wyszły dopiero w testach:

  • Snippet, który OpenAI daje w panelu, startuje z włączoną zgodą na pomiar. Wklejony wprost do <head> mierzy, zanim odwiedzający odpowie na banner cookies. Piksel wpiąłem więc w istniejącą warstwę pomiarową i wstrzykuję SDK dopiero po zgodzie marketingowej.
  • Audyt wyłapał błąd w mojej bramce zgody: po wycofaniu i ponownym udzieleniu zgody bez przeładowania strony pomiar nie wracał do końca tej odsłony. Przyczyną była raz-flaga, która kończyła funkcję ładującą, zanim ta zdążyła wysłać consent(true).
  • Druga poprawka: strona potwierdzenia newslettera brała świeży identyfikator przy każdym załadowaniu, więc odświeżenie wysyłało zdarzenie z nowym identyfikatorem i kopie nie spełniały warunku deduplikacji. Identyfikator czyta się teraz z parametru w adresie, jednym mechanizmem dla wszystkich kanałów naraz.
  • Komplet CSP to trzy dyrektywy i cztery pary dyrektywa-źródło. Jedna z nich pobiera konfigurację piksela. W mojej konfiguracji stało w niej automatic_advanced_matching_enabled: true, czyli odblokowanie tej ścieżki umożliwia pobranie konfiguracji z włączonym automatycznym dopasowaniem. Sprawdziłem to, zanim uzupełniłem politykę prywatności.
  • Komunikat queue flushed w konsoli nie dowodzi, że OpenAI przyjęło zdarzenie. SDK wysyła je w trybie no-cors, więc kod strony nie odczytuje odpowiedzi serwera.
  • Normalizacja danych kontaktowych dla OpenAI wymaga w PHP mb_strtolower. Przy strtolower imię „Łukasz” daje inny wynik normalizacji i inny hasz.

02 / Podstawy Czym różni się piksel ChatGPT Ads od Conversions API?

Piksel to skrypt w przeglądarce odwiedzającego. Conversions API to wysyłka tego samego zdarzenia z Twojego serwera. Oba trafiają do tego samego źródła danych w Menedżerze reklam OpenAI i mogą się uzupełniać, ograniczając utratę zdarzeń.

Piksel a Conversions API
przesuń w bok, żeby zobaczyć całość →
Piksel (SDK)Conversions API
gdzie działaprzeglądarka odwiedzającegoTwój serwer
endpointPOST /v1/sdk/eventsPOST /v1/events
uwierzytelnieniepubliczny identyfikator piksela, bez tajnego kluczaAuthorization: Bearer
typ treścitext/plainapplication/json
pole identyfikatoraevent_id w opcjach zdarzeniaid w zdarzeniu
widzi status odpowiedzinietak
co go zatrzymujewtyczka w przeglądarce, filtrujący DNS, brak zgodywysyłka do OpenAI nie zależy od blokad w przeglądarce; dotarcie zdarzenia do serwera nadal zależy od klienta

Kanał serwerowy może dostarczyć zdarzenie mimo zablokowania domeny OpenAI w przeglądarce, o ile samo zdarzenie dotarło wcześniej do serwera. Wracam do tego w sekcji 08.

Deduplikacja działa po trójce: identyfikator piksela, nazwa zdarzenia i identyfikator zdarzenia. OpenAI zachowuje kopię, która dotarła pierwsza, i ignoruje kolejne. Obie kopie muszą mieć zgodny identyfikator piksela, nazwę zdarzenia i identyfikator zdarzenia, a przy zdarzeniach niestandardowych także tę samą nazwę własną. Dlatego nazwa pola ma tu znaczenie, bo w pikselu identyfikator przekazuje się jako event_id, a w Conversions API jako id.

03 / Zgoda Dlaczego nie wkleiłem snippetu z panelu

Krótka odpowiedź: bo pomiar jest w nim domyślnie włączony. Dokumentacja OpenAI mówi wprost, że piksel inicjuje zgodę jako true, dopóki nie ustawisz jej na false, chyba że ma zapisany wcześniej sprzeciw. Zablokowane zdarzenia nie są potem odtwarzane.

Na stronie, która uruchamia pomiar dopiero po zgodzie, domyślne ustawienie jest sprzeczne z tą zasadą. Snippet wklejony do <head> pobiera skrypt i zapisuje ciasteczka, zanim odwiedzający kliknie cokolwiek w bannerze.

Zamiast tego wpiąłem piksel w plik assets/js/tracking.js, w którym już wcześniej mieszkały Meta i LinkedIn. Jedno miejsce zmiany zamiast 35 plików HTML i ta sama bramka zgody co reszta narzędzi. Kolejność wewnątrz funkcji ładującej jest istotna: najpierw stub kolejkujący, potem jawne oaiq('consent', true), dopiero na końcu init.

Przy okazji wyłączyłem debug. Snippet pobrany z mojego panelu 1 września miał go ustawionego na true, więc każdy odwiedzający dostawałby logi w konsoli. U mnie tryb diagnostyczny włącza się wyłącznie parametrem w adresie.

Przetestowałem cztery stany, bo trzy pierwsze to za mało:

przesuń w bok, żeby zobaczyć całość →
StanCzego oczekiwałemCo zmierzyłem
brak decyzji w bannerzezero żądań, brak window.oaiqzgodnie z oczekiwaniem, skrypt nie jest nawet pobierany
zgoda udzielonaSDK pobrany, zdarzenia wychodzązgodnie z oczekiwaniem
zgoda wycofanameasure nie generuje żądaniazgodnie z oczekiwaniem
zgoda udzielona ponownie, bez przeładowaniapomiar wracanie wracał, patrz sekcja 04

Ciasteczka, które SDK zapisało po zgodzie, z czasami życia odczytanymi 1 września: __obref z identyfikatorem przeglądarki na 365 dni, __oaiq_consent na 30 dni oraz __oppref na 30 dni, przy czym to ostatnie powstaje tylko wtedy, gdy w adresie lądowania jest identyfikator kliknięcia reklamy. Wszystkie trafiły do polityki prywatności.

04 / Błąd Dlaczego po ponownej zgodzie pomiar nie wracał

Krótka odpowiedź: funkcja ładująca wychodziła na raz-fladze, zanim zdążyła włączyć zgodę.

Moja loadOaiPixel() zaczynała się od if (oaiLoaded) return. Zabezpieczenie przed dwukrotnym wstrzyknięciem skryptu jest tu potrzebne, natomiast przy takim zapisie obsługuje też ścieżkę, w której SDK jest już załadowane, a zgoda została w międzyczasie wycofana. Wtedy funkcja kończy działanie i nikt nie wysyła consent(true).

Skutek: odwiedzający, który wycofał zgodę, a potem udzielił jej ponownie w tej samej odsłonie, nie był mierzony do końca tej odsłony. Dopiero przeładowanie strony przywracało pomiar.

Poprawka jest jednolinijkowa. Raz-flaga nadal chroni przed powtórnym wstrzyknięciem skryptu, ale przed wyjściem wysyła oaiq('consent', true).

Wynik testu: po wycofaniu zgody wywołanie measure nie generuje żadnego żądania, a po ponownym jej udzieleniu lead_created wychodzi normalnie.

Piszę o tym osobną sekcją, bo to był mój błąd, nie usterka platformy, a jest to dokładnie ten rodzaj usterki, którego nie znalazłem w sprawdzonych materiałach. Ścieżki „zgoda, wycofanie, ponowna zgoda, wszystko bez przeładowania” nie opisuje dokumentacja, a w bannerze cookies da się ją przejść trzema kliknięciami.

05 / Zdarzenia Które nazwy zdarzeń przyjmuje ChatGPT Ads

Krótka odpowiedź: zamkniętą listę nazw, a własne zdarzenia wysyła się jako custom z nazwą własną. Nazwa spoza zestawu nie przechodzi, a w badanej wersji SDK działo się to bez widocznego błędu.

Dokumentacja OpenAI wymienia 13 nazw, w tym custom. Przeglądarkowy SDK obsługuje 11 z nich, ponieważ app_installed i app_opened są dostępne wyłącznie przez Conversions API ze źródłem mobile_app. W praktyce znaczy to 12 zdarzeń standardowych plus custom w dokumentacji i 10 standardowych plus custom w przeglądarce.

Każda nazwa ma przypisaną rodzinę danych i to ona decyduje, jakie pola wolno wysłać:

przesuń w bok, żeby zobaczyć całość →
ZdarzenieRodzina danych
page_viewed, contents_viewed, checkout_started, items_added, order_createdcontents
lead_created, registration_completed, appointment_scheduledcustomer_action
subscription_created, trial_startedplan_enrollment
customcustom
app_installed, app_openedcustomer_action, tylko przez Conversions API

Cztery zasady walidacji, na których łatwo się przewrócić:

  1. W badanej wersji SDK pole spoza listy dozwolonej dla danej rodziny powodowało odrzucenie całego zdarzenia, a nie zignorowanie samego pola. Po stronie Conversions API błąd w jednym zdarzeniu potrafi odrzucić całą paczkę.
  2. amount wymaga currency. Kwoty idą jako liczby całkowite w najmniejszej jednostce waluty, czyli dla złotego w groszach.
  3. custom_event_name przekazuje się w opcjach wywołania piksela, a w Conversions API w samym obiekcie zdarzenia. Nazwa ma 1-64 znaki, zaczyna się i kończy literą albo cyfrą, składa się z liter, cyfr, podkreśleń i myślników i nie może kolidować z nazwą wbudowaną. Sam używam wyłącznie małych liter.
  4. type jest obowiązkowe i musi odpowiadać rodzinie zdarzenia.

Tak wygląda mapowanie u mnie. Lewa kolumna to nazwy z mojej warstwy pomiarowej, wspólne dla Mety, LinkedIn i ChatGPT Ads:

przesuń w bok, żeby zobaczyć całość →
Moje zdarzenieChatGPT AdsRodzina
page_view_capipage_viewedcontents
view_contentcontents_viewedcontents
lead_submitlead_createdcustomer_action
newsletter_confirmedregistration_completedcustomer_action
newsletter_submitcustom / newsletter_intentcustom
contact_intentcustom / contact_intentcustom

Konwersją główną jest lead_created, czyli wysłanie formularza kontaktowego. W materiałach, które dostałem przy zakładaniu piksela, przykładem było registration_completed. To zdarzenie opisuje ukończoną rejestrację, na przykład konta albo zapisu na wydarzenie, więc skopiowane bez zastanowienia daje konwersję mierzącą coś innego, niż się wydaje. Przypisanie go u mnie do potwierdzenia newslettera to moja decyzja pomiarowa, nie reguła platformy.

06 / CSP Trzy dyrektywy, cztery pary i jedna konsekwencja

Krótka odpowiedź: restrykcyjna CSP potrafi zablokować wybrane operacje piksela, a naruszenia widać dopiero w konsoli i w panelu sieciowym. Cztery pary dyrektywa-źródło poniżej to minimum dla samego SDK. Jeżeli Twoja polityka blokuje też kod inline, dochodzi nonce albo hash dla skryptu inicjalizującego.

przesuń w bok, żeby zobaczyć całość →
DyrektywaŹródłoPo co
script-srchttps://bzrcdn.openai.comwczytanie SDK
connect-srchttps://bzr.openai.comwysyłka zdarzeń
connect-srchttps://bzrcdn.openai.compobranie konfiguracji piksela
img-srchttps://bzr.openai.comawaryjny transport obrazkowy

Pierwsze wdrożenie miałem bez trzeciego wiersza. Audyt to zgłosił i miał rację, tylko konsekwencja okazała się poważniejsza, niż wyglądała.

Dodanie tego źródła do connect-src umożliwia SDK pobranie pliku pixel-config/v1/<ID>.json. Zajrzałem do niego, zanim cokolwiek zmieniłem. Zwracał:

pixel-config/v1/<ID>.json
{"automatic_advanced_matching_enabled": true}

Automatyczne dopasowanie zaawansowane oznacza, że piksel sam wykrywa w formularzach adres e-mail, telefon i imię, haszuje je w przeglądarce i dołącza do zdarzeń konwersji. W badanej wersji SDK sterowała tym ta konfiguracja, bez zmian w kodzie strony, a udokumentowane klucze init to pixelId, debug i user. Przełącznik samego dopasowania znalazłem w panelu.

W praktyce znaczy to tyle, że uzupełnienie CSP bez sprawdzenia zawartości tego pliku może rozjechać politykę prywatności ze stanem faktycznym. Moja mówiła wtedy wprost, że dane kontaktowe do OpenAI nie trafiają. Sama flaga nie dowodzi jeszcze, że dane faktycznie są dołączane, natomiast wystarczy, żeby dotychczasowy zapis polityki wymagał ponownej weryfikacji.

Rozstrzygnąłem to decyzją, a nie przypadkiem: dopasowanie zostaje włączone, CSP uzupełnione o brakujące źródło, a polityka prywatności poprawiona w sekcjach o odbiorcach danych i o narzędziach marketingowych, analogicznie do wpisów o Meta CAPI, LinkedIn CAPI i konwersjach rozszerzonych Google. Role stron i przetwarzanie danych z EOG sprawdziłem w Ad Tools DPA OpenAI, do którego odsyłają Warunki dotyczące konwersji: co do zasady obie strony są niezależnymi administratorami, a dane z EOG przetwarza OpenAI Ireland Limited.

Jeżeli wdrażasz to u siebie, potraktuj tę sekcję jako punkt do sprawdzenia, a nie jako gotowy wniosek. Konfiguracja jest per piksel, więc u Ciebie ten plik może zwracać co innego.

07 / Dowód Dlaczego „queue flushed” nie potwierdza przyjęcia zdarzenia

Krótka odpowiedź: SDK wysyła zdarzenia tak, że sam nie widzi odpowiedzi serwera.

W badanej wersji SDK wysyłka szła jako fetch w trybie no-cors, z typem treści text/plain. Odpowiedź jest wtedy nieprzezroczysta, czyli kod strony nie odczyta ani statusu HTTP, ani treści. Komunikat o opróżnieniu kolejki mówi wyłącznie tyle, że SDK oddało paczkę do wysłania.

Więcej mówią trzy inne rzeczy, każda o czymś innym: panel sieciowy przeglądarki pokazuje status konkretnego żądania, strumień zdarzeń w Menedżerze reklam potwierdza odbiór po stronie OpenAI, a żądanie kontrolne spoza przeglądarki sprawdza sam endpoint. Żadna z nich nie mówi nic o atrybucji.

Strzał kontrolny na kanał pikselowy wygląda tak:

szablon · kanał pikselowy
curl -sS -i -X POST \ "https://bzr.openai.com/v1/sdk/events?pid=<PIXEL_ID>&st=oaiq-web&sv=0.1.32&t=$(date +%s)000&ec=1" \ -H "Content-Type: text/plain" \ -H "Origin: https://twojadomena.pl" \ --data '{"obref":null,"events":[{"type":"page_viewed","timestamp_ms":<ms>, "id":"<UUID>","source_url":"https://twojadomena.pl/","data":{"type":"contents"}}]}'

To jest szablon: <PIXEL_ID>, <ms> i <UUID> trzeba podmienić na realne wartości, inaczej ciało nie jest poprawnym JSON-em. Wersję w parametrze sv podaję taką, jaką miało u mnie SDK 1 września.

Odpowiedzi, które zaobserwowałem w testach:

przesuń w bok, żeby zobaczyć całość →
OdpowiedźZnaczenie
202 {"accepted_events":1}przyjęte do przetworzenia; nie znaczy jeszcze, że rekord pojawi się w panelu
401 unknown client keyzły identyfikator piksela
400 z opisem polabłąd w ciele żądania

Dwie uwagi, zanim to odpalisz. Po pierwsze, taki test może zapisać zdarzenie i wpłynąć na statystyki. Po drugie, kształt tego żądania odtworzyłem z zachowania SDK, a nie z publicznej dokumentacji: obref musiał być kanonicznym UUID małymi literami albo null, a parametr ec w adresie musiał odpowiadać liczbie zdarzeń w ciele. Udokumentowany kontrakt serwerowy to /v1/events, opisany niżej.

To jest test transportu, nie test integracji. Sukces żądania do endpointu pikselowego nie mówi nic o tym, czy Twoje Conversions API działa, bo to inny endpoint i inne uwierzytelnienie.

08 / Drugi kanał Po co Conversions API, skoro piksel działa

Krótka odpowiedź: bo piksela zatrzymuje wtyczka albo sieć odwiedzającego, a wysyłki z serwera nie.

To nie jest przypadek teoretyczny i mam na to konkretny pomiar. 1 września w mojej przeglądarce każde żądanie do bzr.openai.com wracało z kodem 503. Z tej samej maszyny curl dostawał normalne odpowiedzi:

przesuń w bok, żeby zobaczyć całość →
TestPrzeglądarkacurl z tej samej maszyny
GET na goły adres503404, czyli normalna odpowiedź serwera
POST z celowo błędnym ciałem503400 z opisem błędu
POST z poprawnym ciałem503202, accepted_events: 1

Wykonałem testy zawężające diagnostykę. CSP odpada, bo żądanie wychodzi i wraca ze statusem. Strona nie rejestruje service workera. Identyfikator piksela jest poprawny, bo obcy daje 401, a mój 202. Kształt ciała jest poprawny, bo identyczne ciało przechodzi przez curl. Nagłówki też nie tłumaczą różnicy, bo curl z pełnym zestawem dostaje 202. Żaden z tych testów nie odtwarza jednak całego zachowania przeglądarki.

Nie ustaliłem, która warstwa powodowała tę różnicę. Meta Pixel działał w tej samej przeglądarce, więc nie wszystko było blokowane. To jednak nie wyklucza blokera z regułą na ten jeden host ani innego traktowania obu klientów po stronie odbiorcy.

Epilog: w teście z 3 września zdarzenia z tej samej przeglądarki pojawiły się w strumieniu jako pixel_sdk. Przyczyny ustąpienia objawu znam dokładnie tyle samo, co przyczyny jego wystąpienia, czyli nic.

Żeby było jasno: Conversions API niczego tu nie naprawiło i nie odzyskało konkretnych utraconych zdarzeń. Nie mam z tamtego dnia równoczesnego testu obu kanałów, więc takiego twierdzenia nie postawię. Ten przypadek skłonił mnie natomiast do utrzymania dwóch kanałów pomiaru i sprawdzania odbioru każdego z nich osobno. Odwiedzającego, u którego piksel nie przechodzi, nie zapytasz, dlaczego.

Kanał serwerowy działa u mnie jako proxy PHP, które waliduje wejście, haszuje dane kontaktowe, dokłada adres IP i User-Agent, a potem wysyła zdarzenie z nagłówkiem Authorization: Bearer. Klucz API leży poza katalogiem publicznym i nigdy nie trafia do przeglądarki.

Trzy rzeczy, których proxy pilnuje po mojej stronie:

  • Katalog zdarzeń jest na serwerze. Klient podaje nazwę, a rodzinę danych dobiera serwer, więc podrobiony klient nie narzuci własnego mapowania nazwy na rodzinę. Pozostałe pola i tak wymagają osobnej walidacji.
  • Znacznik czasu poza oknem, które API akceptuje, jest odrzucany, czyli starszy niż 7 dni albo dalszy niż 10 minut w przód. Pierwsza wersja podmieniała go na czas bieżący i to był błąd: przesuwałaby zdarzenie do innego okresu raportowego zamiast je odrzucić.
  • Adres źródłowy jest przypięty do własnej domeny, a query i fragment są odcinane. Odrzuca też mojadomena.pl.evil.com.

Uczciwie o granicy: ochrona tego proxy opiera się na nagłówkach Origin i Referer, które klient serwerowy potrafi podrobić, oraz na limicie żądań na adres IP. To nie jest pełne uwierzytelnienie i nie potwierdza, że zdarzenie odpowiada prawdziwemu działaniu użytkownika. Mocniejsze warianty istnieją, na przykład jednorazowy token generowany przy renderowaniu strony. Wdrożyłbym go wtedy dla obu proxy naraz, nie tylko dla OpenAI.

09 / Normalizacja Dlaczego „Łukasz” daje inny hasz niż powinien

Krótka odpowiedź: bo strtolower w PHP nie zmienia wielkich liter spoza ASCII, więc normalizacja daje inny wynik i inny hasz.

Zanim dane kontaktowe zostaną zahaszowane, trzeba je znormalizować dokładnie tak, jak robi to OpenAI. Inaczej hasz się nie zgodzi, a ten identyfikator przestaje pasować, bez żadnego widocznego błędu:

przesuń w bok, żeby zobaczyć całość →
PoleReguła
e-mailprzytnij białe znaki, zamień na małe litery
telefonusuń spacje, nawiasy, kropki i myślniki, potem wiodący plus i wiodące zera; zostaje 8-15 cyfr
imię, nazwiskomałe litery, usuń białe znaki i interpunkcję ASCII, zachowaj znaki spoza ASCII
identyfikator zewnętrznytylko przytnij białe znaki, wielkość liter zostaje
kraj, miastosurowo, bez haszowania

W normalizacji dla OpenAI trzeba zachować wielkość liter identyfikatora zewnętrznego, a imiona i nazwiska zamieniać na małe litery z obsługą znaków spoza ASCII. Te dwa wiersze warto objąć testami.

strtolower w PHP zamienia tylko litery ASCII. Wielkie znaki spoza ASCII zostawia w spokoju, co przy imionach zaczynających się od nich daje inny wynik niż mb_strtolower. Sprawdziłem to na kilku przypadkach:

przesuń w bok, żeby zobaczyć całość →
Wejściestrtolowermb_strtolowerHasz identyczny
Michałmichałmichałtak
ŁukaszŁukaszłukasznie
ŚwiątekŚwiątekświąteknie
JOSÉjosÉjosénie

Zwróć uwagę na pierwszy wiersz, bo pokazuje, dlaczego taki błąd łatwo przeoczyć. Na „Michale” obie funkcje dają to samo, więc wdrożenie sprawdzone na tym jednym przykładzie wygląda na poprawne. Rozjeżdża się dopiero przy Łukaszu, Świątku czy Żanecie. Ostatni wiersz tabeli pokazuje, że nie jest to problem wyłącznie polski.

Do zamiany imion i nazwisk na małe litery używam mb_strtolower($x, 'UTF-8'). Uwaga: tej zamiany nie wolno zastosować do identyfikatora zewnętrznego, w którym wielkość liter ma zostać zachowana.

Zestaw kontrolny, który warto powtórzyć po każdej zmianie w normalizacji: +1 (415) 555-2671 daje 14155552671, Mary Jane daje maryjane, O'Connor daje oconnor, José daje josé. Na koniec sprawdź hasz. SHA-256 z 14155552671 musi dać 758fbf68…52417fe0, bo dokładnie ta wartość stoi w przykładzie w dokumentacji OpenAI. Zgodność potwierdza wtedy poprawny wynik dla tego przypadku, nie dla całej normalizacji.

Jeszcze jedna rzecz z tej kategorii. Testowałem endpoint klientem urllib z Pythona i dostawałem 403 z kodem 1010, czyli blokadę Cloudflare opartą na sygnaturze klienta. Ta odpowiedź nie pozwalała ocenić poprawności klucza. Testuj curl-em, a klucz podawaj przez plik konfiguracyjny, nigdy w linii poleceń.

10 / Identyfikator Dlaczego odświeżenie strony wysyłało zdarzenie drugi raz

Krótka odpowiedź: bo identyfikator zdarzenia powstawał przy każdym załadowaniu strony.

Strona /newsletter/potwierdzone odpala zdarzenie potwierdzenia rejestracji przy każdym wejściu, a moja warstwa pomiarowa generowała do niego świeży identyfikator. Odświeżenie strony albo ponowne otwarcie linku z maila wysyłało więc kolejne zdarzenie z nowym identyfikatorem, przez co kopie nie spełniały warunku deduplikacji. Jak policzyło to raportowanie, nie sprawdziłem.

To wygląda niewinnie do momentu, w którym zdasz sobie sprawę, że link z maila potwierdzającego ludzie klikają po kilka razy.

Poprawka polega na tym, żeby identyfikator opisywał działanie, a nie odsłonę strony. Serwer wstawia w adres potwierdzenia parametr z identyfikatorem, którego użył po swojej stronie, a warstwa pomiarowa czyta go stamtąd. Powtórne wejścia niosą jedną stabilną wartość.

Ważny szczegół z drugiej rundy audytu: naprawiłem to u źródła, czyli w jednym miejscu obsługującym wszystkie kanały naraz. Wcześniej robiły to osobno LinkedIn i ChatGPT Ads, a Meta jako jedyna dalej brała losową wartość przy każdym odświeżeniu. Ten błąd istniał u mnie przed wdrożeniem piksela OpenAI i wyszedł przy okazji.

Jedno zastrzeżenie, żeby nie było nieporozumienia. Ten sam identyfikator wędruje u mnie do Mety, LinkedIn i ChatGPT Ads, ale to nie znaczy, że platformy deduplikują cokolwiek między sobą. Każda deduplikuje wyłącznie własne kopie. Wspólna wartość służy temu, żeby piksel i kanał serwerowy tej samej platformy rozpoznały to samo zdarzenie.

11 / Panel Cztery kroki kreatora i pułapka w nazwie

Piksel zbierający zdarzenia to dopiero połowa roboty. Żeby kampania konwersyjna miała pod co się optymalizować, trzeba utworzyć zdarzenie konwersji i przypiąć je do kampanii.

Kreator w Menedżerze reklam ma cztery kroki: utwórz źródło danych, utwórz zdarzenie konwersji, zaimplementuj i zarejestruj zdarzenie, połącz zdarzenie z kampanią.

Karta „Skonfiguruj śledzenie konwersji” w Menedżerze reklam OpenAI z czterema krokami: trzy pierwsze odhaczone zielonym znacznikiem, czwarty „Połącz zdarzenie z kampanią” pozostaje nieukończony
Trzy kroki zamknięte, czwarty czeka na kampanię. Przypięcie konwersji robi się przy tworzeniu kampanii, a przy kampanii konwersyjnej zdarzenia nie da się już potem zmienić.

Moja konfiguracja:

przesuń w bok, żeby zobaczyć całość →
PoleWartość
nazwaFormularz kontaktowy
zdarzenie bazowelead_created
źródło danychpiksel mojej strony
okno atrybucji po kliknięciu30 dni

I drobiazg, który kosztuje minutę, jeżeli się na niego nie uważa. Pole nazwy jest wstępnie wypełnione nazwą zdarzenia bazowego, a wpisywanie dopisuje do tej wartości zamiast ją zastąpić. Limit to 30 znaków, więc bez zaznaczenia całości powstaje nazwa w rodzaju „Lead CreatedFormularz kontakto”.

Panel pokazuje też metrykę nazwaną „pokrycie identyfikatorów”. Jej wartości nie podaję z powodu opisanego w sekcji 12.

→ Porozmawiajmy

Chcesz uruchomić reklamy w ChatGPT i mieć pomiar, któremu ufasz?

Na bezpłatnej rozmowie sprawdzimy, co mierzy dziś Twoja strona, czego brakuje do kampanii konwersyjnej i czy w ogóle warto zaczynać od tego kanału.

Zobacz, jak wdrażam pomiar ChatGPT Ads →

12 / Granice Czego to wdrożenie nie dowodzi

Tę sekcję piszę, bo bez niej reszta tekstu byłaby mocniejsza, niż na to zasługuje.

Co potwierdziłem. W kontrolowanym wejściu na podstronę Menedżer reklam zarejestrował page_viewed i contents_viewed z obu kanałów, osobno oznaczonych jako przeglądarkowy i serwerowy. To potwierdza odbiór zdarzeń obiema drogami. Cztery stany zgody zachowują się tak, jak powinny. Bramka zgody obejmuje przy tym oba kanały, bo wysyłka serwerowa jest wywoływana wewnątrz funkcji, która bez zgody marketingowej kończy działanie. To odczytałem z kodu, nie zmierzyłem osobnym testem żądań do proxy. Normalizacja daje hasz zgodny z przykładem opublikowanym w dokumentacji.

Czego nie potwierdziłem. Widzę, że obie kopie zostały przyjęte, natomiast nie widzę warstwy raportowej, więc deduplikacji na tej podstawie nie stwierdzam. Bliskość czasowa niczego tu nie dowodzi, bo warunkiem deduplikacji jest zgodność identyfikatora piksela, nazwy zdarzenia i identyfikatora zdarzenia, a nie sekunda różnicy. Rozstrzygnie to dopiero kontrolowane zdarzenie o znanym identyfikatorze zestawione z raportem kampanii.

Piksel zbiera zdarzenia, mimo że nie uruchomiłem jeszcze żadnej kampanii. Konkretnych liczników z panelu nie podaję, z powodu opisanego niżej.

Czego tu w ogóle nie ma. Wyników kampanii. Kampania nie jest uruchomiona, więc żadna liczba w tym tekście nie jest wynikiem reklamowym.

Zostały mi w panelu dwa ostrzeżenia i oba są zgodne z obecnym stanem konta: nie mam aktywnej kampanii korzystającej z tej konwersji, a w części zarejestrowanych zdarzeń brakuje danych użytkownika. Pełnego podziału tego braku według nazw zdarzeń i kanałów nie ustaliłem.

Dlaczego w tym artykule nie ma zrzutów z panelu

Miałem je przygotowane i ostatecznie ich nie publikuję. Powód siedzi w Warunkach dotyczących konwersji, które akceptuje się, włączając narzędzia pomiarowe OpenAI. Definicja w punkcie 7 jest szeroka:

Reporting Data means reports, analytics, metrics, insights, or other information provided or made available to you in connection with the Conversion Tools. Reporting Data is OpenAI's Confidential Information.

Punkt 3 mówi, że bez uprzedniej pisemnej zgody OpenAI nie wolno tych danych ujawniać, udostępniać ani zapewniać do nich dostępu, z wyjątkiem przekazania własnym dostawcom usług. Liczniki zdarzeń, pokrycie identyfikatorów i wiersze strumienia czytam jako mieszczące się w tej definicji, więc zrzuty tych widoków wyleciały z artykułu razem z liczbami.

Wyciąłem w związku z tym wszystko, co pochodzi z widoków panelu. Reszta tekstu opiera się na moim własnym kodzie, konfiguracji i testach normalizacji, więc została nienaruszona. Nie przesądzam, gdzie dokładnie przebiega granica tej definicji, bo od tego są prawnicy, a nie ja.

Piszę o tym osobno, bo to pułapka łatwa do przeoczenia. Zrzut z panelu reklamowego wygląda na dowód, który wolno pokazać, skoro to własne konto. Przy narzędziach konwersji OpenAI warunki mówią co innego, a sprawdzenie ich zajmuje pięć minut.

Zakres wdrożenia

Kamil Sławiński wdrożył na kamilslawinski.com pomiar ChatGPT Ads przez piksel OpenAI i Conversions API, z obsługą zmian zgody marketingowej oraz wspólnymi identyfikatorami zdarzeń. Weryfikacja z 3 września 2026 r. potwierdziła odbiór zdarzeń z obu kanałów w Menedżerze reklam. Opis wdrożenia dokumentuje kod, wykryte błędy i wyniki testów.

Data wdrożenia: 1 września 2026 r. Data weryfikacji: 3 września 2026 r.

Zanim uruchomisz kampanię, sprawdź dwa roboty

OpenAI ma osobnego robota OAI-AdsBot, który odwiedza strony zgłoszone jako reklamy i sprawdza je pod kątem bezpieczeństwa. Jeżeli blokujesz roboty regułą zbiorczą, warto się upewnić, że ten ma dostęp.

Osobno działa OAI-SearchBot, który obsługuje wyszukiwanie w ChatGPT. Strona wyłączona z jego dostępu nie pojawi się w odpowiedziach wyszukiwarki, choć dopuszczenie dostępu samo w sobie niczego nie gwarantuje.

To dwie różne sprawy i warto sprawdzić obie w robots.txt, zanim zaczniesz wydawać budżet. U mnie reguły pozwalają na dostęp obu robotom. Sam plik nie potwierdza jednak, że robot faktycznie stronę odwiedził ani że przepuszcza go warstwa CDN.

13 / FAQ Najczęstsze pytania

Czy trzeba wdrażać piksel i Conversions API jednocześnie?

Nie trzeba, ale warto. Piksel wystarczy do uruchomienia pomiaru. Kanał serwerowy dokłada zdarzenia od tych odwiedzających, u których przeglądarka albo sieć blokuje żądania do OpenAI. U mnie był dzień, w którym z mojej własnej przeglądarki nie przechodziło nic, a poprawne odpowiedzi uzyskiwałem przez curl. Odbiór obu kanałów potwierdziłem dopiero w późniejszym teście.

Czy załadowanie skryptu oznacza, że pomiar działa?

Nie. SDK wysyła zdarzenia w trybie no-cors, więc nie odczytuje odpowiedzi serwera, a komunikat o opróżnieniu kolejki mówi tylko tyle, że paczka została oddana do wysłania. Potwierdzenie daje panel sieciowy przeglądarki oraz strumień zdarzeń w Menedżerze reklam. Żądanie kontrolne wysłane spoza przeglądarki sprawdza sam endpoint i nie potwierdza działania integracji przeglądarkowej.

Jak podłączyć piksel ChatGPT Ads do bannera zgód?

Nie wklejaj snippetu z panelu do sekcji head, bo piksel startuje z włączoną zgodą i zacznie mierzyć przed decyzją odwiedzającego. Wstrzykuj SDK dopiero po zgodzie marketingowej, wyślij jawne consent(true) przed init, a przy wycofaniu zgody wyślij consent(false). Przetestuj cztery stany, łącznie z ponownym udzieleniem zgody bez przeładowania strony.

Dlaczego po ponownym udzieleniu zgody pomiar nie wraca?

W moim wdrożeniu przyczyną było to, że funkcja ładująca piksel miała zabezpieczenie przed dwukrotnym wstrzyknięciem skryptu i kończyła działanie, zanim wysłała zgodę. SDK zostawało wtedy wyłączone do końca odsłony. Rozwiązanie: przed wyjściem z funkcji wyślij consent(true).

Jak uniknąć podwójnego liczenia konwersji z piksela i Conversions API?

Obie kopie muszą nieść ten sam identyfikator zdarzenia, w pikselu w polu event_id, a w Conversions API w polu id. Deduplikacja działa po trójce: identyfikator piksela, nazwa zdarzenia i identyfikator zdarzenia. Zachowana zostaje kopia, która dotarła pierwsza.

Dlaczego odświeżenie strony podziękowania wysyła zdarzenie drugi raz?

W moim wdrożeniu przyczyną było to, że identyfikator zdarzenia powstawał przy każdym załadowaniu strony i opisywał odsłonę zamiast działania. Identyfikator powinien pochodzić z serwera, na przykład z parametru w adresie strony potwierdzenia, żeby ponowne wejścia niosły tę samą wartość.

Które zdarzenia obsługuje ChatGPT Ads?

Dokumentacja OpenAI wymienia 13 nazw, w tym custom. Przeglądarkowy SDK obsługuje 11 z nich, ponieważ app_installed i app_opened działają wyłącznie przez Conversions API ze źródłem mobile_app. Własne zdarzenia wysyła się jako custom z nazwą własną. Każda nazwa ma przypisaną rodzinę danych, a w badanej wersji SDK pole spoza niej powodowało odrzucenie całego zdarzenia.

Dlaczego API przyjmuje zdarzenia, a panel jest pusty?

Najpierw sprawdź, czy w konfiguracji nie został włączony tryb walidacyjny. Przy validate_only ustawionym na true API poprawnie przyjmuje zdarzenia i ich nie zapisuje, więc wszystko wygląda sprawnie, a w panelu nie ma nic. Rozdziel przy tym dwa widoki: pusty strumień źródła danych nie potwierdza odbioru zdarzeń i wymaga sprawdzenia żądań, walidacji, filtrów i czasu przetwarzania, a pusty raport kampanii może wynikać z braku skonfigurowanej konwersji albo z tego, że zdarzenia nie zostały przypisane do reklamy.

→ Zaczynamy?

Wdrażam pomiar i uruchamiam kampanie ChatGPT Ads dla firm i sklepów

Spinam piksel, API konwersji i zdarzenie pod Twój cel, a potem uruchamiam kampanię i prowadzę ją z miesiąca na miesiąc. Zaczynamy od miesiąca testowego z ustalonym budżetem, a o kontynuacji decydujesz Ty.

Reklamy w ChatGPT dla Twojej firmy →

Odpowiadam w ciągu 24 godzin w dni robocze. Budżet reklamowy rozliczasz bezpośrednio z OpenAI.

Kamil Sławiński

Kamil Sławiński

Konsultant AI · wdrożenia agentów AI, szkolenia z AI, AI Visibility i Meta Ads

Przez ponad siedem lat prowadziłem kampanie Meta Ads dla firm w Polsce i UK. Dziś wdrażam agenty AI, które przejmują procesy marketingu - montaż wideo, kampanie reklamowe, ofertowanie, strony www. Prowadzę też szkolenia z AI dla zespołów i audyty AI Visibility (GEO). Piszę AI Memo - newsletter o agentach AI w praktyce. Założyciel WAYSTAR Kamil Sławiński (NIP PL6511727806).