Saltar al contenido

Marketing · Principiante

UTMs que sobreviven 30 díasatribuye la venta aunque compren semanas después

Las UTMs funcionan perfecto en el escenario que nadie vive: la persona hace clic y se registra en la misma visita. En la vida real llega desde Instagram, se distrae, vuelve tres días después por un recordatorio de WhatsApp o escribiendo la URL, y ahí ya no hay parámetros. Si tu landing lee window.location.search en el momento del submit, ese lead se atribuye a nada. La solución no es complicada, pero tiene cuatro trampas que cuestan semanas de datos sucios: Safari borra cookies de script a los 7 días, Meta a veces manda {{campaign.name}} sin resolver, el _fbc tiene un formato exacto con timestamp en milisegundos, y una función de limpieza que existe pero que nadie llama no limpia nada. Aquí va todo junto, con el código que uso en producción.

Publicada 28 de agosto de 2026Lectura 18 minNecesitas una web propia

De un vistazo

01

Capturas la campaña en la primera visita, no al enviar

02

Se guarda 30 días para atribuir la venta que tarda

03

Verificas que llegue completo al CRM y a Meta

01 el problema medido

Lo que pierdes cuando lees la URL al momento del submit

La primera versión de mis landings guardaba las UTMs en un useRef al montar la página y las mandaba con el formulario. Funcionaba en las pruebas porque yo hacía clic y llenaba el formulario de inmediato. En producción, en una landing de eventos presenciales con cientos de registros diarios, alrededor de uno de cada cuatro registros llegaba con utm_source vacío. No eran orgánicos: el volumen subía y bajaba con el gasto en anuncios. Eran personas que habían llegado por un anuncio en una visita y se registraban en otra.

Visita de retorno

La persona vuelve por la URL directa, por un link compartido sin parámetros o por un mensaje de recordatorio. La URL ya no trae nada y la memoria del componente se fue con la pestaña.

Embudo de varias páginas

Landing, página de oferta, checkout, gracias. Si las UTMs viven en memoria de la primera página, en la tercera ya no existen. El evento de compra sale sin campaña.

Pixel bloqueado o lento

La cookie _fbc la escribe el pixel de Meta cuando ve fbclid en la URL. Con un bloqueador, o si el submit ocurre antes de que cargue, no hay cookie. El fbclid estuvo en la URL y nadie lo guardó.

Safari y su ITP

Las cookies escritas desde JavaScript en Safari duran máximo 7 días, y 24 horas si la navegación llegó con parámetros de tracking. Tu cookie de 30 días no dura 30 días ahí.

La cifra que te importa no es cuántos leads tienen UTM, sino cuántos NO la tienen mientras estás pagando anuncios. Si ese porcentaje se mueve con el presupuesto, es atribución perdida, no tráfico orgánico.

02 la lista

Qué capturar y por qué cada cosa

Captura más de lo que crees que vas a usar. Guardar un campo extra cuesta cero; reconstruirlo después es imposible. Esta es la lista mínima que persisto en toda landing con formulario:

CampoDe dónde salePara qué sirve
utm_source, utm_medium, utm_campaign, utm_content, utm_termQuery de la URLReportes por campaña y por anuncio en tu CRM y tu dashboard. utm_content = el anuncio concreto.
utm_idQuery de la URLID numérico de campaña si lo pones en el link; sobrevive a los renombres que hace el equipo.
fbclidQuery de la URL (lo agrega Meta)Reconstruir _fbc para la Conversions API cuando el pixel no escribió la cookie.
gclid, ttclidQuery de la URL (Google, TikTok)Lo mismo para conversiones offline de Google Ads y Events API de TikTok.
referrerdocument.referrerDistinguir un directo real de un clic desde Instagram cuya app no pasó parámetros.
landing_pathpathname + search de la primera visitaSaber en qué página aterrizó, no en cuál se registró.
first_seen_atDate.now() en la primera capturaCalcular días entre clic y conversión, y expirar el registro a los 30 días.

First-touch o last-touch

