Skip to content
EVE Milano Consulenza SEO

EveMilano Logo White EveMilano Logo White

Hai una campagna su ChatGPT Ads e un sito WordPress. Perché quella campagna funzioni, la piattaforma deve sapere quali click hanno prodotto un ordine o un contatto: senza quel ritorno di informazione l’algoritmo compra click e non clienti, e tu non sai quali annunci ripagano e quali stanno bruciando budget.

Questa guida installa quel collegamento. Si copiano e si incollano dei file PHP — nessun plugin da comprare, nessuna modifica al tema — e alla fine hai:

  • il pixel di misurazione su tutte le pagine, con il consenso pilotato dalla tua CMP;
  • l’identificatore di click (oppref) catturato all’atterraggio e conservato fino alla conversione;
  • gli eventi del funnel e la conversione, inviata sia dal browser sia dal server;
  • gli UTM e i canali GA4 allineati, per ritrovare quel traffico nei tuoi report.

Quanto ci vuole: mezz’ora se raccogli solo contatti da form, circa un’ora se hai WooCommerce. Le due variabili che possono allungare i tempi sono la tua CMP e l’eventuale Content Security Policy: entrambe hanno una sezione dedicata.

Cosa non è. Non è una traduzione della documentazione OpenAI. Gli snippet sono scritti per WordPress, verificati con php -l su PHP 8.2 e controllati contro i sorgenti di WooCommerce 11. Il perché di ogni scelta — deduplica, finestre di attribuzione, cosa succede se oppref non sopravvive, come GA4 classifica questo traffico — sta nella guida tecnica completa al tracciamento delle ChatGPT Ads. Qui si incolla e si verifica.

Cosa ti serve prima di iniziare

CosaDove si prendeDove va
Pixel IDAds Manager → scheda ConversionsFile 1 (browser) e File 5 (server)
Conversions API keyAds Manager → Conversions → Create conversion keySolo in wp-config.php, mai in un file del tema
Accesso FTP/SFTP o File ManagerIl tuo hostingCartella wp-content/mu-plugins/

Quali file ti servono davvero

Non servono tutti a tutti. Questa tabella è la mappa: leggila prima di copiare qualsiasi cosa.

FileA cosa serveTi serve se…
File 1 — pixelCarica lo script, gestisce il consenso, mette in coda gli eventiSempre. È la base di tutto
File 2opprefCattura e conserva l’identificatore di clickSempre. Senza, le conversioni non vengono attribuite
File 3 — consensoCollega la tua CMP al pixelSempre, se hai un banner cookie (cioè quasi certamente)
File 4 — eventi WooCommerceProdotto visto, checkout avviato, ordine conclusoSolo con WooCommerce. Senza shop, salta al File 6
File 5 — Conversions APIRimanda l’ordine dal server: la fonte più affidabileSolo con WooCommerce. Tecnicamente opzionale, in pratica è quello che recupera le conversioni perse
File 6 — lead da formContatto inviato, con o senza pagina di ringraziamentoSe raccogli lead invece di vendere online, o in aggiunta

Detto in due righe: sito vetrina o di servizi → file 1, 2, 3, 6. E-commerce WooCommerce → file 1, 2, 3, 4, 5, più il 6 se hai anche form di contatto.

Cos’è la cartella mu-plugins e perché usarla

**MU sta per Must Use. È una cartella dentro wp-content/ che WordPress carica automaticamente a ogni richiesta**, prima dei plugin normali. Ogni file PHP che ci metti diventa un plugin sempre attivo: non compare nell’elenco con un pulsante “Attiva”, non si può disattivare per sbaglio, e nessun aggiornamento di WordPress, del tema o dei plugin lo tocca.

Se non esiste, creala: wp-content/mu-plugins/. Non serve installare niente, WordPress la cerca da sé.

Perché lì e non nel functions.php del tema:

  • Sopravvive al cambio tema. Il codice nel functions.php sparisce il giorno che cambi tema, e il tracciamento si spegne senza che nessuno se ne accorga per settimane.
  • Non si disattiva per errore. Un plugin di tracciamento spento da un collega è un classico.
  • Un file, una funzione. Se qualcosa va storto, cancelli un file e hai isolato il problema.

Due vincoli da conoscere: i mu-plugin vengono caricati solo se stanno nella root della cartella — un file dentro una sottocartella viene ignorato — e non si disattivano dal pannello: per spegnerne uno si rinomina o si cancella il file.

L’ordine dei file non conta: tutto gira dentro gli hook di WordPress, quindi al momento dell’esecuzione sono già stati caricati tutti.

File 1 — Il pixel su tutte le pagine

Serve a tutti. wp-content/mu-plugins/chatgpt-ads-pixel.php

Mette lo snippet in cima al <head> di ogni pagina, con il consenso negato di default, e aggiunge una coda per gli eventi. Quella coda è la parte che conta: con consenso negato il pixel scarta gli eventi e non li ripete quando il consenso arriva dopo. La coda li trattiene e li rilascia al momento giusto.

<?php
/**
 * Plugin Name: ChatGPT Ads - Pixel
 * Description: Measurement Pixel su tutte le pagine, consenso negato di default,
 *              coda eventi in attesa del consenso.
 */

defined( 'ABSPATH' ) || exit;

// <<< UNICA COSA DA MODIFICARE IN QUESTO FILE
const CGPT_PIXEL_ID = 'INSERISCI-IL-TUO-PIXEL-ID';

