Si haces publicidad en ChatGPT (OpenAI Ads) desde tu tienda Shopify, hay un problema de fondo antes de optimizar nada: la plataforma no sabe qué pasa después del clic. ¿Compró? ¿Por cuánto? ¿Quién es? Sin esa información no hay campaña que aprenda ni presupuesto que se asigne bien. En esta guía instalamos el primero de los dos canales que resuelven esto — el Píxel del navegador — de la forma correcta: dentro de Eventos de clientes de Shopify, no pegado en el tema.
Mira el vídeo completo con la instalación en pantalla, o sigue el paso a paso escrito debajo:El problema de fondo: medir la conversión
Para que OpenAI Ads pueda optimizar campañas, necesita saber qué pasa después de que alguien hace clic en un anuncio. A eso se le llama conversión, y para medirla bien hacen falta dos canales que se complementan:- El Píxel (navegador): un script que corre en el ordenador o móvil del visitante. Es el que cubre esta guía.
- La Conversions API / CAPI (servidor): una llamada que hace directamente el servidor a OpenAI. Se cubre en la Parte 2 de esta serie.
Paso 0 — Crear el Pixel en OpenAI Ads Manager
Antes de tocar Shopify, hace falta el pixel dado de alta en la cuenta de anuncios: en Ads Manager ve a Conversiones → Fuente de datos → Crear y genera un pixel nuevo. Copia el «ID del pixel» — lo necesitarás en el paso siguiente.Paso 1 — Instalar el Custom Pixel en Shopify
Dónde se instala
En el admin de Shopify: Configuración → Eventos de clientes → Add custom pixel.- Ponle un nombre identificable, por ejemplo «ChatGPT Ads».
- Pulsa «Add pixel» para crearlo (queda vacío, listo para configurar).
- Verás dos secciones antes del código: «Permission» y «Data sale».
Qué marcar en Privacidad del cliente
Estas opciones determinan cuándo se le permite al pixel recopilar datos, respetando el consentimiento del visitante (RGPD en la UE, CCPA en California):| Sección | Opción | Qué marcar |
|---|---|---|
| Permiso | Obligatorio | Marcado — el pixel solo actúa con el permiso del visitante |
| Permiso | Marketing | Sí — categoría correcta para atribución publicitaria |
| Permiso | Informes y estadísticas | No — este pixel no es analítica interna |
| Permiso | Preferencias | No aplica |
| Venta de datos | «Los datos califican como venta de datos» | Sí — clasificación estándar para pixeles de terceros |
El código a pegar
Baja hasta el campo «Code» del propio Custom Pixel y pega el contenido completo del Anexo, al final de este artículo. Antes de guardar, sustituye un único valor:<TU-PIXEL-ID> por el Pixel ID copiado en el Paso 0. El resto del código no necesita ningún cambio — ya reconoce automáticamente vistas de página, vistas de producto, añadidos al carrito, inicio de checkout y compra completada.Qué cubre este pixel
| Evento de Shopify | Evento enviado a OpenAI | Qué mide |
|---|---|---|
| page_viewed | page_viewed | Visitas a páginas |
| product_viewed | contents_viewed | Alguien mira una ficha de producto |
| product_added_to_cart | items_added | Añadir al carrito |
| checkout_started | checkout_started | Empieza el proceso de pago |
| checkout_completed | order_created | Compra confirmada |
Cómo mejora la coincidencia de usuario (email, teléfono, nombre)
Cuando alguien navega sin haber iniciado sesión — la inmensa mayoría de los visitantes — no hay ningún dato de usuario que enviar todavía. Por eso el código refuerza el objeto «user» en dos momentos clave del checkout:- En checkout_started: en cuanto Shopify expone email, teléfono o dirección de quien paga, se hashean en SHA-256 y se reenvían.
- En checkout_completed: se repite el refuerzo, más un external_id (el ID interno de cliente de Shopify) que solo existe aquí, porque el pedido no se crea hasta que la compra se confirma.
Paso 2 — Comprobar que funciona
El propio Custom Pixel incluye un «Pixel Helper» que muestra en tiempo real qué eventos se han recibido mientras navegas la tienda:- Abre la tienda en una pestaña nueva: entra a una página de producto, añade algo al carrito, empieza el checkout.
- Con el Pixel Helper abierto, confirma que aparecen page_viewed, product_viewed, items_added y checkout_started según avanzas.
- Completa un pedido de prueba (pago manual, sin coste real) y confirma que aparece checkout_completed en la página de agradecimiento.
- En paralelo, en Ads Manager → Conversiones → Flujo de eventos, con «Iniciar sondeo» activo, deberías ver los mismos eventos llegando por el canal pixel_sdk.
Preguntas frecuentes
¿Puedo instalar el pixel de ChatGPT Ads pegándolo en theme.liquid?
No. El checkout de Shopify no forma parte del tema, así que ese método nunca llega a la página de pago y no respeta el consentimiento de cookies. El único método correcto es un Custom Pixel creado desde Configuración → Eventos de clientes.¿Hace falta crear un «evento de conversión» en Ads Manager antes de instalar el pixel?
No. Ese paso solo sirve para poder seleccionar el evento al configurar una campaña. La verificación del pixel mediante «Flujo de eventos» capta los datos en bruto, con o sin un evento de conversión creado.¿Por qué el pixel del navegador no es suficiente por sí solo?
Porque bloqueadores de anuncios, Safari o el rechazo de cookies pueden impedir que se dispare, aunque el visitante compre. Combinarlo con una Conversions API del lado del servidor (Parte 2 de esta serie) cubre esos casos y da el dato más completo posible.¿Qué pasa si el external_id no aparece en un pedido?
No es un error del código. Un comprador invitado que nunca crea una cuenta a veces no genera un «customer» en Shopify asociado a ese pedido concreto, así que no hay ID que hashear. El email sí debería aparecer en la gran mayoría de los pedidos.Anexo — Código completo del Custom Pixel
var OPENAI_PIXEL_ID = "<TU-PIXEL-ID>"; y sustituye <TU-PIXEL-ID> (con las comillas incluidas) por el Pixel ID copiado en el Paso 0. Es la única línea que cambia en todo el archivo./**
* Custom Pixel de Shopify — ChatGPT Ads (OpenAI Ads)
* Instalar en: Shopify Admin > Settings > Customer events > Add custom pixel
*
* Sustituye <TU-PIXEL-ID> por el Pixel ID real de tu cuenta de Ads Manager
* (Conversiones > Fuente de datos > tu pixel > "ID del pixel").
*
* IMPORTANTE: en un Custom Pixel creado desde el Admin, `analytics`, `browser`
* e `init` ya existen como variables GLOBALES del sandbox — NO se usa
* `register(...)` (eso es solo para App Pixel Extensions distribuidas como
* apps de Shopify). Tampoco hay acceso al `oaiq` de la página principal
* (el sandbox está aislado), así que el SDK hay que cargarlo aquí dentro.
*/
var OPENAI_PIXEL_ID = "<TU-PIXEL-ID>";
var OPENAI_DEBUG = true; // pon esto a false cuando ya esté todo confirmado
// ---------------------------------------------------------------------
// 0. Cargar el SDK de OpenAI dentro del propio sandbox del Custom Pixel
// ---------------------------------------------------------------------
(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");
// ---------------------------------------------------------------------
// 1. Utilidades
// ---------------------------------------------------------------------
async function sha256Hex(value) {
if (!value) return undefined;
var enc = new TextEncoder();
var buf = await crypto.subtle.digest("SHA-256", enc.encode(value));
return Array.prototype.map
.call(new Uint8Array(buf), function (b) { return b.toString(16).padStart(2, "0"); })
.join("");
}
function normalizeEmail(email) {
return email ? email.trim().toLowerCase() : undefined;
}
// Deja solo dígitos, quita ceros/+ iniciales, tal como pide OpenAI para el teléfono.
function normalizePhone(phone) {
if (!phone) return undefined;
var digits = phone.replace(/[\s().-]/g, "").replace(/^\+/, "").replace(/^0+/, "");
return digits || undefined;
}
// Minúsculas, sin espacios ni puntuación, conservando acentos/ñ tal como pide OpenAI.
function normalizeName(name) {
if (!name) return undefined;
return name.toLowerCase().replace(/[^\p{L}\p{N}]/gu, "") || undefined;
}
function toMinorUnits(amount) {
var n = parseFloat(amount);
return isFinite(n) ? Math.round(n * 100) : undefined;
}
function extractNumericOrderId(rawId) {
if (!rawId) return undefined;
var match = String(rawId).match(/(\d+)$/);
return match ? match[1] : String(rawId);
}
// Extrae el precio unitario de una variante, venga de donde venga (product_viewed, cartLine, lineItem de checkout).
function unitPriceOf(item) {
var variant = item.variant || item.merchandise || {};
return variant.price || {};
}
function toContentItem(item) {
var variant = item.variant || item.merchandise || {};
var product = variant.product || {};
var price = unitPriceOf(item);
return {
id: String(variant.sku || variant.id || item.id || ""),
name: product.title || variant.title || item.title || "",
content_type: "product",
quantity: item.quantity || 1,
amount: toMinorUnits(price.amount),
currency: price.currencyCode,
};
}
/**
* Construye el objeto "user" hasheado a partir de los datos de checkout
* (email, teléfono, nombre, dirección) y/o de un cliente ya identificado.
* Devuelve {} si no hay ningún dato disponible.
*/
async function buildUserObject(customer, checkout) {
var address = (checkout && (checkout.billingAddress || checkout.shippingAddress)) || {};
var email = normalizeEmail((checkout && checkout.email) || (customer && customer.email));
var phone = normalizePhone((checkout && checkout.phone) || (customer && customer.phone));
var firstName = normalizeName(address.firstName || (customer && customer.firstName));
var lastName = normalizeName(address.lastName || (customer && customer.lastName));
var externalId = customer && customer.id ? String(customer.id) : undefined;
var user = {};
var emailHash = await sha256Hex(email);
var phoneHash = await sha256Hex(phone);
var firstNameHash = await sha256Hex(firstName);
var lastNameHash = await sha256Hex(lastName);
var idHash = await sha256Hex(externalId);
if (emailHash) user.email_sha256 = emailHash;
if (phoneHash) user.phone_number_sha256 = phoneHash;
if (firstNameHash) user.first_name_sha256 = firstNameHash;
if (lastNameHash) user.last_name_sha256 = lastNameHash;
if (idHash) user.external_id_sha256 = idHash;
if (address.countryCode) user.country = address.countryCode;
if (address.city) user.city = address.city;
if (address.zip) user.postal_code = address.zip;
return user;
}
/** Llama a init otra vez con los datos de usuario ya disponibles (no hace falta repetir pixelId). */
async function refreshUserData(customer, checkout) {
var user = await buildUserObject(customer, checkout);
if (Object.keys(user).length) {
oaiq("init", { pixelId: OPENAI_PIXEL_ID, user: user });
}
}
// ---------------------------------------------------------------------
// 2. Inicialización del pixel, con datos de usuario si ya se conocen
// (solo cubre al visitante que YA está logueado al cargar la página)
// ---------------------------------------------------------------------
(async function initPixel() {
var customer = (init.data && init.data.customer) || {};
var user = await buildUserObject(customer, null);
var payload = { pixelId: OPENAI_PIXEL_ID, debug: OPENAI_DEBUG };
if (Object.keys(user).length) payload.user = user;
oaiq("init", payload);
})();
// ---------------------------------------------------------------------
// 3. Vistas de página y de producto
// ---------------------------------------------------------------------
analytics.subscribe("page_viewed", function () {
oaiq("measure", "page_viewed", { type: "contents" });
});
analytics.subscribe("product_viewed", function (event) {
var v = event.data.productVariant || {};
oaiq("measure", "contents_viewed", {
type: "contents",
amount: toMinorUnits(v.price && v.price.amount),
currency: v.price && v.price.currencyCode,
contents: [toContentItem({ variant: v, quantity: 1 })],
});
});
// ---------------------------------------------------------------------
// 4. Añadir al carrito
// ---------------------------------------------------------------------
analytics.subscribe("product_added_to_cart", function (event) {
var cartLine = event.data.cartLine || {};
var totalAmount = cartLine.cost && cartLine.cost.totalAmount;
oaiq("measure", "items_added", {
type: "contents",
amount: toMinorUnits(totalAmount && totalAmount.amount),
currency: totalAmount && totalAmount.currencyCode,
contents: [toContentItem(cartLine)],
});
});
// ---------------------------------------------------------------------
// 5. Checkout iniciado — AQUÍ es donde por fin se conoce al comprador,
// aunque sea invitado, así que aprovechamos para reforzar el "user"
// ---------------------------------------------------------------------
analytics.subscribe("checkout_started", async function (event) {
var checkout = event.data.checkout || {};
var total = checkout.totalPrice;
await refreshUserData(null, checkout);
oaiq("measure", "checkout_started", {
type: "contents",
amount: toMinorUnits(total && total.amount),
currency: total && total.currencyCode,
contents: (checkout.lineItems || []).map(toContentItem),
});
});
// ---------------------------------------------------------------------
// 6. Compra completada — con event_id compartido con el Worker, y con
// los datos de usuario reforzados otra vez por si cambiaron
// ---------------------------------------------------------------------
analytics.subscribe("checkout_completed", async function (event) {
var checkout = event.data.checkout || {};
var total = checkout.totalPrice;
// El customer.id solo existe aquí dentro de checkout.order (no en checkout_started,
// porque el pedido todavía no se ha creado en ese punto del funnel).
var orderCustomer = (checkout.order && checkout.order.customer) || null;
await refreshUserData(orderCustomer, checkout);
var rawOrderId = checkout.order && checkout.order.id;
var orderId = extractNumericOrderId(rawOrderId);
var eventId = orderId ? "order_" + orderId : undefined;
oaiq(
"measure",
"order_created",
{
type: "contents",
amount: toMinorUnits(total && total.amount),
currency: total && total.currencyCode,
contents: (checkout.lineItems || []).map(toContentItem),
},
eventId ? { event_id: eventId } : {}
);
});Serie ChatGPT Ads en Shopify · Parte 1 de 2. La Parte 2 (Conversions API vía Webhook + Cloudflare Worker) se publicará próximamente en este blog y en el canal de YouTube.
- ShopifyAutomatiza tareas en Shopify con Manus AI, sin apps
- ShopifyPayPal Pay Later dinámico en Shopify, sin apps
