Engineering

Elevar la calidad de coincidencia de CAPI desde el servidor

El Event Match Quality es un recuento de cuántas señales identificativas llegan a la plataforma por evento. La mayoría de las configuraciones del lado del servidor envían tres o cuatro y se quedan ahí. Beaconry hace en cambio el trabajo de las claves de coincidencia en el servidor: un baúl cifrado y propio que recuerda los identificadores entre eventos, enriquecimiento de GeoIP y _fbp que rellena los huecos que normalmente rellenaría un píxel de navegador, y canonicalización estricta para que la misma persona produzca siempre el mismo hash. No hace falta ningún píxel de navegador.

Tiempo de lectura: ~9 minPublicado: 2026-06-08

Qué cuenta realmente la calidad de coincidencia

El Event Match Quality de Meta, la tasa de eventos coincidentes de TikTok, la puntuación de match-rate de Pinterest: todas son la misma idea con nombres distintos. La plataforma recibe un evento, intenta vincularlo a una persona real de su grafo y te puntúa según cuántas señales aprovechables le entregaste. El email hasheado coincide con fuerza. El teléfono hasheado coincide con fuerza. Nombre más apellido más ciudad más código postal lo acotan aún más. Un click-ID (fbc, ttclid) o una cookie propia de navegador (_fbp) ancla el evento a un navegador conocido. La IP y el user agent son débiles por sí solos, pero elevan la confianza en combinación.

La configuración ingenua del lado del servidor envía lo que WordPress tenga por casualidad a mano en el momento en que se dispara el evento. En un checkout, eso es mucho: la clienta tecleó su email, teléfono, nombre y dirección en el formulario de pedido. En una simple vista de página desde un clic de anuncio, no es casi nada, porque la visitante todavía no ha entregado nada. Así que los eventos que más importan para la optimización, los del inicio del embudo que la plataforma usa para aprender a quién mostrar tu anuncio, llegan con los datos de coincidencia más finos. Ese es el hueco que cierra este trabajo.

La respuesta del píxel de navegador a esto es bien conocida: deja que el propio JavaScript de Meta o de TikTok establezca sus propias cookies de primera parte y las vuelva a leer. Eso funciona, pero reintroduce el script de terceros que los adblockers eliminan, cuesta bytes y rompe la postura de "sin seguimiento de terceros". El modo híbrido cubre los casos en los que ese intercambio vale la pena (escribimos un artículo entero sobre cuándo activarlo). Todo lo de abajo es lo que hace Beaconry antes de que recurras al modo híbrido, enteramente en el servidor, para las visitantes a las que un píxel bloqueado habría dejado vacías.

El baúl de PII: recordar identificadores entre eventos

El problema central de los eventos anónimos del inicio del embudo es el momento. Una visitante hace clic en un anuncio de Meta, aterriza en una entrada de blog (vista de página, sin PII), la lee, vuelve dos días después desde una búsqueda de Google, navega por tres productos (tres eventos view_item, todavía sin PII) y solo en la cuarta visita rellena un formulario de contacto. El envío del formulario tiene un email fuerte. Los once eventos anteriores no tenían nada. Sin persistencia, esos once eventos quedan para siempre con baja coincidencia, y la plataforma nunca aprende que la persona que finalmente convirtió es la misma que hizo clic en el anuncio.

El baúl de PII de Beaconry corrige la asimetría. Cuando cualquier evento lleva un identificador real, un email de un formulario, un nombre y una dirección de un pedido de WooCommerce, el baúl lo guarda. Cada evento posterior lo vuelve a leer y se enriquece con esas mismas claves de coincidencia. El email del envío de formulario viaja hacia adelante a cada vista de página y vista de producto posterior de ese navegador.

El almacenamiento es una única cookie propia llamada nl_v. No es una cookie en texto plano. La carga útil es el conjunto de identificadores, codificado en JSON, y luego cifrado con AES-256-GCM con una clave derivada del propio wp_salt('auth') del sitio:

// key derivation: fold the 64-byte auth salt to 32 bytes for AES-256
private static function derive_key(): string {
    return hash( 'sha256', (string) wp_salt( 'auth' ), true );
}

// encrypt: random 12-byte IV, 16-byte GCM tag, base64( iv ++ tag ++ ciphertext )
$iv         = random_bytes( 12 );
$ciphertext = openssl_encrypt( $plaintext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag, '', 16 );
return base64_encode( $iv . $tag . $ciphertext );

El navegador solo ve en todo momento base64 de texto cifrado autenticado. No puede volver a leer el email ni manipularlo (GCM está autenticado, un byte invertido falla la comprobación del tag al descifrar y el baúl lo trata como ausente). Bajo el artículo 4(5) del RGPD, eso convierte la cookie en un almacén seudonimizado: inútil sin la clave del lado del servidor que nunca sale del servidor. La cookie se establece como HttpOnly (sin acceso desde JavaScript), SameSite=Lax (sin fugas entre sitios) y Secure en HTTPS.

Diez campos son elegibles para el baúl, exactamente el conjunto que reconoce el pipeline de hasheo de CAPI:

private const VAULT_FIELDS = [
    'em', 'ph', 'fn', 'ln', 'ct', 'st', 'zp', 'country', 'ge', 'db',
];

Es notable que external_id queda deliberadamente excluido, ese ya es el hash de la cookie de dispositivo y vive por separado, así que el baúl nunca lo duplica. Los valores se guardan recortados pero no pre-hasheados, porque cada canal aplica su propia normalización en el momento del hasheo (más sobre esto abajo) y el pre-hasheo fijaría el valor al formato de un solo canal.

Tres reglas mantienen el baúl honesto. Solo escribe cuando está presente el consentimiento nl_pref.marketing. Lee tras la misma barrera de consentimiento, de modo que una retirada posterior del consentimiento detiene la reproducción de PII almacenada hacia los canales incluso antes de que la cookie expire de forma natural. Y el TTL está acotado: 30 días por defecto, configurable, con tope duro de 180 para mantenerse dentro de la ventana de retención que coincide con los ciclos de refresco de audiencias personalizadas de Meta y Pinterest y que las autoridades de protección de datos de la UE aceptan ampliamente como razonable.

El enriquecimiento se ejecuta una vez, para cada canal, mediante el filtro de despacho

El baúl no parchea el método de despacho de cada canal. Se engancha al único filtro previo al despacho por el que pasa cada evento, de modo que un canal recién añadido hereda el enriquecimiento gratis:

// priority 6: after the commerce-adapter enrich (5), before GeoIP (7)
add_filter( 'bcnr_pre_dispatch_event', [ __CLASS__, 'enrich_event' ], 6 );

public static function enrich_event( $event ): array {
    $vault = self::recall();
    if ( $vault === [] ) return $event;

    $existing = $event['user_data'] ?? [];
    foreach ( $vault as $k => $v ) {
        // gap-fill only: never overwrite a value the event already has
        if ( ! isset( $existing[ $k ] ) || $existing[ $k ] === '' ) {
            $existing[ $k ] = $v;
        }
    }
    $event['user_data'] = $existing;
    return $event;
}

El orden de prioridad es todo el diseño. Los datos de sesión del adaptador de comercio (la clienta activa de WooCommerce, la sesión de EDD, el pedido de SureCart) se ejecutan en prioridad 5 y ganan, porque son la fuente más fresca y autorizada. El baúl se ejecuta en 6 y rellena solo lo que el adaptador dejó vacío, porque los datos del baúl pueden ser más antiguos. GeoIP se ejecuta en 7 y rellena solo lo que aún falta tras ambos, porque una ciudad derivada de una IP es una conjetura y una dirección declarada por la visitante siempre gana a una conjetura. Rellenar huecos, nunca sobrescribir, en un orden de confianza deliberado.