add_action( 'wp_head', 'cgpt_pixel_snippet', 1 ); // priorità 1 = il più in alto possibile
function cgpt_pixel_snippet() {
	if ( ! cgpt_pixel_ready() ) {
		return;
	}
	?>
<script>
(function (w, d, s, u) {
  if (w.oaiq) return;
  var q = function () { q.q.push(arguments); };
  q.q = []; w.oaiq = q;
  var js = d.createElement(s); js.async = true; js.src = u;
  var f = d.getElementsByTagName(s)[0];
  f.parentNode.insertBefore(js, f);
})(window, document, "script", "https://bzrcdn.openai.com/sdk/oaiq.min.js");

// Consenso negato PRIMA dell'init: senza questa riga il pixel parte con consenso true.
oaiq("consent", false);
oaiq("init", { pixelId: <?php echo wp_json_encode( CGPT_PIXEL_ID ); ?> });

// Coda eventi. Gli eventi bloccati dal consenso non vengono ripetuti dall'SDK:
// li tratteniamo qui e li rilasciamo quando la CMP conferma.
window.cgptConsent = null;
window.cgptQueue = [];
window.cgptMeasure = function () {
  var args = [].slice.call(arguments);
  if (window.cgptConsent === true) { oaiq.apply(null, ["measure"].concat(args)); }
  else if (window.cgptConsent === null) { window.cgptQueue.push(args); }
  // consenso negato: l'evento viene scartato, non accodato
};
window.cgptGrant = function (granted) {
  window.cgptConsent = !!granted;
  oaiq("consent", !!granted);
  if (granted) {
    while (window.cgptQueue.length) {
      oaiq.apply(null, ["measure"].concat(window.cgptQueue.shift()));
    }
  } else {
    window.cgptQueue.length = 0;
  }
};
</script>
	<?php
}

/** Evita di sparare richieste con il Pixel ID segnaposto ancora dentro. */
function cgpt_pixel_ready() {
	return defined( 'CGPT_PIXEL_ID' )
		&& CGPT_PIXEL_ID
		&& 'INSERISCI-IL-TUO-PIXEL-ID' !== CGPT_PIXEL_ID;
}

Da qui in avanti non chiamerai mai oaiq("measure", ...) direttamente: userai sempre cgptMeasure(...), che rispetta il consenso da solo.

File 2 — Catturare oppref lato server

Serve a tutti. wp-content/mu-plugins/chatgpt-ads-oppref.php

oppref è l’identificatore che ChatGPT Ads aggiunge all’URL quando qualcuno clicca il tuo annuncio. È il filo che collega quel click alla conversione: se si spezza, la campagna non sa di aver funzionato.

Il pixel lo salva in un cookie suo, ma quel cookie è scritto via JavaScript: PHP non lo vede in modo affidabile e Safari ne limita la durata a sette giorni. Questo file lo cattura dalla query string e lo riscrive con Set-Cookie lato server, sul dominio padre.

<?php
/**
 * Plugin Name: ChatGPT Ads - Cattura oppref
 * Description: Salva oppref dalla query string in un cookie leggibile lato server.
 *              Espone cgpt_oppref() e cgpt_obref() per gli altri file.
 * Requisiti: PHP 7.3+ (opzioni setcookie in array).
 */

defined( 'ABSPATH' ) || exit;

const CGPT_OPPREF_COOKIE = 'cgpt_oppref';
const CGPT_OPPREF_DAYS   = 90;   // allinea alla tua finestra di attribuzione

add_action( 'init', 'cgpt_capture_oppref', 1 );
function cgpt_capture_oppref() {
	if ( empty( $_GET['oppref'] ) ) {
		return;
	}
	$oppref = cgpt_opaque( wp_unslash( $_GET['oppref'] ) );
	if ( '' === $oppref ) {
		return;
	}

	$host   = (string) wp_parse_url( home_url(), PHP_URL_HOST );
	$domain = ( 0 === strpos( $host, 'www.' ) ) ? substr( $host, 4 ) : $host;

	setcookie(
		CGPT_OPPREF_COOKIE,
		$oppref,
		array(
			'expires'  => time() + DAY_IN_SECONDS * CGPT_OPPREF_DAYS,
			'path'     => '/',
			'domain'   => '.' . $domain,  // dominio padre: leggibile anche su shop.dominio.it
			'secure'   => is_ssl(),
			'httponly' => true,           // non serve al JavaScript di pagina
			'samesite' => 'Lax',
		)
	);
	$_COOKIE[ CGPT_OPPREF_COOKIE ] = $oppref; // disponibile già in questa richiesta
}

/**
 * oppref e obref sono stringhe opache: si validano, non si normalizzano.
 * Niente lowercase, niente trim, niente sanitize che possa alterarle.
 */
function cgpt_opaque( $value ) {
	$value = is_string( $value ) ? $value : '';
	return preg_match( '/^[A-Za-z0-9_.~-]{1,512}$/', $value ) ? $value : '';
}

function cgpt_oppref() {
	return isset( $_COOKIE[ CGPT_OPPREF_COOKIE ] )
		? cgpt_opaque( wp_unslash( $_COOKIE[ CGPT_OPPREF_COOKIE ] ) )
		: '';
}

function cgpt_obref() {
	// __obref lo scrive il pixel via JS, ma il browser lo rimanda comunque
	// al server nelle richieste successive: PHP può leggerlo direttamente.
	return isset( $_COOKIE['__obref'] )
		? cgpt_opaque( wp_unslash( $_COOKIE['__obref'] ) )
		: '';
}

Attenzione alla cache. Questo file gira solo se PHP viene eseguito. Se hai una page cache che serve HTML statico ignorando la query string, oppref non viene mai catturato e tutto il resto è inutile. La sezione sulle esclusioni cache è obbligatoria, non opzionale.