First-touch guarda la primera fuente y no la pisa. Last-touch sobrescribe con cada visita que traiga parámetros. Para un embudo de registro a evento o de venta de un solo producto, first-touch es el correcto: quieres saber qué anuncio trajo a la persona, no cuál remarketing la cerró. Mi regla en el módulo de abajo es un punto medio que en la práctica se comporta como first-touch: un valor nuevo solo pisa al anterior si viene lleno, y nunca un vacío borra un lleno. Si quieres last-touch puro, guarda dos bloques: el original congelado y el último, y decide en el reporte.

03 la decisión

Dónde guardarlo para que dure 30 días

Hay dos lugares en el navegador y ninguno es suficiente solo. La cookie first-party viaja al servidor en cada request (útil si tu API route quiere leerla sin que el cliente la mande) y es lo que Meta espera para _fbc. localStorage no viaja al servidor, pero Safari no lo expira a los 7 días como hace con las cookies de script, y aguanta valores más largos. Guardo en los dos y leo primero la cookie viva.

Cookie first-partylocalStorage
Vida en Chrome/EdgeLo que pongas en max-age (30 días)Hasta que el usuario borre datos
Vida en Safari (ITP)7 días máximo; 24 h si llegó con parámetros de tracking7 días sin interacción con el sitio (también aplica ITP), pero sin el recorte de 24 h
Viaja al servidorSí, en cada requestNo; el cliente lo manda en el body
Tamaño cómodoUnos 4 KB por cookieVarios MB
Quién más lo leePixel de Meta lee _fbc y _fbpSolo tu código

Atributos de la cookie que importan: max-age=2592000 (30 días en segundos; tiene prioridad sobre expires), path=/ para que se lea en todo el embudo, SameSite=Lax para que sobreviva a la navegación desde la app de Instagram, y Secure porque estás en HTTPS. No pongas domain a menos que tengas subdominios que compartan el embudo.

Regla que evita el 80% de los datos sucios: nunca pises un valor lleno con uno vacío. Al fusionar lo nuevo con lo guardado, una llave ausente en la URL no debe borrar la que ya tenías. Es un if de una línea y cambia todo el reporte.

04 para copiar

El módulo completo: capturar, persistir, leer

Un archivo, sin dependencias, seguro en SSR (todo hace early-return si no hay window). Tres funciones públicas: captureAttribution() se llama al montar cualquier página del embudo; getAttribution() se llama en el submit; limpiarUTM() la exportas para que ningún otro archivo lea utm_ de la URL por su cuenta. Más adelante vas a ver por qué esa última es la importante.

Atajo: clonar el repo de esta guía (terminal)
git clone https://github.com/davidiriza-lab/utms-30-dias.git
Repo público con licencia MIT: github.com/davidiriza-lab/utms-30-dias. Trae los archivos de abajo listos para copiar a tu proyecto.
lib/attribution.ts
/* Atribución: captura y persiste utm_* + fbclid/gclid + referrer 30 días.
   Cookie first-party + localStorage como respaldo. Cliente-only, seguro en SSR. */

const KEY = "attr";
const MAX_AGE_S = 30 * 24 * 60 * 60;        // 30 días para la cookie
const MAX_AGE_MS = MAX_AGE_S * 1000;         // 30 días para localStorage

export interface Attribution {
  utm_source?:   string;
  utm_medium?:   string;
  utm_campaign?: string;
  utm_content?:  string;
  utm_term?:     string;
  utm_id?:       string;
  fbclid?:       string;
  gclid?:        string;
  ttclid?:       string;
  referrer?:     string;
  landing_path?: string;
  first_seen_at?: number;
  fbp?:          string;
  fbc?:          string;
}

const URL_KEYS = [
  "utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term",
  "utm_id", "fbclid", "gclid", "ttclid",
] as const;

/** Descarta placeholders sin resolver de las plataformas de ads:
    {{campaign.name}}, {{site_source_name}}, {campaign}. Solo si la cadena
    empieza Y termina con llaves; "{promo}-2026" o "NR-Norte-Sep" no se tocan. */
export function limpiarUTM(v: string | null | undefined): string | undefined {
  const s = String(v ?? "").trim();
  if (!s) return undefined;
  if (/^\{+.*\}+$/.test(s)) return undefined;
  return s;
}

function readCookie(name: string): string | undefined {
  if (typeof document === "undefined") return undefined;
  const safe = name.replace(/([.$?*|{}()[\]\\/+^])/g, "\\$1");
  const m = document.cookie.match(new RegExp("(?:^|; )" + safe + "=([^;]*)"));
  return m ? decodeURIComponent(m[1] ?? "") : undefined;
}

