Capturas la campaña en la primera visita, no al enviar
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.
De un vistazo
Se guarda 30 días para atribuir la venta que tarda
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.
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.
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.
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ó.
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:
| Campo | De dónde sale | Para qué sirve |
|---|---|---|
| utm_source, utm_medium, utm_campaign, utm_content, utm_term | Query de la URL | Reportes por campaña y por anuncio en tu CRM y tu dashboard. utm_content = el anuncio concreto. |
| utm_id | Query de la URL | ID numérico de campaña si lo pones en el link; sobrevive a los renombres que hace el equipo. |
| fbclid | Query de la URL (lo agrega Meta) | Reconstruir _fbc para la Conversions API cuando el pixel no escribió la cookie. |
| gclid, ttclid | Query de la URL (Google, TikTok) | Lo mismo para conversiones offline de Google Ads y Events API de TikTok. |
| referrer | document.referrer | Distinguir un directo real de un clic desde Instagram cuya app no pasó parámetros. |
| landing_path | pathname + search de la primera visita | Saber en qué página aterrizó, no en cuál se registró. |
| first_seen_at | Date.now() en la primera captura | Calcular 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-party | localStorage | |
|---|---|---|
| Vida en Chrome/Edge | Lo 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 tracking | 7 días sin interacción con el sitio (también aplica ITP), pero sin el recorte de 24 h |
| Viaja al servidor | Sí, en cada request | No; el cliente lo manda en el body |
| Tamaño cómodo | Unos 4 KB por cookie | Varios MB |
| Quién más lo lee | Pixel de Meta lee _fbc y _fbp | Solo 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.
git clone https://github.com/davidiriza-lab/utms-30-dias.git
/* 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.
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:
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.
"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>;
}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
- 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.
- 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.
- Ahora abre con ?utm_campaign={{campaign.name}}. Revisa que utm_campaign NO cambió a la cadena con llaves: el valor anterior se conservó.
- 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.
- Repite lo mismo en Safari de iPhone. Es el navegador donde se pierde más; si ahí funciona, funciona en todos.
- 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.
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;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
- fbp y fbc: parámetros de clic y cookies (Conversions API, Meta for Developers) ↗El formato exacto de _fbc y _fbp (versión, índice de subdominio, creationTime en milisegundos, fbclid), cómo construirlo desde fbclid cuando no hay cookie y la expiración recomendada de 90 días.
- Especificaciones de los parámetros de URL dinámicos (Centro de ayuda de Meta para empresas) ↗Los marcadores {{campaign.name}}, {{adset.name}}, {{ad.name}}, {{placement}} y {{site_source_name}} que Meta sustituye al servir el anuncio, y que a veces llegan sin resolver.
- Document.cookie (MDN Web Docs) ↗Semántica de max-age, expires, path, SameSite y Secure, y por qué max-age tiene prioridad sobre expires.
- Intelligent Tracking Prevention 2.3 (blog de WebKit) ↗Por qué en Safari la cookie que escribes dura 24 horas si la visita llegó con parámetros en la URL, y por qué localStorage se borra tras 7 días sin interacción con el sitio.
Sigue con estas guías
- Nº 010 · Píxel + CAPI sin duplicar →El fbc que construyes aquí es el que viaja en user_data de la Conversions API; esa guía cubre el doble disparo pixel + servidor con el mismo event_id.
- Nº 007 · Analytics propio en tu landing →El tracker propio guarda estas mismas UTMs por sesión; esta guía las hace sobrevivir entre sesiones para el registro.
- Nº 006 · Escrituras seguras desde una landing →El API route que recibe la atribución y escribe en base, CRM y CAPI sigue las reglas de esa guía: llaves solo en el servidor.
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.
por David Iriza