Una versión anterior cometió aquí un error sutil que vale la pena señalar: las lecturas del baúl solían vivir dentro de los adaptadores de comercio, lo que significaba que solo los eventos enrutados por el adaptador las veían. Las vistas de página enrutadas por el navegador y los eventos de seguimiento de clics nunca recibían el baúl, así que una visitante que enviaba un formulario y luego golpeaba una vista de página /cart/ seguía llegando a Pinterest y Snap con solo tres o cuatro campos de user_data. Mover la lectura al filtro global previo al despacho lo arregló: ahora cada evento recibe el baúl, sin importar cómo entró en el pipeline.

Captura gratuita de email desde enlaces de newsletter

Un truco extra cabalga sobre el baúl. Una gran parte del tráfico del inicio del embudo llega a través de newsletters por email, y los grandes ESP incrustan la dirección de la destinataria directamente en el enlace. Beaconry la lee y siembra el baúl al aterrizar, antes de que la visitante haga nada:

  • ?email= para Mailchimp y HubSpot, ?em= y ?contact_email= para otras plantillas de ESP: texto plano, leído directamente.
  • ?_ke= para Klaviyo: base64 seguro para URL. Beaconry lo convierte de nuevo a base64 estándar, vuelve a rellenar el padding, decodifica y maneja tanto la forma de email plano como la envuelta en JSON ({"email":"..."}) que emite Klaviyo.

Cada candidato se valida con is_email() antes de guardarse, y la captura se omite si el baúl ya contiene un email (un enlace de newsletter obsoleto nunca debería pisar un envío de formulario más fresco). El efecto neto: un clic de newsletter da a cada evento de esa sesión claves de coincidencia fuertes, con cero PII tecleada por la visitante.

_fbp y _fbc en el servidor: plantar la cookie del navegador sin el navegador

El grafo de coincidencia de Meta se apoya con fuerza en dos de sus propias cookies de primera parte. _fbp es un identificador estable por navegador, y _fbc es el compuesto de clic derivado del fbclid que un anuncio de Meta añade a la URL de aterrizaje. Normalmente solo el JavaScript del píxel de Meta las escribe, lo que significa que existen solo cuando el píxel se ejecutó: consentimiento de marketing dado, sin bloqueador de scripts, una ruta ininterrumpida a connect.facebook.net. Para una visitante con adblock, las tres fallan y _fbp simplemente nunca está ahí.

Beaconry genera ambas en el servidor. _fbc se sintetiza a partir del parámetro de consulta fbclid en el formato documentado de Meta cuando todavía no existe ninguna cookie:

// fb.<subdomain_index>.<creation_ms>.<fbclid>
return 'fb.1.' . ( time() * 1000 ) . '.' . $fbclid;

_fbp se genera con la misma especificación, fb.1.<creation_time_ms>.<random_10_digits>, y se vuelve a escribir como una cookie real para que cualquier llamada posterior del píxel de Meta (si la clienta llega a activar el modo híbrido) siga usando el mismo valor en lugar de acuñar uno competidor:

$ts_ms     = (int) round( microtime( true ) * 1000 );
$random_10 = (string) random_int( 1000000000, 9999999999 );
$fbp       = 'fb.1.' . $ts_ms . '.' . $random_10;

Esta cookie se establece con tres condiciones de guarda, y importan. Solo escribe cuando hay un destino de Meta configurado (has_meta(), de lo contrario la cookie no tiene consumidor y es puro ruido), solo cuando hay consentimiento de marketing presente (escribirla de otro modo violaría ePrivacy) y solo cuando aún no se han enviado las cabeceras. A diferencia del baúl, _fbp se establece a propósito como HttpOnly=false: la extensión Meta Pixel Helper y un futuro píxel de navegador necesitan ambos leerla, así que no puede quedar encerrada lejos de JavaScript. El TTL es de 90 días, el valor por defecto de la propia Meta.

El resultado: una visitante con adblock que hizo clic en un anuncio de Meta ahora llega a Meta CAPI con un fbc válido (de su fbclid) y un fbp estable, las dos cookies exactas que el píxel de navegador habría establecido, plantadas por una petición del mismo origen que el adblocker no puede eliminar.

GeoIP: rellenar la ubicación para los verdaderamente anónimos