function writeCookie(name: string, value: string): void {
  if (typeof document === "undefined") return;
  document.cookie =
    name + "=" + encodeURIComponent(value) +
    "; max-age=" + MAX_AGE_S + "; path=/; SameSite=Lax; Secure";
}

function readStore(): Attribution {
  if (typeof window === "undefined") return {};
  // 1. cookie (viaja al servidor)   2. localStorage (aguanta mejor en Safari)
  const raw = readCookie(KEY) ?? (() => {
    try { return localStorage.getItem(KEY) ?? undefined; } catch { return undefined; }
  })();
  if (!raw) return {};
  try {
    const o = JSON.parse(raw) as Attribution;
    if (typeof o.first_seen_at === "number" && Date.now() - o.first_seen_at > MAX_AGE_MS) return {};
    return o;
  } catch { return {}; }
}

function writeStore(a: Attribution): void {
  const raw = JSON.stringify(a);
  writeCookie(KEY, raw);
  try { localStorage.setItem(KEY, raw); } catch { /* modo privado o cuota */ }
}

/** Llamar al montar CUALQUIER página del embudo (landing, oferta, checkout, gracias). */
export function captureAttribution(): Attribution {
  if (typeof window === "undefined") return {};
  const p = new URLSearchParams(window.location.search);
  const stored = readStore();
  const merged: Attribution = { ...stored };

  // Un valor nuevo solo pisa si viene lleno; un vacío nunca borra un lleno.
  for (const k of URL_KEYS) {
    const v = limpiarUTM(p.get(k));
    if (v) merged[k] = v;
  }
  if (!merged.referrer && document.referrer) merged.referrer = document.referrer;
  if (!merged.landing_path) merged.landing_path = window.location.pathname + window.location.search;
  if (!merged.first_seen_at) merged.first_seen_at = Date.now();

  // fbp/fbc: la cookie viva del pixel gana; si no hay _fbc, se construye del fbclid.
  const fbp = readCookie("_fbp");
  if (fbp) merged.fbp = fbp;
  const cookieFbc = readCookie("_fbc");
  if (cookieFbc) merged.fbc = cookieFbc;
  else if (!merged.fbc && merged.fbclid) merged.fbc = "fb.1." + Date.now() + "." + merged.fbclid;

  writeStore(merged);
  return merged;
}

/** Leer en el submit: lo persistido + cookies vivas (que ganan para fbc/fbp). */
export function getAttribution(): Attribution {
  const stored = readStore();
  const fbp = readCookie("_fbp");
  const fbc = readCookie("_fbc");
  return { ...stored, ...(fbp ? { fbp } : {}), ...(fbc ? { fbc } : {}) };
}

Dos detalles de diseño: la cookie guarda el JSON completo en una sola cookie (unos 300 a 600 bytes en la práctica) en lugar de una cookie por campo, para que el max-age se renueve completo en cada captura. Y first_seen_at cumple doble función: expira el registro a los 30 días y te deja calcular cuántos días pasaron entre el clic y la conversión, que es un número que te va a sorprender.

05 la trampa silenciosa

Los {{campaign.name}} que llegan sin resolver

Meta permite poner parámetros dinámicos en la URL del anuncio: {{campaign.name}}, {{adset.name}}, {{ad.name}}, {{placement}}, {{site_source_name}}. Cuando el anuncio se sirve, Meta los sustituye por el valor real y la URL llega con utm_campaign=Verano-Norte. Cuando NO los sustituye (previsualizaciones, links copiados desde el administrador, algunos placements, tests del equipo), la URL llega con utm_campaign={{campaign.name}}, literal, con llaves. Si guardas eso, tu reporte tiene una 'campaña' llamada {{campaign.name}} que acumula leads que no son de ninguna.

