O payload canônico de evento
O SupremeTracking aceita eventos de diferentes fontes, incluindo tags de navegador e webhooks. Cada fonte é traduzida para o mesmo Supreme Event Payload canônico antes de o Node processá-la.
Essa regra de canônico-primeiro mantém o código compartilhado do pipeline genérico. Tradutores de fonte tratam as diferenças de entrada, e cada integração de destino mapeia o evento canônico para o formato da própria plataforma.
Um evento canônico representativo
Os campos opcionais exatos dependem da fonte e do contexto disponível, mas um evento normalizado tem este formato:
{
"event_id": "evt_example_123",
"event_name": "purchase",
"event_time": 1730000000,
"channel": "webevents",
"action_source": "browser",
"provider": "direct",
"property_id": 1,
"source": "browser",
"source_platform": "direct",
"context": {
"stuid": "0123456789abcdef0123456789abcdef",
"session_id": "1.1730000000.0123456789abcdef",
"page_url": "https://shop.example.com/thank-you",
"page_title": "Order complete",
"referrer": "https://shop.example.com/checkout",
"utm": {},
"tracking_ids": {}
},
"user": {
"email": "buyer@example.com",
"phone": null
},
"params": {
"transaction_id": "order_123",
"value": 99.9,
"currency": "USD",
"items": []
}
}
Essa é a representação normalizada do Node, não um corpo de requisição bruto para copiar no endpoint de ingestão.
Campos principais
event_ididentifica o evento e viabiliza a deduplicação.event_nameé a ação canônica, comopage_view,lead,add_to_cartoupurchase.event_timeé normalizado para segundos Unix.channelidentifica a família de ingestão: web events, webhooks, eventos internos ou imports.action_sourcedescreve onde a ação aconteceu, como browser ou server.provideridentifica o provedor normalizado associado à fonte.property_idatribui o evento a uma Property ativa do Node. Não há fallback: um evento que não resolve para uma Property válida e ativa é rejeitado durante a validação. Ele é carimbado pelo servidor — do?pid=do loader no navegador, da credencial numa escrita servidor-a-servidor — nunca lido do payload.sourceesource_platformpreservam contexto útil de origem.contextcarrega página, sessão, identidade de primeira parte, atribuição e contexto de requisição quando disponíveis.usercontém campos normalizados do usuário. Capitalização de e-mail, formatação de telefone e aliases são normalizados pelo Node.paramscontém valores específicos do evento, como ID de transação, value, currency, status e items. Numlead, os campos do próprio Lead vivem emparams.lead.
Não adicione lógica de destino específica de plataforma a esse formato compartilhado. Meta, GA4, Google Ads, CRM e outros handlers são donos do próprio mapeamento canônico-para-plataforma dentro dos seus módulos.
Eventos de Lead
Um lead é o único evento canônico que carrega um bloco de pipeline de vendas. Quatro portas o produzem — uma chamada window.supremeSend escrita à mão, o template do Google Tag Manager, o snippet da skill de Web Events e a API de Leads — e todas as quatro convergem para o mesmo evento canônico. Não existe variante do formato abaixo por porta.
O bloco canônico de Lead
event_name: "lead"
user: identidade — email, phone, first_name, last_name, full_name, company, job_title,
street, street2, city, state, zip, country (normalizados pelo Node)
params: {
value?: número ≥ 0 (unidades maiores, ex. 150.5)
currency?: "BRL" — ISO-4217, 3 letras maiúsculas; obrigatório quando value vem
lead?: {
status?: "new" | "contacted" | "qualified" | "won" | "lost"
source?: string ^[a-z0-9][a-z0-9_.-]{0,63}$
source_lead_id?: string 1–191 caracteres, sem caractere de controle
custom_fields?: objeto raso (regras abaixo)
}
}
Pelo navegador — à mão, pela tag ou pela skill — o wire é este:
window.supremeSend({
payload: {
events: [{ name: 'lead', id: '<event id ≤ 50 bytes UTF-8, opcional>', data: { params: {
value: 150.5, currency: 'BRL',
lead: { status: 'qualified', source: 'landing-page', source_lead_id: 'form-2026-0001',
custom_fields: { plan_interest: 'pro', employees: 12, newsletter: true } }
} } }],
context: { user: { email: 'maria@example.com', phone: '+5511987654321',
first_name: 'Maria', last_name: 'Silva',
company: 'Acme', job_title: 'Head of Growth' } }
}
});
Identidade e campos do Lead são dois blocos diferentes
A identidade vai em context.user na entrada e aparece como user no evento canônico. Os campos do próprio Lead vão em params.lead.
É params.lead e não params.status porque params.status já significa status de pagamento de uma compra. O status de pipeline de um Lead precisa do próprio espaço, não de um segundo sentido para uma chave existente.
Para eventos lead, o Node lê de context.user o conjunto de identidade completo: email, phone, first_name, last_name, full_name, company, job_title, street, street2, city, state, zip e country. Para qualquer outro evento, continua lendo só email, phone, first_name e last_name — então um payload que não é lead produz exatamente o evento canônico que já produzia.
Um lead exige e-mail ou telefone. Sem nenhum dos dois, o evento é recusado.
A normalização é a do próprio Node: e-mail e os campos de nome ficam em minúsculas, e o telefone é reduzido a dígitos mantendo um + inicial, então +55 (11) 98765-4321 vira +5511987654321. Os demais campos de identidade são aparados, não passados para minúsculas.
source_lead_id é uma referência ao sistema de onde o lead veio. Nunca substitui o identificador que o Node tem para aquela pessoa, e nunca vira external_id em um payload de destino.
status só muda a linha do Lead quando vem no payload — uma captura que o omite nunca o reseta. O default new vale só na criação da linha.
custom_fields
params.lead.custom_fields carrega os campos que são seus, não do produto:
- Um objeto JSON, nunca uma lista. As chaves atendem
^[a-z][a-z0-9_]{0,63}$, e são no máximo 50. - Os valores são só escalares: string, inteiro, float finito, boolean ou
null. Nenhum array, nenhum objeto aninhado. - Tamanho: o objeto codificado em JSON com Unicode e barras não escapados precisa ter no máximo 16384 bytes.
- Chaves reservadas, recusadas se usadas:
email,phone,first_name,last_name,full_name,company,job_title,street,street2,city,state,zip,country,gender,birth_date,external_id,lead_source,ip_address,user_agent,lead_id,status,source,source_lead_id,value,currency,property_id,stuid,event_id,event_name,event_time,channel,action_source,provider,source_platform,metadata,context,user,params,attribution,utm_source,utm_medium,utm_campaign,utm_content,utm_term,gclid,gbraid,wbraid,gad_source,fbclid,fbc,fbp,msclkid,ttclid,rdt_cid,srsltid,ga_client_id,ga_session_id,ctwa_clid,delivery,session_id,page_url,referrer,created_at,updated_at,first_seen_at,last_seen_at,is_sandbox.
Numa captura seguinte o objeto é mesclado, não substituído: chave presente na nova captura sobrescreve, chave ausente dela é preservada, e chave enviada com null remove. O resultado mesclado também obedece aos dois limites — se não obedecer, a captura inteira é recusada, não sobrando nem evento nem linha.
Códigos de erro
| Código | Quando |
|---|---|
lead_block_not_object |
params.lead presente e não é objeto |
lead_status_invalid |
status fora do conjunto |
lead_source_invalid |
source fora da regex |
source_lead_id_invalid |
vazio, com mais de 191 caracteres, ou com caractere de controle |
custom_fields_not_object |
custom_fields presente e não é objeto |
custom_field_key_invalid |
chave fora da regex |
custom_field_key_reserved |
chave reservada |
custom_field_value_not_scalar |
valor array ou objeto (ou float não finito) |
custom_fields_too_many |
mais de 50 chaves (na captura ou no resultado mesclado) |
custom_fields_too_large |
mais de 16384 bytes (na captura ou no resultado mesclado) |
value_invalid |
value não numérico, negativo ou não finito |
currency_invalid |
currency fora de ^[A-Z]{3}$ |
currency_required |
value presente sem currency |
identity_required |
nem e-mail nem telefone |
event_id_too_long |
event id com mais de 50 bytes UTF-8 |
Severidade por canal
São duas regras diferentes, e há um motivo:
- Um bloco
params.leadinválido — qualquer códigolead_*,source_*oucustom_*acima — recusa o evento inteiro, em todos os canais. Nada legado manda esse bloco, então nada quebra por ser estrito. - Um
valueoucurrencyinválido é recusado pela API de Leads e pelas superfícies (o template do Google Tag Manager, a skill de Web Events). No caminho de navegador do Node, o evento segue como hoje e a linha do Lead simplesmente não recebe valor nem moeda.valueecurrencyjá chegam em produção em payloads delead, então não podem passar a derrubar eventos.
Paridade navegador/API
O mesmo conteúdo produz o mesmo evento canônico e o mesmo estado final do Lead pelas duas portas. O que difere é só a proveniência, e difere por construção: pela API de Leads o evento chega com channel: "webhooks", action_source: "server", provider: "leads_api" e source_platform: "leads_api", e o event_id dele deriva do header Idempotency-Key quando um é enviado.
Um Lead é uma linha por pessoa por Property. A identidade — nome, e-mail, telefone, empresa, cargo, endereço — não mora nessa linha: ela fica com as identidades e é lida por um join.
Carimbado pelo servidor, nunca pelo payload
Estes nunca são tirados de um payload de Lead:
property_id— do?pid=do loader no navegador, da credencial na API de Leads. Umproperty_idno corpo da API é recusado comounknown_field.channel,action_sourceeprovider— atribuídos pela via de ingestão que recebeu o evento.lead_idestuid— cunhados e resolvidos pelo Node.
O que é ingerido vs. o que é armazenado
Tudo acima descreve o payload que o Node recebe. A forma como ele armazena e relê esse evento é outra. A partir da 3.7.0, os campos que viviam duplicados dentro de custom_payload passaram a morar em colunas da própria linha do evento e em tabelas próprias. O evento canônico de ingestão não mudou — o armazenamento mudou.
Quem só usa o Console não precisa fazer nada com essa distinção. Ela importa para quem lê o banco por fora: uma consulta própria, uma integração externa, um export.
Onde cada parte vive hoje
| No payload de ingestão | Onde fica armazenado desde a 3.7.0 |
|---|---|
context.page_url, context.user_agent, context.ip_address |
colunas na linha do evento |
context.utm.* |
colunas utm_source, utm_medium, utm_campaign, utm_term, utm_content |
channel, action_source, provider, source_platform |
colunas na linha do evento |
| campanha, adset, anúncio, placement, rede e tipo de campanha | colunas campaign_id, adset_id, ad_id, placement, network, campaign_type |
o conjunto channel_* e attribution_platform |
colunas na linha do evento |
context.tracking_ids — entre eles gclid, gbraid, wbraid, gad_source, fbclid, fbp, fbc, ga_client_id, ga_session_id, ttclid e msclkid |
tabela attribution_identifiers |
user |
tabela identities |
params.lead (eventos lead) |
a linha de Lead da Property — uma por pessoa — com a identidade continuando em identities |
| os parâmetros de uma compra | purchases.platform_data |
| a sessão | tabela sessions, mais as colunas de sessão na linha do evento |
params, page.title, page.referrer, screen, ingest_host, metadata |
continuam em custom_payload |
context.cookies (o jar cru do navegador) |
não é persistido — veja abaixo |
custom_payload virou resíduo, não o evento inteiro
Antes da 3.7.0, ler custom_payload era ler o evento. Hoje essa coluna guarda apenas o que não tem outro lugar para morar: params, page.title, page.referrer, screen, ingest_host e metadata. Quando não sobra nada, ela fica NULL.
Quem consulta custom_payload diretamente recebe só essa última linha da tabela acima — não o evento. Os parâmetros de uma compra nem isso: eles vivem em purchases.platform_data, que é a cópia autoritativa e acompanha mudanças de status, enquanto a cópia congelada dentro do evento não acompanhava.
Se a sua integração ou o seu relatório lê custom_payload esperando o evento inteiro, é a leitura que precisa mudar de fonte.
O Console remonta o evento na leitura
A aba Raw JSON do detalhe do evento não imprime custom_payload. Ela mostra o evento remontado a partir de todas as fontes da tabela acima — as colunas da linha, attribution_identifiers, identities, purchases e a sessão. É por isso que ela mostra mais do que custom_payload contém, e é a referência de o que o Node guardou sobre aquele evento.
O que fica do jar de cookies
O jar de cookies cru é dado de trânsito: chega no bootstrap, serve o pipeline e não é gravado em lugar nenhum. Antes de evaporar, o que carregava identidade é destilado e persistido:
_fbp, lido do cookie de mesmo nome._fbc, lido do cookie quando ele existe e sintetizado a partir do parâmetro?fbclid=da URL quando não existe — ou seja, parte dos valores guardados nunca foi um cookie.ga_client_idega_session_id, extraídos dos cookies_gae_ga_*.stuid, lido do cookie próprio do Node.
Cookies que não correspondem a nenhum identificador conhecido — os de terceiros, os do seu próprio site — não são persistidos em lugar nenhum. attribution_identifiers é um registro de identificadores que às vezes vêm de cookies, não um espelho do jar do navegador.
Guardar o identificador em vez do cookie é uma decisão de projeto: o Node fica com o que serve à atribuição e não acumula o resto do navegador do seu visitante.
Os cinco estágios do pipeline
Depois da tradução, EventPipeline::process() roda estes estágios em ordem:
- Filter aplica regras de rejeição antes de qualquer outro processamento.
- Validation verifica se o evento canônico é aceitável.
- Enrichment resolve e adiciona identidade e contexto disponíveis localmente.
- Persistence armazena o evento resultante no Node.
- Forwarding passa o evento canônico para cada handler de integração ativo.
Como Persistence roda antes de Forwarding, um evento pode estar visível no Console mesmo quando um destino depois o rejeita ou falha em recebê-lo. Use os logs do Node e o próprio diagnóstico do destino para investigar essa segunda metade.
Do GTM a um destino
O template atual do GTM monta um envelope de navegador e chama window.supremeSend. O Node traduz essa entrada para o formato canônico acima. Saiba como configurar a fonte em Connect Google Tag Manager (em inglês).
Os módulos de Forwarding então leem o evento canônico e montam os payloads de destino. Saiba como os módulos são separados em Visão geral de integrações.
Para inspecionar o resultado no seu próprio Node, verifique seu primeiro evento.