Para una visitante sin email, sin pedido y sin historial en el baúl, la ubicación todavía es recuperable a partir de su IP, y ciudad más región más código postal más país es una contribución significativa a la calidad de coincidencia. Beaconry la resuelve a partir de dos fuentes, en orden:

  1. Cabeceras de borde de Cloudflare. Cualquier sitio detrás de Cloudflare obtiene CF-IPCountry, CF-IPCity, CF-Region-Code y CF-Postal-Code en cada petición, resueltas en el borde, gratis e inmunes a la prevención de seguimiento del lado del cliente. Beaconry rechaza los centinelas Tor de Cloudflare (XX, T1) para que un nodo de salida anonimizador no envenene el campo de país.
  2. Geolocalización de WooCommerce como respaldo cuando Cloudflare no está. WC_Geolocation::geolocate_ip() usa la base de datos MaxMind mantenida por WooCommerce para país y región.

Como el baúl, GeoIP solo rellena huecos. Se ejecuta en prioridad 7, el último de la cadena, y toca un campo solo si sigue vacío después de que el adaptador y el baúl hayan tenido su turno. Una ciudad declarada por la visitante siempre gana a una adivinada por IP. La búsqueda además se memoiza en una estática de petición, de modo que un solo evento que se abre en abanico hacia diez canales resuelve la geo una vez, no diez veces.

Canonicalización: la misma persona, el mismo hash, siempre

Nada de lo anterior ayuda si el mismo valor lógico llega con tres grafías y produce tres cadenas hash distintas. Una región alemana puede aparecer como "Niedersachsen", "NI" o "ni". SHA-256 las convierte en tres hashes sin relación, y el emparejador de audiencias de la plataforma nunca las vuelve a agrupar. Los datos de coincidencia están técnicamente presentes y en la práctica son inútiles.

Así que Beaconry canonicaliza los campos sensibles al formato antes de hashear. Esto se ejecuta dentro de hash_user_data(), justo antes de la pasada de SHA-256, para cada canal:

  • country se normaliza a ISO-3166-1 alpha-2 en minúsculas. Una gran tabla de búsqueda mapea códigos alpha-3 y nombres de país en inglés, alemán y español ("Deutschland", "Germany", "Alemania", "deu" colapsan todos a de).
  • state se normaliza al código de subdivisión ISO-3166-2, en minúsculas, con el prefijo de país eliminado. Cloudflare entrega las regiones como DE-NI; Meta y TikTok quieren solo ni. El prefijo de país hace además de pista de desambiguación, porque ni es a la vez Baja Sajonia en la posición de región y Nigeria en la posición de país.
  • zip es solo dígitos para la región DACH y la mayor parte de la UE, pero se mantiene alfanumérico para el Reino Unido, Canadá, los Países Bajos e Irlanda, cuyos códigos postales contienen legítimamente letras ("SW1A 1AA", "M5V 3L9").
  • db (fecha de nacimiento) se normaliza a YYYYMMDD sin separadores, parseando cualquier formato de entrada vía DateTime.

La recompensa es que el email del envío de formulario, la dirección del pedido de WooCommerce, el nombre recordado por el baúl, la ciudad de GeoIP y el email capturado del newsletter convergen todos en una identidad canónica, hasheada de forma idéntica, por persona. Esa convergencia es lo que de verdad mueve la aguja de la calidad de coincidencia, porque la plataforma por fin ve una huella consistente en lugar de un borrón de casi-duplicados.

El conjunto de campos hasheados por canal