La corrección es una regex. Lo que quiero contarte es lo que pasó con ella. La función limpiarUTM existió durante meses en el módulo de atribución y no limpió nada, porque las cinco plantillas de landing leían las UTMs de la URL por su cuenta, sin pasar por el módulo, y mandaban su propio objeto al API. Al fusionar objetos, la llave ausente del módulo (que había descartado el placeholder) no pisaba la llave cruda de la plantilla, y el {{campaign.name}} sobrevivía hasta la base de datos, el CRM y la Conversions API. Se descubrió cuando alguien preguntó por qué la campaña con más leads del mes tenía llaves en el nombre.

  • Exporta limpiarUTM y prohíbe en el repo cualquier p.get('utm_ que no pase por ella. Un grep en el pre-commit basta.
  • Repite el saneado del lado del servidor, en el API route que guarda. El cliente se puede saltar; el servidor no.
  • Sé conservador: descarta solo lo que empieza y termina con llaves. {promo}-2026 es un nombre válido que alguien puso a propósito.
  • Guarda vacío, no el valor sucio ni un 'desconocido'. Los filtros de tu dashboard ignoran vacíos; un 'desconocido' se convierte en categoría.
Del lado del servidor, en el API route (misma regla, distinta firma)
function sanitizeUtm(value: unknown): string {
  const s = String(value ?? "").trim();
  return /^\{+.*\}+$/.test(s) ? "" : s;
}

06 el formato exacto

Construir _fbc desde fbclid (y el error de los segundos)

La Conversions API acepta el parámetro fbc dentro de user_data y con él Meta ata tu evento server-side al clic del anuncio. Normalmente lo lees de la cookie _fbc que escribe el pixel. Cuando la cookie no existe pero tienes el fbclid guardado, la documentación de Meta dice que lo construyas tú con este formato:

Formato documentado de _fbc
fb.<subdomainIndex>.<creationTime>.<fbclid>

fb               versión, siempre "fb"
subdomainIndex   1 para tudominio.com, 2 para www.tudominio.com (0 = TLD)
creationTime     UNIX time en MILISEGUNDOS del momento en que recibiste el fbclid
fbclid           el valor tal cual llegó en la URL; es case-sensitive, no lo toques

Ejemplo:  fb.1.1756368000000.IwAR2xkQ...

El error que cometí: mi primera versión hacía Math.floor(Date.now() / 1000) porque asumí segundos, como casi todo lo que es 'UNIX time'. Meta lo especifica en milisegundos y su propio ejemplo de _fbp (fb.1.1596403881668.1116446470) lo confirma: 13 dígitos. Un timestamp en segundos no rompe el envío (Meta lo acepta) pero el clic parece de 1970 y el matching pierde calidad. En el módulo de arriba está corregido: Date.now() sin dividir.

  • Cookie viva primero. Si el pixel escribió _fbc, ese valor gana siempre sobre tu reconstrucción: es el que Meta ya conoce.
  • Meta recomienda una expiración de 90 días para _fbc; tu registro de atribución de 30 días es más corto y está bien, porque el clic que te importa está dentro de tu ventana de conversión.
  • En un embudo de varias páginas, reconstruye el fbc una vez (en la landing) y arrástralo persistido. Si lo reconstruyes en cada página, cada evento sale con un creationTime distinto para el mismo clic.
  • Manda también fbp (la cookie _fbp) y un external_id estable (un visitor_id en localStorage) con el mismo evento. fbc solo te da el clic; los tres juntos dan la persona.

07 el viaje completo

Adjuntarlo al formulario y al CRM

El cliente llama captureAttribution() al montar y getAttribution() al enviar, y manda el objeto junto con nombre, correo y teléfono a un API route. El servidor sanea otra vez, guarda en tu base con await (la respuesta al usuario depende de eso) y dispara CRM y Conversions API sin esperar. Las escrituras siempre desde el servidor: la llave del CRM no vive en el navegador.

Cliente: al montar y al enviar
"use client";
import { useEffect, type FormEvent } from "react";
import { captureAttribution, getAttribution } from "@/lib/attribution";

export function FormularioRegistro() {
  useEffect(() => { captureAttribution(); }, []);

  async function onSubmit(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const form = new FormData(e.currentTarget);
    const attr = getAttribution();
    const event_id = crypto.randomUUID();     // el mismo para pixel y CAPI

    const res = await fetch("/api/registro", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        name:  String(form.get("name") ?? ""),
        email: String(form.get("email") ?? ""),
        phone: String(form.get("phone") ?? ""),
        event_id,
        ...attr,                                // utm_*, fbclid, gclid, referrer, fbc, fbp, first_seen_at
      }),
    });
    if (!res.ok) { /* mostrar error */ return; }
    // aquí disparas el pixel con { eventID: event_id } y rediriges a /gracias
  }

  return <form onSubmit={onSubmit}>{/* campos */}</form>;
}
app/api/registro/route.ts (esquema; rellena tus clientes)
import { NextResponse, type NextRequest } from "next/server";
import { guardarRegistro, upsertContactoCRM, enviarCAPI } from "@/lib/integraciones";