File 3 — Collegare la CMP al consenso

Serve a tutti quelli che hanno un banner cookie. wp-content/mu-plugins/chatgpt-ads-consent.php

Questo file fa una cosa sola, ed è il punto in cui si perdono più conversioni: traduce “l’utente ha accettato i cookie di marketing” in “il pixel può inviare”. Senza, il File 1 lascia il consenso a false per sempre e non parte niente.

Va detto chiaramente perché sorprende quasi tutti: il consenso di ChatGPT Ads non è Google Consent Mode. Anche se la tua CMP gestisce già ad_storage per Google Ads e GA4, il pixel OpenAI non legge quei segnali. È un canale separato e va cablato a mano, una volta sola.

Prima: quale CMP hai?

Guarda l’elenco plugin del sito. Nella grande maggioranza dei casi è uno di questi quattro nomi:

PluginCategoria di consenso da controllare
Complianzmarketing
Cookiebotmarketing
CookieYesadvertisement
iubendafinalità 5, Targeting e Pubblicità

Se non trovi nessuno di questi, cerca “cookie” o “consent” nell’elenco: qualunque CMP espone un evento JavaScript al momento della scelta dell’utente, ed è quello che serve.

Il file

Incolla il blocco della tua CMP e cancella gli altri: tenerli tutti non rompe niente, ma rende il file illeggibile fra sei mesi.

<?php
/**
 * Plugin Name: ChatGPT Ads - Consenso
 * Description: Collega la CMP al consenso del pixel OpenAI. Tieni UN SOLO blocco.
 * Richiede: chatgpt-ads-pixel.php
 */

defined( 'ABSPATH' ) || exit;

add_action( 'wp_footer', 'cgpt_consent_bridge', 5 );
function cgpt_consent_bridge() {
	?>
<script>
/* ============ COMPLIANZ ============ */
document.addEventListener("cmplz_enable_category", function (e) {
  if (e.detail && e.detail.category === "marketing") { cgptGrant(true); }
});
document.addEventListener("cmplz_status_change", function () {
  if (typeof cmplz_has_consent === "function") { cgptGrant(cmplz_has_consent("marketing")); }
});
// consenso già memorizzato da una visita precedente
if (typeof cmplz_has_consent === "function" && cmplz_has_consent("marketing")) { cgptGrant(true); }

/* ============ COOKIEBOT ============ */
// CookiebotOnConsentReady copre sia la scelta appena fatta sia quella già memorizzata.
window.addEventListener("CookiebotOnConsentReady", function () {
  cgptGrant(!!(window.Cookiebot && Cookiebot.consent && Cookiebot.consent.marketing));
});

/* ============ COOKIEYES ============ */
document.addEventListener("cookieyes_consent_update", function (e) {
  var accepted = e.detail && e.detail.accepted;
  cgptGrant(!!(accepted && accepted.indexOf("advertisement") !== -1));
});

/* ============ IUBENDA ============ */
/* Non si aggancia da qui: va aggiunto al tuo _iub.csConfiguration, campo callback.
   callback: {
     onConsentGiven:    function () { cgptGrant(true);  },
     onConsentRejected: function () { cgptGrant(false); }
   }                                                                              */

/* ============ NESSUNA CMP (nessun banner cookie sul sito) ============ */
/* cgptGrant(true); */
</script>
	<?php
}

Se la tua CMP non è nell’elenco

Funzionano tutte allo stesso modo: espongono un evento al momento della scelta. Serve solo trovarne il nome nella documentazione del plugin e chiamare cgptGrant(true) o cgptGrant(false). Lo schema è sempre questo:

document.addEventListener("NOME_EVENTO_DELLA_TUA_CMP", function () {
  cgptGrant( /* espressione che vale true se c'è consenso marketing */ );
});

Come verificare che funzioni davvero

Trenta secondi, ed è l’unico modo per esserne sicuri. Apri il sito in una finestra anonima e, nella console del browser:

window.cgptConsent   // null  → nessuna scelta ancora fatta, gli eventi sono in coda

Accetta i cookie dal banner e ricontrolla: deve diventare true. Poi revoca il consenso dalle impostazioni della CMP e controlla ancora: deve tornare false. È il test che quasi nessuno fa, e la revoca è la metà che di solito non è cablata.

Se dopo l’accettazione resta null, l’evento della tua CMP ha un nome diverso da quello che hai incollato.

File 4 — Eventi WooCommerce

Serve solo se hai WooCommerce. Se il tuo sito raccoglie soltanto contatti da form, salta direttamente al File 6: questo file e il prossimo non ti riguardano.

wp-content/mu-plugins/chatgpt-ads-woo.php

Segnala i tre momenti che contano in un e-commerce: prodotto visto, checkout avviato, ordine concluso. Usa wp_footer con i condizionali di WooCommerce invece degli hook dei template classici, così funziona anche con i temi a blocchi e con il checkout a blocchi, dove diversi hook storici non vengono più eseguiti.

<?php
/**
 * Plugin Name: ChatGPT Ads - Eventi WooCommerce
 * Description: contents_viewed, checkout_started, order_created lato browser.
 * Richiede: chatgpt-ads-pixel.php
 */

defined( 'ABSPATH' ) || exit;