"Hashea la PII y envíala" no es una sola regla, es una regla distinta por canal, y equivocarse en la forma por canal hunde la tasa de coincidencia en silencio incluso cuando los datos son correctos. El ayudante de hasheo de Beaconry toma un conjunto canónico y cada despachador lo remodela a la especificación de su canal:

  • Meta CAPI hashea el conjunto completo: em, ph, fn, ln, ct, st, zp, country, ge, db y external_id. Al teléfono se le quita su + inicial para dejarlo solo en dígitos, porque el grafo de audiencias de Meta coincide con esa forma. client_ip_address, client_user_agent, fbc y fbp viajan en crudo, nunca hasheados, según la especificación.
  • TikTok Events API es lo opuesto deliberado para la ubicación: ciudad, región y país van sin hashear (solo en minúsculas y recortados), mientras que email, phone, first_name, last_name, zip_code y external_id se hashean. El teléfono mantiene aquí el + (E.164), lo contrario de Meta. Enviar a TikTok una ciudad hasheada es un fallo de coincidencia silencioso, la auditoría con la documentación por delante atrapó exactamente esto.
  • Pinterest, Snap y Reddit toman los campos hasheados envueltos en arrays de un solo elemento. Comparten una regla de respaldo: si no llegó ningún external_id a través del pipeline de PII, Beaconry suministra el ID de dispositivo hasheado (la cookie nl_dev, o un hash compuesto de IP más user agent) para que incluso una visita de página anónima lleve una clave estable y emparejable. La advertencia de buenas prácticas de Pinterest "external_id missing" fue el arma humeante que impulsó esto.
  • GA4 para datos proporcionados por el usuario usa sus propios nombres de clave, sha256_email_address y sha256_phone_number, hasheados con la misma normalización de recortar-minúsculas-SHA-256 para que los valores cuadren con lo que habría enviado un gtag de navegador.

El único núcleo de canonicalizar-luego-hashear garantiza que el valor sea idéntico entre canales; cada despachador posee solo la peculiaridad del formato de transporte (el envoltorio en array, el renombrado de claves, el + del teléfono, la decisión de hashear o no la ubicación). Esa separación es la razón por la que un valor que coincide en Meta también coincide en TikTok, en lugar de derivar por canal.

Juntándolo todo: un evento anónimo

Sigue una sola vista de página de una visitante con adblock que ayer hizo clic en un anuncio de Meta y hoy más temprano envió un formulario de contacto, todo en el mismo navegador:

  1. El evento entra en el pipeline con casi nada: una URL de página, una marca de tiempo, una cookie de ID de dispositivo.
  2. Prioridad 5, el adaptador de comercio, no tiene nada que añadir (esto no es un evento de tienda).
  3. Prioridad 6, el baúl, recuerda el email que la visitante tecleó esta mañana en el formulario y lo añade.
  4. Prioridad 7, GeoIP, rellena ciudad, región y código postal a partir de las cabeceras de borde de Cloudflare.
  5. En el despacho, _fbc se sintetiza a partir del fbclid de ayer (todavía en la cadena de URL o en la cookie), y _fbp se genera y se planta, sin necesidad de píxel de navegador.
  6. La canonicalización colapsa la región a ni y el país a de, y luego SHA-256 hashea todo el lote en la forma exacta de Meta.

El evento que empezó con tres señales débiles se va con email hasheado, nombre donde esté disponible, ciudad, región, código postal y país hasheados, un fbc válido, un fbp estable, más IP y user agent en crudo. Esa es la diferencia entre una calificación "Poor" y una "Good" de Event Match Quality, lograda en una vista de página anónima, de una visitante cuyo píxel de navegador nunca cargó.

Conclusión

La calidad de coincidencia no es una credencial que pegas una vez. Es la suma por evento de señales identificativas, y los eventos que más la necesitan son los anónimos que la configuración ingenua envía casi vacíos. Beaconry la trata como un problema de pipeline del lado del servidor: un baúl propio, cifrado y con barrera de consentimiento lleva los identificadores hacia adelante a lo largo de todo el recorrido de una visitante, la generación de _fbp y _fbc en el servidor planta las cookies que un píxel bloqueado habría establecido, GeoIP rellena la ubicación para los verdaderamente anónimos, y la canonicalización estricta se asegura de que la misma persona produzca el mismo hash en cada canal. El resultado de titular para un anunciante que persigue el Event Match Quality: lo elevas para las visitantes con adblock que un píxel de navegador no puede alcanzar, sin cargar ningún píxel de navegador.