interface RegistroBody {
  name: string; email: string; phone: string; event_id: string;
  utm_source?: string; utm_medium?: string; utm_campaign?: string;
  utm_content?: string; utm_term?: string; utm_id?: string;
  fbclid?: string; gclid?: string; referrer?: string; landing_path?: string;
  first_seen_at?: number; fbc?: string; fbp?: string;
}

function sanitizeUtm(value: unknown): string {
  const s = String(value ?? "").trim();
  return /^\{+.*\}+$/.test(s) ? "" : s;
}

export async function POST(req: NextRequest) {
  const body = (await req.json()) as RegistroBody;
  if (!body.email || !body.name) {
    return NextResponse.json({ error: "faltan campos" }, { status: 400 });
  }

  const attr = {
    utm_source:   sanitizeUtm(body.utm_source),
    utm_medium:   sanitizeUtm(body.utm_medium),
    utm_campaign: sanitizeUtm(body.utm_campaign),
    utm_content:  sanitizeUtm(body.utm_content),
    utm_term:     sanitizeUtm(body.utm_term),
    utm_id:       sanitizeUtm(body.utm_id),
    fbclid:       String(body.fbclid ?? "").slice(0, 256),
    gclid:        String(body.gclid ?? "").slice(0, 256),
    referrer:     String(body.referrer ?? "").slice(0, 512),
    landing_path: String(body.landing_path ?? "").slice(0, 512),
    dias_clic_a_registro: body.first_seen_at
      ? Math.round((Date.now() - body.first_seen_at) / 86_400_000)
      : null,
  };

  // 1. Tu base de datos: AWAIT (service role, nunca desde el cliente)
  const saved = await guardarRegistro({ ...body, ...attr });
  if (!saved.ok) return NextResponse.json({ error: saved.error }, { status: 500 });

  // 2 y 3. CRM y Conversions API: fire-and-forget con log de fallas
  upsertContactoCRM({ email: body.email, name: body.name, phone: body.phone, attr })
    .catch((e: unknown) => console.error("[crm]", e));
  enviarCAPI({ event_id: body.event_id, email: body.email, phone: body.phone,
               fbc: body.fbc ?? "", fbp: body.fbp ?? "", attr })
    .catch((e: unknown) => console.error("[capi]", e));

  return NextResponse.json({ ok: true });
}

En el CRM, las UTMs van a campos personalizados (uno por utm_*, más fbclid y la página de aterrizaje) y el contacto se hace con upsert por correo, no create: si la persona ya existe, no quieres un duplicado ni quieres pisar la fuente original con la de hoy. La mayoría de los CRM dejan configurar 'solo escribir si está vacío' por campo; si el tuyo no, hazlo en el código: lee el contacto, y manda solo las UTMs que él no tenga.

Si además de la base propia usas un CRM con su propia atribución automática, no confíes en ella para el retorno a 30 días: casi todas leen la URL de la visita en que se envió el formulario. Tus campos personalizados son la fuente de verdad; la atribución del CRM es un extra.

08 antes de darlo por bueno

Cómo verificar que llega

  1. Abre la landing en ventana privada con ?utm_source=prueba&utm_campaign=verificacion&utm_content=anuncio-1&fbclid=TEST123. En DevTools, Application, revisa la cookie attr y la llave attr en localStorage: deben tener los cinco valores y first_seen_at.
  2. Cierra la pestaña. Abre la landing de nuevo SIN parámetros, en la misma ventana privada. Vuelve a revisar: los valores siguen ahí y no se pisaron con vacíos.
  3. Ahora abre con ?utm_campaign={{campaign.name}}. Revisa que utm_campaign NO cambió a la cadena con llaves: el valor anterior se conservó.
  4. Registra un contacto de prueba desde la visita sin parámetros. En tu base y en el CRM, el contacto debe traer utm_campaign=verificacion y fbc=fb.1.<13 dígitos>.TEST123.
  5. Repite lo mismo en Safari de iPhone. Es el navegador donde se pierde más; si ahí funciona, funciona en todos.
  6. En producción, una vez por semana: cuenta registros sin utm_source en los días con gasto de anuncios. Si baja de 25% a menos de 10%, el sistema está haciendo su trabajo. Lo que queda es orgánico real.