add_action( 'wp_footer', 'cgpt_woo_events', 20 );
function cgpt_woo_events() {
	if ( ! function_exists( 'WC' ) ) {
		return;
	}

	// 1. Scheda prodotto
	if ( function_exists( 'is_product' ) && is_product() ) {
		// Non usare la global $product: su wp_footer, e sui temi a blocchi,
		// spesso non è più valorizzata.
		$product = wc_get_product( get_the_ID() );
		if ( $product instanceof WC_Product ) {
			cgpt_emit( 'contents_viewed', array(
				'type'     => 'contents',
				'amount'   => cgpt_cents( $product->get_price() ),
				'currency' => get_woocommerce_currency(),
				'contents' => array(
					array(
						'id'           => (string) $product->get_id(),
						'name'         => $product->get_name(),
						'content_type' => 'product',
						'quantity'     => 1,
					),
				),
			) );
		}
	}

	// 2. Checkout avviato
	if ( function_exists( 'is_checkout' ) && is_checkout() && ! is_order_received_page()
		&& WC()->cart && ! WC()->cart->is_empty() ) {
		$items = array();
		foreach ( WC()->cart->get_cart() as $line ) {
			$p = $line['data'];
			if ( ! $p instanceof WC_Product ) {
				continue;
			}
			$items[] = array(
				'id'           => (string) $p->get_id(),
				'name'         => $p->get_name(),
				'content_type' => 'product',
				'quantity'     => (int) $line['quantity'],
				'amount'       => cgpt_cents( $p->get_price() ),
				'currency'     => get_woocommerce_currency(),
			);
		}
		cgpt_emit( 'checkout_started', array(
			'type'     => 'contents',
			'amount'   => cgpt_cents( WC()->cart->get_total( 'edit' ) ),
			'currency' => get_woocommerce_currency(),
			'contents' => $items,
		) );
	}

	// 3. Ordine concluso
	if ( function_exists( 'is_order_received_page' ) && is_order_received_page() ) {
		$order = cgpt_current_order();
		if ( $order ) {
			cgpt_emit(
				'order_created',
				array(
					'type'     => 'contents',
					'amount'   => cgpt_cents( $order->get_total() ),
					'currency' => $order->get_currency(),
					'contents' => cgpt_order_contents( $order ),
				),
				cgpt_event_id( $order )   // stesso valore che userà la Conversions API
			);
		}
	}
}

/**
 * Importi in unità minore ISO 4217: 248,00 EUR -> 24800.
 * Per le valute senza decimali (JPY, KRW) restituire l'importo così com'è.
 */
function cgpt_cents( $amount ) {
	return (int) round( (float) $amount * 100 );
}

function cgpt_event_id( WC_Order $order ) {
	return 'wc_' . $order->get_id();
}

function cgpt_order_contents( WC_Order $order ) {
	$out = array();
	foreach ( $order->get_items() as $item ) {
		$qty   = max( 1, (int) $item->get_quantity() );
		$out[] = array(
			'id'           => (string) $item->get_product_id(),
			'name'         => $item->get_name(),
			'content_type' => 'product',
			'quantity'     => $qty,
			'amount'       => cgpt_cents( (float) $item->get_total() / $qty ),
			'currency'     => $order->get_currency(),
		);
		// group_id e variant_dict NON vanno qui: sono "Conversions API only".
	}
	return $out;
}

/**
 * Recupera l'ordine della pagina di conferma.
 *
 * Si legge da $wp->query_vars come fa WooCommerce stesso: la chiave interna è
 * sempre 'order-received' anche quando lo slug pubblico dell'endpoint è stato
 * personalizzato nelle impostazioni.
 *
 * Il controllo sulla order key serve perché wp_footer viene eseguito comunque:
 * senza, chiunque indovini un ID ordine farebbe partire un evento con i dati
 * di un ordine altrui.
 */