Consulta de salud semanal (SQL, ajusta la tabla)
SELECT date_trunc('day', created_at) AS dia, count(*) AS total, count(*) FILTER (WHERE utm_source = '' OR utm_source IS NULL) AS sin_fuente, count(*) FILTER (WHERE utm_campaign LIKE '{{%') AS con_llaves, count(DISTINCT fbc) AS fbc_unicos FROM registros WHERE created_at > now() - interval '7 days' GROUP BY 1 ORDER BY 1;
con_llaves debe ser 0 siempre. fbc_unicos cerca de total = clics reales; un mismo fbc repetido en decenas de registros = alguien compartió un link con fbclid pegado (típico en grupos de WhatsApp).

Una última señal que te ahorra un diagnóstico largo: si la base tiene cientos de registros diarios con utm_campaign correcto y fbc únicos, pero el administrador de anuncios reporta casi cero conversiones para esa campaña, la atribución de tu lado está bien. El problema es del pixel o de la Conversions API, y eso es otra guía.

FAQ lo que suelen preguntar

Preguntas frecuentes

¿Por qué 30 días y no 7 o 90?

Porque es la ventana de atribución más larga que Meta ofrece para clics (7 días clic es la default; 28 días existía antes) y cubre el ciclo real de decisión de un evento o un producto de ticket medio. Más largo acumula visitas viejas que ya no explican la compra; más corto pierde a los que tardan dos semanas. Si vendes algo que se decide en horas, baja a 7. Si es un ciclo B2B de meses, sube y guarda first y last por separado.

¿No basta con la cookie _fbc que ya escribe el pixel de Meta?

No por tres razones: el pixel puede estar bloqueado, puede no haber cargado cuando la persona envía el formulario, y en Safari la cookie muere a los 7 días (o 24 horas si la visita llegó con fbclid). Tu copia en localStorage con el fbclid crudo te permite reconstruirla. Y la cookie del pixel no guarda utm_*, que es lo que tu CRM necesita.

¿Qué pasa con el consentimiento de cookies?

Una cookie first-party con tus propios parámetros de campaña es de las que la mayoría de marcos legales tratan como analítica propia, pero eso depende de tu jurisdicción y de tu aviso de privacidad. Lo práctico: si tienes banner de consentimiento, llama a captureAttribution() después del consentimiento y guarda el fbclid en memoria mientras tanto para no perderlo.

¿Cómo manejo un embudo con landing en un dominio y checkout en otro?

Ni la cookie ni localStorage cruzan dominios. Pasa la atribución en la URL al saltar: getAttribution() serializado como parámetros del link al checkout, y captureAttribution() del otro lado la vuelve a persistir. Es la misma razón por la que el módulo se llama al montar cualquier página y no solo la primera.

¿Sirve el mismo módulo para Google Ads y TikTok?

Sí, ya captura gclid y ttclid. Para Google, el gclid se manda con la conversión offline (importación de conversiones) junto con la fecha; el first_seen_at te da esa fecha. Para TikTok, la Events API acepta ttclid en el contexto del evento. El formato reconstruido fb.1.<ms>.<fbclid> es solo de Meta.

Cierre de la guía

Todo esto cabe en un archivo de 100 líneas y una regla: nunca leas utm_ de la URL fuera del módulo. Lo que cambia no es el código sino el reporte: la columna 'directo' se encoge a lo que de verdad es directo, y las campañas recuperan los leads que siempre fueron suyos. Instálalo, corre la prueba de Safari y mide la semana siguiente cuántos registros llegan sin fuente. Esta guía vive en el Lab de David Iriza.

Fuentes oficiales4

Sigue con estas guías

Guía escrita con la información oficial disponible al 28 de agosto de 2026. Esta página no está afiliada a Meta. Las herramientas cambian; ante la duda, revisa la documentación de la Conversions API de Meta.