function cgpt_current_order() {
	global $wp;
	$order_id = isset( $wp->query_vars['order-received'] )
		? absint( $wp->query_vars['order-received'] )
		: 0;
	if ( ! $order_id ) {
		return null;
	}
	$order = wc_get_order( $order_id );
	if ( ! $order ) {
		return null;
	}
	$key = isset( $_GET['key'] ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : '';
	return hash_equals( (string) $order->get_order_key(), (string) $key ) ? $order : null;
}

function cgpt_emit( $event, array $data, $event_id = '' ) {
	$opts = $event_id ? array( 'event_id' => $event_id ) : array();
	printf(
		'<script>window.cgptMeasure && cgptMeasure(%s, %s, %s);</script>' . "\n",
		wp_json_encode( $event ),
		wp_json_encode( $data ),
		wp_json_encode( (object) $opts )
	);
}

File 5 — Conversions API: la conversione mandata dal server

Serve solo con WooCommerce, e richiede i file 1, 2 e 4. Prima di incollare, le tre domande che ricevo sempre.

È obbligatorio?

No. Il File 4 manda già order_created dal browser e la campagna funziona. Ma il browser è il canale che si perde più facilmente: ad blocker, pagina chiusa prima del render, un errore JavaScript causato da un altro plugin, la connessione mobile che cade sul più bello. La documentazione OpenAI indica la Conversions API come la fonte di tracciamento più affidabile, ed è quella che recupera gli ordini che il pixel non riesce a raccontare. Su uno shop con volumi, saltarla significa sottostimare le conversioni e far ottimizzare la campagna su dati parziali.

Dove lo “sparo”? Va nella thank you page?

No, e questa è la differenza che confonde di più: non è uno script di pagina. Non va nel tema, non va nella thank you page, non va in Google Tag Manager. È codice che gira sul tuo server, dentro WordPress, nel momento in cui WooCommerce segna l’ordine come pagato. Il browser del cliente non è coinvolto: potrebbe essere già chiuso.

Come si incastra con la thank you page?

Così:

  1. il cliente atterra sulla pagina di conferma → il File 4 manda order_created dal browser con event_id: wc_1042;
  2. WooCommerce registra il pagamento → il File 5 manda lo stesso order_created dal server con id: wc_1042;
  3. OpenAI riceve due eventi con lo stesso identificativo e ne conta uno solo.

Non sono due conversioni: è la stessa conversione mandata per due strade, così che ne basti una che arrivi. Il momento in cui parte l’invio dal server dipende dal metodo di pagamento — con carta è quasi immediato, con bonifico o con notifiche asincrone può arrivare ore dopo. Va bene comunque: la finestra ammessa è di sette giorni.

Il codice

Prima, in wp-config.php, sopra la riga /* That's all, stop editing! */:

define( 'CGPT_CAPI_KEY', 'la-tua-conversion-api-key' );
define( 'CGPT_CAPI_TEST', false );  // true = valida senza salvare (validate_only)

Poi wp-content/mu-plugins/chatgpt-ads-capi.php:

<?php
/**
 * Plugin Name: ChatGPT Ads - Conversions API
 * Description: Invia order_created server-side, deduplicato con l'evento del pixel.
 * Richiede: chatgpt-ads-pixel.php, chatgpt-ads-oppref.php, chatgpt-ads-woo.php
 */

defined( 'ABSPATH' ) || exit;

// 1. Al checkout lega oppref e obref all'ordine: quando partirà l'invio,
//    il browser del cliente non ci sarà più.
add_action( 'woocommerce_checkout_create_order', 'cgpt_attach_refs', 10, 2 );
function cgpt_attach_refs( $order, $data ) {
	if ( function_exists( 'cgpt_oppref' ) && cgpt_oppref() ) {
		$order->update_meta_data( '_cgpt_oppref', cgpt_oppref() );
	}
	if ( function_exists( 'cgpt_obref' ) && cgpt_obref() ) {
		$order->update_meta_data( '_cgpt_obref', cgpt_obref() );
	}
	// IP e user agent non servono: WooCommerce li salva già sull'ordine.
}

// 2. Invio quando l'ordine diventa pagato. Più hook, un solo invio:
//    ci pensano il guard in memoria e quello su meta.
add_action( 'woocommerce_payment_complete', 'cgpt_send_conversion' );
add_action( 'woocommerce_order_status_processing', 'cgpt_send_conversion' );
add_action( 'woocommerce_order_status_completed', 'cgpt_send_conversion' );

function cgpt_send_conversion( $order_id ) {
	static $done = array();

	if ( ! defined( 'CGPT_CAPI_KEY' ) || ! function_exists( 'cgpt_pixel_ready' ) || ! cgpt_pixel_ready() ) {
		return;
	}
	if ( isset( $done[ $order_id ] ) ) {
		return;                       // già inviato in questa stessa richiesta
	}
	$order = wc_get_order( $order_id );
	if ( ! $order || $order->get_meta( '_cgpt_capi_sent' ) ) {
		return;                       // già inviato in una richiesta precedente
	}
	$done[ $order_id ] = true;

	// Finestra ammessa: ultimi 7 giorni, max +10 minuti nel futuro.
	$paid = $order->get_date_paid() ? $order->get_date_paid()->getTimestamp() : time();
	if ( $paid < time() - 7 * DAY_IN_SECONDS ) {
		$order->update_meta_data( '_cgpt_capi_sent', 'skipped-too-old' );
		$order->save();
		return;
	}

	$user = array(
		'ip_address' => (string) $order->get_customer_ip_address(),
		'user_agent' => (string) $order->get_customer_user_agent(),
	);
	if ( $order->get_meta( '_cgpt_obref' ) ) {
		$user['obref'] = $order->get_meta( '_cgpt_obref' );
	}
	if ( $order->get_billing_email() ) {
		// Email: trim + lowercase, poi SHA-256 esadecimale minuscolo.
		$user['emails_sha256'] = array( hash( 'sha256', strtolower( trim( $order->get_billing_email() ) ) ) );
	}
	if ( $order->get_billing_country() ) {
		$user['countries'] = array( $order->get_billing_country() );
	}
	if ( $order->get_billing_city() ) {
		$user['cities'] = array( $order->get_billing_city() );
	}
	if ( $order->get_billing_postcode() ) {
		$user['postal_codes'] = array( $order->get_billing_postcode() );
	}

	$event = array(
		'id'            => cgpt_event_id( $order ),   // identico all'event_id del pixel
		'type'          => 'order_created',
		'timestamp_ms'  => $paid * 1000,
		'source_url'    => $order->get_checkout_order_received_url(),
		'action_source' => 'web',
		'user'          => array_filter( $user ),
		'data'          => array(
			'type'     => 'contents',
			'amount'   => cgpt_cents( $order->get_total() ),
			'currency' => $order->get_currency(),
			'contents' => cgpt_order_contents( $order ),
		),
	);
	if ( $order->get_meta( '_cgpt_oppref' ) ) {
		$event['oppref'] = $order->get_meta( '_cgpt_oppref' ); // livello evento, non dentro user
	}

	$response = wp_remote_post(
		add_query_arg( 'pid', rawurlencode( CGPT_PIXEL_ID ), 'https://bzr.openai.com/v1/events' ),
		array(
			'timeout' => 15,
			'headers' => array(
				'Authorization' => 'Bearer ' . CGPT_CAPI_KEY,
				'Content-Type'  => 'application/json',
			),
			'body'    => wp_json_encode(
				array(
					'validate_only'      => (bool) ( defined( 'CGPT_CAPI_TEST' ) && CGPT_CAPI_TEST ),
					'integration_source' => 'wordpress-woocommerce',
					'events'             => array( $event ),
				)
			),
		)
	);

	$code = is_wp_error( $response ) ? 0 : (int) wp_remote_retrieve_response_code( $response );
	if ( $code >= 200 && $code < 300 ) {
		$order->update_meta_data( '_cgpt_capi_sent', gmdate( 'c' ) );
	} else {
		$order->add_order_note(
			'ChatGPT Ads CAPI fallita: ' .
			( is_wp_error( $response ) ? $response->get_error_message() : $code . ' ' . wp_remote_retrieve_body( $response ) )
		);
	}
	$order->save();
}

Quattro cose che questo file fa e che di solito mancano nelle implementazioni fai-da-te: non invia due volte lo stesso ordine (guard in memoria più guard su meta), non prova nemmeno a inviare ordini fuori dalla finestra dei sette giorni, scrive l’errore nelle note dell’ordine invece di fallire in silenzio, e per IP e user agent usa quelli che WooCommerce salva già sull’ordine invece di duplicarli in meta custom.

File 6 — Lead da form, con e senza pagina di ringraziamento

Serve se raccogli contatti: da solo su un sito di servizi, oppure in aggiunta a WooCommerce se hai anche form di preventivo.

L’evento è lead_created con data.type: "customer_action". Attenzione a una regola che si sbaglia spesso: quella shape non accetta contents[]plan_id — solo type, e facoltativamente amount e currency. Infilarci l’elenco dei servizi richiesti è fuori specifica.

I form WordPress si comportano in due modi diversi, e il codice cambia di conseguenza. Guarda cosa succede dopo l’invio sul tuo sito e scegli il caso corrispondente.

Caso A — Il form invia senza ricaricare la pagina (AJAX)

È il comportamento predefinito di Contact Form 7 e di molti altri: il messaggio “Grazie” compare al posto del form e l’URL non cambia. Si ascolta l’evento che il plugin emette a invio riuscito.

<script>
// Contact Form 7. Nota: questi eventi partono SOLO in modalità AJAX.
document.addEventListener("wpcf7mailsent", function (e) {
  // Per limitarlo a un form specifico:  if (e.detail.contactFormId !== 123) return;
  if (window.cgptMeasure) {
    cgptMeasure("lead_created", { type: "customer_action" });
  }
}, false);
</script>

Per Gravity Forms l’evento è gform_confirmation_loaded, per WPForms quello documentato dal plugin: cambia il nome dell’evento, il resto è identico.

Caso B — Il form fa un redirect a una pagina di ringraziamento

È il caso più comune sui siti aziendali: dopo l’invio l’utente atterra su /grazie/. Qui non c’è nessun evento JavaScript da ascoltare, perché la pagina si ricarica da zero: l’evento va sparato sulla pagina di arrivo, riconoscendola dal suo ID.

wp-content/mu-plugins/chatgpt-ads-lead.php

<?php
/**
 * Plugin Name: ChatGPT Ads - Lead su thank you page
 * Description: Invia lead_created sulle pagine di ringraziamento dei form.
 * Richiede: chatgpt-ads-pixel.php
 */

defined( 'ABSPATH' ) || exit;

// ID delle pagine di ringraziamento. L'ID si legge nell'URL quando modifichi
// la pagina: wp-admin/post.php?post=123&action=edit  ->  123
const CGPT_THANKYOU_PAGES = array( 123, 456 );

add_action( 'wp_footer', 'cgpt_lead_thankyou', 20 );
function cgpt_lead_thankyou() {
	if ( ! is_page( CGPT_THANKYOU_PAGES ) ) {
		return;
	}
	?>
<script>
(function () {
  // Un refresh della pagina, o un ritorno con il tasto indietro,
  // non devono contare un secondo lead.
  var k = "cgpt_lead_" + location.pathname;
  try {
    if (sessionStorage.getItem(k)) { return; }
    sessionStorage.setItem(k, "1");
  } catch (e) { /* sessionStorage non disponibile: si procede comunque */ }

  if (window.cgptMeasure) {
    cgptMeasure("lead_created", { type: "customer_action" });
  }
})();
</script>
	<?php
}

Il limite da conoscere prima, non dopo: una pagina di ringraziamento è raggiungibile anche senza aver compilato il form — da un link, dalla cronologia, da un bot. Il conteggio dei lead sarà quindi un po’ più generoso del reale. Il guard su sessionStorage elimina i refresh, non le visite dirette. Se ti serve un conteggio esatto, la strada è il Caso C.

Caso C — Lead inviato dal server (il più affidabile)

Parte dal tuo server quando il form viene realmente inviato: niente falsi positivi, niente ad blocker, e funziona anche con i form che non usano AJAX. L’esempio è su Contact Form 7, ma l’aggancio vale per qualsiasi plugin che esponga un hook a invio riuscito.

<?php
/**
 * Plugin Name: ChatGPT Ads - Lead server-side
 * Description: Invia lead_created via Conversions API quando il form viene inviato.
 * Richiede: chatgpt-ads-pixel.php, chatgpt-ads-oppref.php
 */

defined( 'ABSPATH' ) || exit;

add_action( 'wpcf7_mail_sent', 'cgpt_lead_from_cf7' );
function cgpt_lead_from_cf7( $form ) {
	cgpt_send_lead( 'cf7_' . $form->id() . '_' . time() );
}

function cgpt_send_lead( $event_id ) {
	if ( ! defined( 'CGPT_CAPI_KEY' ) || ! function_exists( 'cgpt_pixel_ready' ) || ! cgpt_pixel_ready() ) {
		return;
	}
	$user = array_filter(
		array(
			'obref'      => function_exists( 'cgpt_obref' ) ? cgpt_obref() : '',
			'ip_address' => isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '',
			'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '',
		)
	);

	$event = array(
		'id'            => $event_id,
		'type'          => 'lead_created',
		'timestamp_ms'  => time() * 1000,
		'source_url'    => wp_get_referer() ? wp_get_referer() : home_url( '/' ),
		'action_source' => 'web',
		'user'          => $user,
		'data'          => array( 'type' => 'customer_action' ),
	);
	$oppref = function_exists( 'cgpt_oppref' ) ? cgpt_oppref() : '';
	if ( $oppref ) {
		$event['oppref'] = $oppref;
	}

	wp_remote_post(
		add_query_arg( 'pid', rawurlencode( CGPT_PIXEL_ID ), 'https://bzr.openai.com/v1/events' ),
		array(
			'timeout'  => 10,
			'blocking' => false,          // non rallentare la risposta all'utente
			'headers'  => array(
				'Authorization' => 'Bearer ' . CGPT_CAPI_KEY,
				'Content-Type'  => 'application/json',
			),
			'body'     => wp_json_encode(
				array(
					'validate_only'      => (bool) ( defined( 'CGPT_CAPI_TEST' ) && CGPT_CAPI_TEST ),
					'integration_source' => 'wordpress-forms',
					'events'             => array( $event ),
				)
			),
		)
	);
}

Scegline uno. Per i lead: browser oppure server, mai entrambi. A differenza degli ordini WooCommerce, qui non c’è un identificativo condiviso fra i due canali, quindi attivarli tutti e due significa contare ogni contatto due volte.

Un’ultima cosa se il lead è l’obiettivo della campagna: una campagna oCPC richiede esattamente un evento standard come obiettivo, e non può essere un evento custom.

Le esclusioni cache obbligatorie

Salta questa sezione e il tracciamento funzionerà in staging e non in produzione. Sono cinque righe di configurazione.

CosaPerchéDove si imposta
Non mettere in cache le pagine con ?oppref=Se serve HTML dalla cache, PHP non gira e il cookie non viene scrittoWP Rocket: Never Cache URLs · LiteSpeed: Do Not Cache Query Strings
Non rimuovere oppref e gli utm_* dalla query stringAlcuni plugin “puliscono” gli URL e li perdonoImpostazioni del plugin di cache o di ottimizzazione
Mai in cache la thank you pageServiresti a tutti l’ordine di qualcun altroWooCommerce lo fa di suo: verifica che nessun plugin lo sovrascriva
Su Cloudflare: query string nella cache keyCon Ignore Query String la pagina viene servita staticaCaching → Cache Key, o una Page Rule di bypass
Nessun redirect che perda la query stringhttp→https, non-www→www, trailing slashVerifica con il curl più avanti

UTM e GA4

Nell’URL di destinazione della campagna, in Ads Manager:

https://www.tuosito.it/pagina
  ?utm_source=chatgpt
  &utm_medium=cpc
  &utm_campaign=brand_q4_2026_it_tema
  &utm_content=id_annuncio

utm_medium=cpc è la scelta corretta: rende il traffico riconoscibile come a pagamento in GA4. Il rovescio è che lo esclude dal canale nativo AI Assistant, che richiede medium esattamente ai-assistant. Si recupera con un gruppo di canali personalizzato — il ragionamento completo è nella guida tecnica.

In GA4, tre verifiche da tre minuti:

  1. Amministrazione → Stream di dati → Elenca i referral indesiderati: chatgpt.com non deve esserci.
  2. Amministrazione → Impostazioni dati → Gruppi di canali: crea un canale AI Paid con Source che corrisponde a chatgpt e Medium che corrisponde all’espressione regolare dei medium a pagamento.
  3. Report → Tempo reale, con la dimensione Gruppo di canali predefinito della sessione: apri la landing in incognito con gli UTM e guarda dove finisce.

Sulla costruzione dei parametri vale quanto scritto nella guida ai parametri UTM.

CSP, se ce l’hai

Se hai una Content Security Policy attiva — plugin di security headers, o header impostati dal server — il pixel viene bloccato senza errori visibili finché non autorizzi i domini OpenAI.

Nginx:

add_header Content-Security-Policy "script-src 'self' https://bzrcdn.openai.com; connect-src 'self' https://bzr.openai.com https://bzrcdn.openai.com; img-src 'self' data: https://bzr.openai.com" always;

Apache, in .htaccess:

Header set Content-Security-Policy "script-src 'self' https://bzrcdn.openai.com; connect-src 'self' https://bzr.openai.com https://bzrcdn.openai.com; img-src 'self' data: https://bzr.openai.com"

Le direttive sono da integrare nella tua policy esistente, non da incollare al posto. E non aggiungere 'unsafe-inline' per far passare lo snippet: usa un nonce.

Sequenza in quattro passi sulla thank you page: la CMP risolve il consenso, la coda si svuota, parte order_created dal pixel e poi dalla Conversions API con lo stesso id, che deduplica la conversione

Verifica in dieci minuti

Nell’ordine. Se un passo fallisce, gli altri non hanno senso.

1. oppref sopravvive ai redirect

curl -sIL "https://www.tuosito.it/?oppref=TEST123&utm_source=chatgpt&utm_medium=cpc" \
  | grep -iE '^(HTTP/|location:)'
# Atteso: nessun redirect, oppure ogni Location che riporta ?oppref=TEST123

2. Il cookie server viene scritto

curl -sI "https://www.tuosito.it/?oppref=TEST123" | grep -i set-cookie
# Atteso: set-cookie: cgpt_oppref=TEST123; ...

3. Il pixel carica. Apri la landing con ?oppref=TEST123, DevTools → Network, filtra openai: devi vedere oaiq.min.js da bzrcdn.openai.com. Se non c’è, è la CSP o un ad blocker.

4. Il consenso commuta. In console, prima di accettare i cookie: window.cgptConsent deve essere null. Dopo l’accettazione true, e window.cgptQueue.length deve tornare a 0. Dopo la revoca, di nuovo false.

5. La conversione parte. Fai un ordine reale di prova, o invia un form. In Network cerca la richiesta verso bzr.openai.com.

6. La Conversions API risponde. Metti CGPT_CAPI_TEST a true, fai un ordine, apri l’ordine in WooCommerce e guarda le note: se non c’è nessuna nota di errore, la chiamata è andata. Poi rimettilo a false.

7. Il valore è giusto. In Ads Manager → Conversions → View event stream, cerca l’ordine di prova: amount deve essere l’importo × 100. È l’unico controllo che intercetta l’errore più costoso di tutti.

8. GA4 vede il traffico. Tempo reale, dimensione Gruppo di canali predefinito della sessione.

Problemi frequenti

SintomoCausa quasi sempreCosa guardare
Zero conversioni ma il sito vendeoppref perso in un redirect o nella cacheTest 1 e 2
Conversioni doppieevent_id del pixel diverso dall’id CAPIChe entrambi usino cgpt_event_id()
Lead contati il doppioAttivi sia il Caso B sia il Caso CTienine uno solo
Valori 100 volte troppo bassiImporto inviato in euro invece che in centesimiTest 7
Il pixel non caricaCSP, o un ad blocker sul tuo browserConsole, violazioni CSP
Funziona da loggato, non da anonimoPage cache attiva per gli anonimiEsclusioni cache
Eventi solo sulla homePlugin di cache che serve HTML staticoEsclusioni cache
Nessun evento dopo il consensoLa CMP non chiama cgptGrant()window.cgptConsent in console
Note d’ordine con errore CAPIPayload rifiutatoIl testo della nota dice quale campo
Nessun order_created sulla confermaURL della thank you page senza parametro keyChe l’URL contenga &key=wc_order_...
Ordini vecchi mai inviatiOltre la finestra dei sette giorniMeta _cgpt_capi_sent = skipped-too-old

Casi che questa guida non copre

  • Checkout su un dominio diverso (booking engine, gestionale esterno, piattaforma di ticketing): il cookie non attraversa il confine di dominio e va propagato o ricongiunto lato server.
  • WordPress headless: il pixel va nel frontend, la Conversions API resta dove vive l’ordine.
  • Conversioni offline o confermate oltre sette giorni: fuori dalla finestra ammessa, richiedono un disegno diverso.

Sono i tre casi in cui l’attribuzione si rompe in silenzio: la meccanica e le mitigazioni stanno nella guida tecnica al tracciamento delle ChatGPT Ads.

Se preferisci non metterci mano

Progettiamo la misurazione, affianchiamo gli sviluppatori durante l’implementazione e validiamo tutto prima del go-live — oltre a gestire le campagne.

Consulenza e gestione ChatGPT Ads — audit dell’account, disegno del tracciamento, gestione campagne.

Fonti

ArgomentoURL
Measurement Pixeldevelopers.openai.com/ads/measurement-pixel
Conversions APIdevelopers.openai.com/ads/conversions-api
Supported Eventsdevelopers.openai.com/ads/supported-events
Campagne oCPCdevelopers.openai.com/ads/conversion-optimized-campaigns
Gruppo di canali GA4support.google.com/analytics/answer/9756891
Eventi DOM di Contact Form 7contactform7.com/dom-events
Consenso: Complianzcomplianz.io — developers guide
Consenso: Cookiebotcookiebot.com — developer
Consenso: iubendaiubenda.com — Cookie Solution JavaScript API

Autore

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Ultimi articoli aggiornati

21 min lettura

I crawler di Meta e Amazon generano milioni di richieste senza back-off: come quantificarli nei log, verificarne l'autenticità e fermarli con cache, robots.txt, rate limiting e WAF, senza perdere i link preview.
1 mi piace
29 min lettura

Determinare il valore di un dominio richiede l'analisi di metriche SEO, storico di registrazione e potenziale commerciale. Metodologie tecniche per la valutazione di domini premium e exact match, essenziali per acquisizioni aziendali, investimenti e protezione del brand.
1 mi piace

Richiedi un preventivo SEO e Google Ads

Porta il tuo sito web al livello successivo con l’esperienza di EVE Milano. La nostra agenzia di Search Marketing ha ricevuto dal 2010 oltre 1.400 richieste di preventivo, un segnale chiaro della fiducia che webmaster, imprenditori e manager ripongono nella nostra specializzazione tecnica e verticale nella SEO e PPC. Se la tua organizzazione cerca competenze specifiche per emergere nei risultati di Google e chatbot AI, noi siamo pronti a fornire quel valore aggiunto. Richiedi un preventivo ora e scopri la differenza tra noi e gli altri.
Richiedi un preventivo

Vuoi ricevere un avviso al mese con le nuove guide pubblicate?

Iscriviti alla newsletter!