conversor-plataformas

Community node n8n que normaliza payloads de webhooks de plataformas de venda (Hotmart, Guru, Eduzz, Virtu, Greenn, TMB, Kiwify) para um schema canonico de compra.

Package Information

Released: 1 mo ago
Downloads: 283 weeklyΒ /Β 2,898 monthly
Latest Version: 0.10.6
Author: Caio

Documentation

n8n-nodes-conversor-plataformas

Community node Kokopelli ADTK Convert β€” um node, duas actions:

  1. Converter Gateway β€” normaliza webhooks de plataformas de venda
    (Hotmart, Guru, Eduzz, Virtu, Greenn, TMB, Kiwify) para um schema canonico de compra, pronto para
    alimentar upsert_purchase_data() sem remapear campos a cada plataforma.
  2. Converter GTM β€” filtra eventos internos do GTM e normaliza os eventos de
    negocio para o schema de eventos (todos os campos em string).

Ambas aceitam o item do Webhook node ({ headers, body }) e o de fila
RabbitMQ/AMQP
em qualquer forma: { content: { body } } (envelope), { content: <body> }
(body cru direto no content), com content/body como objeto, array ou string JSON
nao-parseada (RabbitMQ sem "JSON Parse Body"). Em todos os casos o body real e
desembrulhado antes do mapeamento.

Status

🟒 7 plataformas implementadas e testadas: Hotmart, Guru, Eduzz, Virtu, Greenn, TMB
e Kiwify β€” todas com compra aprovada e reembolso/estorno cobertos. Auto-deteccao por
user-agent + assinatura do body. Os mapas de status seguem o enum oficial de
cada plataforma (developers.hotmart.com Webhook 2.0.0; docs.digitalmanager.guru;
Greenn/pagar.me; Virtu; docs.kiwify.com.br). PagTrust entra quando houver payload real.

Convencao de status: reembolso/chargeback consumado -> rejected; contestacao
ainda em aberto (Hotmart DISPUTE/protesto, Guru dispute/"Reembolso Sol.",
Kiwify pending_refund/refund_requested) -> pending.

Eduzz β€” dois formatos, ambos suportados (desde 0.9.5, nas DUAS superficies: node
n8n e variavel sGTM conversor-plataformas-gateway.tpl):

formato assinatura evento status vem de
MyEduzz (atual) data.{buyer,price,items} event: "myeduzz.invoice_paid" data.status
Legado "sun" (flat) trans_cod + um de cus_cod/cus_email/product_cod/event_name event_name: "invoice_paid" event_name, fallback trans_status: 3

O legado dispara com user-agent generico (node) e sem o prefixo myeduzz., entao
nao era detectado ate a 0.9.4 β€” toda venda aprovada saia com "Nao foi possivel
auto-detectar a plataforma". Do trans_status numerico so o 3 (paga) e mapeado:
e o unico codigo com evidencia no payload real, e status errado no banco e pior que
coluna vazia.

Action: Converter Gateway β€” schema canonico (saida)

campo tipo observacao
nome string | null
email string | null lowercase/trim
telefone string | null so digitos
valor number | null total da compra, na moeda de moeda
valor_parcela number | null UMA parcela recebida pelo vendedor (valor_total_liquido / parcelas quando ha os dois), na moeda de moeda (0.10.0) β€” ver "Valores financeiros"
valor_total_liquido number | null venda INTEIRA apos as taxas da plataforma (0.10.0) β€” ver "Valores financeiros"
moeda string | null BRL/EUR/USD (Hotmart internacional pode vir EUR/USD)
id_transacao string | null id de dedup/upsert
status pending | approved | rejected | null
nome_produto string | null produto principal
id_produto string | null id do produto principal
plataforma enum hotmart|guru|eduzz|virtu|greenn|tmb|kiwify
utm_source string | null Guru source.utm_*, Eduzz data.utm.* (legado: utm_* com fallback em tracker_utm_*), TMB utm_*, Virtu utm/firstTouchUtm, Greenn utm.* (defensivo), Kiwify TrackingParameters.utm_*
utm_medium string | null
utm_campaign string | null
utm_content string | null
utm_term string | null so Guru envia
xcod string | null rastreamento Hotmart (data.purchase.origin.xcod)
sck string | null 2o codigo de rastreamento: Hotmart (data.purchase.origin.sck) e Kiwify (TrackingParameters.sck)
itens ItemCompra[] detalhamento por produto

ItemCompra: { nome, id, valor, quantidade, tipo }, onde tipo ∈ principal |
order_bump | upsell | downsell | desconhecido. A matriz carrega todos os
produtos do payload
(inclusive bump/downsell); nome_produto/id_produto apontam
pro primeiro item principal (fallback: primeiro item, ex.: transacao 100% bump).

Classificacao do tipo, em ordem de precedencia:

  1. Rotulo nativo do item quando presente e reconhecido (bump/upsell/downsell/
    main, ignorando caixa e os separadores espaco/tab/quebra de linha, _ e -;
    token exato pos-normalizacao). Rotulos genericos de catalogo (product, plan)
    NAO classificam. Nenhuma plataforma suportada documenta upsell/downsell no payload
    hoje (pesquisa 2026-07-16) β€” chegam como compra normal separada; o reconhecimento
    e defensivo/futuro.
  2. Flag de bump da transacao β€” classifica todos os itens sem rotulo nativo
    reconhecido
    (rotulo explicito prevalece) nos mappers multi-item: Guru is_order_bump na raiz (bump = transacao separada), Eduzz
    data.orderBump.isMainSale === false (bump = fatura separada), Virtu
    products[].isOrderBump por produto (defensivo, sem doc publica). Hotmart
    (purchase.order_bump.is_order_bump) e Greenn (sale.bump_id preenchido)
    entregam 1 produto por evento β€” a flag classifica esse unico item.
  3. Heuristica de posicao (duas etapas): o 1ΒΊ item sem tipo explicito =
    principal (a menos que outro item ja seja principal por rotulo), demais =
    order_bump. Item rotulado bump/downsell na posicao 0 nao rouba o posto do
    item nao rotulado. Rotulos sao comparados por token exato pos-normalizacao
    (non-bump/upsell-disabled nao classificam).

Atribuicao por plataforma β€” nao ha regra "ou UTM ou codigo"; cada uma manda
o que o payload dela tem (fonte entre parenteses):

Plataforma utm_* xcod sck
Hotmart β€” βœ“ (data.purchase.origin.xcod) βœ“ (data.purchase.origin.sck)
Kiwify βœ“ (TrackingParameters.*) β€” βœ“ (TrackingParameters.sck)
Guru, Eduzz, TMB, Virtu, Greenn βœ“ β€” β€”

Semantica: xcod e sck sao campos SEPARADOS e independentes β€” na Hotmart
moram no mesmo bloco origin e qualquer um pode vir sozinho (venda de funil
costuma trazer so o sck); o xcod pode encodar varios valores separados por
_. O irmao src da Kiwify nao tem campo canonico e fica de fora.

Ausencia, camada a camada (vale pra sck e pra qualquer campo vazio):

  • schema canonico (PurchaseData): a chave existe SEMPRE β€” emptyPurchase
    inicializa em null.
  • saida do node: o null cai junto com os demais vazios (regra "chaves
    vazias omitidas", mais abaixo) β€” a chave nao aparece no item.
  • destino: insert que hoje nao tem coluna sck segue funcionando sem
    mudanca; pra PERSISTIR o valor, o destino precisa da coluna correspondente.

Formatos de entrada: o node desembrulha o item do Webhook node
({ headers, body }) e o de fila RabbitMQ/AMQP em qualquer forma β€” content
com envelope ({ content: { body } }) ou body cru ({ content: <body> }), e
content/body como objeto, array ou string JSON. Fila e webhook produzem a
mesma saida. Obs: em fila com body cru no content nao ha headers HTTP β€” e, desde 0.9.3, o
header nao e fonte de IP nem no webhook (a menos que Usar IP Do Header seja ligado), entao
o ip vem do payload: o produtor precisa injetar ip_override no body.

Moeda: o valor permanece na moeda original da transacao; quem soma receita
downstream deve agrupar/converter por moeda. Sem alteracao de banco.

Kiwify β€” valores em CENTAVOS: Commissions.charge_amount: 6277 = R$ 62,77.
E a unica plataforma suportada com essa escala; o mapper divide por 100
(centavosParaReais), entao valor sai em reais como o das outras. O contrato
e ESTRITO: centavo e inteiro ou string de digitos β€” formato monetario
(1.234,56) e fracao de centavo (6277.5) viram null, nao valor convertido
(pelo parser generico 1.234,56 daria 12.3456, 100x menor). Campo vazio e
visivel; valor errado e silencioso. A base do valor e charge_amount (total cobrado do cliente), com
fallback product_base_price β€” settlement_amount/my_commission sao repasse
ao produtor e ficam de fora. A deteccao e so por body (order_id + objetos
capitalizados Customer + Product/Commissions): o user-agent da Kiwify e
axios/<versao>, generico demais pra ser assinatura. A Kiwify entrega 1
produto por evento
(webhook configurado POR produto), entao o order bump vem
em evento proprio e commissioned_stores (split produtor/coprodutor/afiliado)
NAO vira item.

Valores financeiros (0.10.0, sanidade em 0.10.x)

Tres valores distintos, todos na moeda de moeda. valor nao mudou de nome nem de fonte; na sanidade
0.10.x o parser passou a ler "1,234.56" (US) como 1234.56 e numero nao finito como ausente.

campo o que e regra
valor bruto total da venda inalterado
valor_parcela UMA parcela recebida pelo vendedor com valor_total_liquido e quantidade de parcelas legivel: valor_total_liquido / parcelas a centavo β€” sem parcelamento (1x, PIX/boleto) e o proprio liquido; liquido > 0 que arredonda a parcela para 0 -> ausente; liquido 0 -> 0. Sem liquido, ou sem quantidade legivel, vale a parcela da origem: explicita, arredondada a centavo e aceita se couber no valor a centavo β€” e for menor que ele quando a quantidade e 2+ ou veio ilegivel; senao valor / parcelas a centavo. Quantidade AUSENTE vira 1x so em PIX/boleto; quantidade ilegivel ("12x", 0) nao divide. Sem valor conhecido, explicita 0 -> ausente
valor_total_liquido venda INTEIRA apos as taxas da plataforma campo que a plataforma rotula como liquido, arredondado a centavo e aceito se estiver em (0, valor] a centavo β€” sem valor conhecido, qualquer liquido > 0 e aceito, sem teto; senao valor - taxa, com taxa em [0, valor] (taxa 0 = isencao, liquido = bruto). Liquido explicito recusado com taxa 0 -> ausente (a conta contradiria a origem)

Mudancas de comportamento da sanidade 0.10.x: parcela e liquido EXPLICITOS passaram a sair
arredondados a centavo (em 0.10.0 saiam como vieram). Os operandos das contas β€” taxas, entradas e as
explicitas lidas por Eduzz legado e TMB β€” passam por um parser ESTRITO: number finito ou texto so com
sinal, prefixo de moeda (R$, US$ ou $), digitos e separadores; "0abc", [1] ou true deixam o
campo derivado ausente em vez de virar 0 ou 1. Limitacao conhecida: notacao cientifica em texto
("1e2") nao e lida β€” em valor o parser tira a letra e le 12 (o template sGTM faz igual); nos
operandos das contas o campo derivado fica ausente.

Mudanca apos a 0.10.5 β€” valor_parcela e a parcela RECEBIDA: a parcela que o comprador paga
carrega os juros do parcelamento, que nao chegam ao vendedor. Guru 12x real: installments.value
120.1 (x 12 = 1441.2, com 244.19 de juros), valor 1441.19, valor_total_liquido 1197 ->
valor_parcela 1197 / 12 = 99.75 (antes 120.1). Com taxa, ela sai antes: Guru 1x de 797 com
marketplace_value 41.05 -> parcela 755.95. valor e valor_total_liquido nao mudaram.

Campo rotulado comissao (Kiwify my_commission, Eduzz pro_value, Hotmart PRODUCER) e a
fatia de UMA conta no rateio com coprodutor/afiliado β€” nao e o liquido da venda e nao e usado.
Contas saem arredondadas a centavo; operando ausente deixa o campo ausente (nunca NaN).

Na coluna valor_parcela, "recebida" = valor_total_liquido / parcelas (regra acima); o que vem
depois de "sem liquido" e a parcela da origem, usada quando falta o liquido ou a quantidade.

Plataforma valor_parcela valor_total_liquido evidencia
Guru recebida por installments.qty (fila 12x: 1197 / 12 = 99.75); sem liquido: payment.installments.value (senao valor / installments.qty) payment.gross - tax.value - marketplace_value (sem gross: total - installments.interest; 1x: total). payment.net fica de fora: e igual ao total e inclui os juros 14 webhooks reais + fila 12x com juros
Kiwify recebida por installments (12x: 179.3 / 12 = 14.94); sem liquido: charge_amount / installments (juros inclusos: e o que o comprador paga) min(product_base_price, charge_amount) - kiwify_fee (juros nao chegam ao vendedor; cupom baixa o cobrado); parcelado com cupom -> ausente; campo com *_currency diferente de moeda conta como ausente, sem vetar os outros 6 vendas reais
Eduzz legado β€” (payload nao traz parcelas; metodo e codigo numerico) trans_value - eduzz_value - other_values; eduzz_value 0 em fatura com valor = nao calculada -> ausente; other_values ilegivel ou negativo -> ausente 12 webhooks reais
Eduzz novo sem liquido no evento: valor / data.installments (bankSlipInstallment.totalInstallments so com installments ausente) β€” (o invoice_paid nao traz taxa) doc oficial
Hotmart recebida por purchase.payment.installments_number; sem liquido (sem MARKETPLACE na moeda): valor / installments_number (valor = price, sem juros) valor - commissions[MARKETPLACE], so na mesma moeda; sem price.value (valor = full_price, com juros), a base e o menor entre full_price e original_offer_price, os dois na moeda de moeda doc + fila real (ARS x USD -> ausente) + webhooks reais 3x
Greenn recebida por sale.installments (PIX: 8.6 / 1); sem liquido: valor / sale.installments (total inclui os juros: e o que o comprador paga) sale.seller_balance (senao o menor entre total e amount, - fee) doc + webhooks reais (PIX e cartao 10x)
TMB sem liquido no webhook: valor_parcela; sem ele, valor / parcelas so sem entrada (entrada ilegivel ou negativa tambem nao divide) β€” (taxa_administracao sem unidade documentada) doc oficial
Virtu PIX/boleto (1x): recebida = sellerNetAmount (53.94); cartao (payload sem quantidade): payment.installmentValue sellerNetAmount webhook real (sem doc publica)

Destino com insert automatico: as duas chaves entram na saida sempre que ha dado. Node
Postgres em Map Automatically recusa chave sem coluna β€” antes de atualizar o pacote numa
instancia assim, crie as colunas (valor_parcela numeric, valor_total_liquido numeric) ou
mapeie as colunas explicitamente. O n8n nao atualiza community node sozinho.

Parametros da action Converter Gateway:

  • Plataforma β€” Auto-detectar (pela estrutura do JSON) ou plataforma fixa.
    Fila com plataformas misturadas (ex: purchase_data) exige Auto-detectar β€”
    plataforma fixa aplica SEMPRE aquele mapper. Desde 0.8.1, payload cuja assinatura
    casa com OUTRA plataforma gera erro acionavel em vez de linha lixo silenciosa no
    banco; payload que nenhuma assinatura reconhece segue processando (o override
    manual existe pra isso).
  • Itens Como JSON β€” serializa itens como string JSON em vez de array.
    Ligue quando o destino for uma coluna json/jsonb no node Postgres nativo:
    com array JS o driver envia text[] e o insert falha
    (column "itens" is of type jsonb but expression is of type text[]); com a
    string JSON o Postgres faz o cast automatico text -> jsonb. Dispensa o node
    Edit Fields intermediario. Desligado (default), itens segue array de objetos
    (consumo em fluxo n8n, ex: Split Out).
  • Campos A Excluir β€” campos a remover da saida (lista = campos do schema de
    compra). Aplica depois de "Itens Como JSON". Vazio (default, e workflow antigo sem o
    parametro) = nao remove nada. Use quando a tabela destino nao tem a coluna: com o node
    Postgres em Map Automatically
    , toda chave da saida precisa de coluna, e desde 0.10.0 a
    saida traz valor_parcela e valor_total_liquido β€” sem essas colunas o upsert falha por
    coluna inexistente. Marque as duas aqui (ou crie as colunas). Parametro separado do
    "Campos A Excluir" do GTM: a selecao feita numa action nao vale na outra.
  • Manter Payload Original β€” anexa o JSON bruto em _original.

Action: Converter GTM

Processa eventos vindos do GTM (server-side). Filtra os eventos internos do
GTM (sem event_name) e normaliza os de negocio para a linha de eventos, com
todos os campos em string (numero -> string, objeto -> JSON, ausente -> "").

Parametros:

  • Eventos Permitidos β€” allowlist de event_name separada por virgula. Vazio = todos
    os eventos passam (inclusive sem nome). Use para restringir aos eventos de negocio.
  • Eventos Excluidos β€” blocklist de event_name separada por virgula. Vazio = nenhum
    excluido. Tem prioridade sobre "Eventos Permitidos" (um evento que esteja nas duas
    listas e excluido). Use quando for mais facil listar os poucos eventos a descartar
    (ex: internos do GTM gtm.dom, gtm.load, gtm.js) do que escolher todos os permitidos.
  • Campos De Saida β€” escolha quais campos manter no output. Vazio = todos.
  • Campos A Excluir β€” campos a remover do output. Aplica depois de "Campos De
    Saida". Vazio = nao remove nada. Use para descartar os poucos campos que a tabela destino
    nao tem (ex: event_name) sem precisar escolher manualmente todos os que ficam β€” evita o
    erro Column 'event_name' does not exist in selected table no insert downstream.
  • Usar IP Do Header (desde 0.9.3, default desligado) β€” le x-real-ip/x-forwarded-for
    como ultima fonte da coluna ip, so quando nenhuma das quatro fontes do payload
    (ip_override, ip, user_data.ip_override, user_data.ip) trouxe valor. Ligue apenas se o
    n8n recebe o webhook DIRETO
    , sem proxy na frente. Atras de proxy (sGTM/Cloud Run, Traefik,
    Cloudflare) esses headers carregam o endereco da infraestrutura, nao o do lead: gravar isso
    contamina geo e atribuicao de todo evento com um IP unico. Desligado, sem IP no payload a
    coluna sai vazia (NULL no banco β€” melhor que IP errado).
  • IPs/Faixas De Proxy a Ignorar (desde 0.9.2; subordinado a "Usar IP Do Header" desde
    0.9.3
    ) β€” IPs ou faixas CIDR separados por virgula (35.191.0.0/16, 130.211.0.0/22). Aparece
    na UI somente com o toggle ligado, porque atua exclusivamente no caminho do header. Faixas
    listadas sao puladas na cadeia do x-forwarded-for; se nada sobrar, ip sai vazio.

    Preferir o toggle desligado a manter esta lista. Foi a tentativa de 0.9.2 de salvar o
    caminho do header, mas lista de faixa de nuvem drifta: o provedor troca de bloco e o IP de
    proxy volta a entrar em silencio. Se ha proxy na frente do n8n, o caminho duravel e deixar
    Usar IP Do Header desligado e mandar o IP do lead no payload via ip_override.
    Escopo (desde 0.9.4): SO esta lista e exclusiva do header. A checagem de endereco valido
    e o descarte de faixa privada/reservada (10.x, 127.x, 192.168.x, 169.254.x,
    100.64/10, fe80::) valem para as duas origens β€” header e payload. Ja o ipsProxy
    continua so no header, porque nomeia a infra de transporte, que nao tem equivalente do
    lado do payload: o mesmo 203.0.113.9 sobrevive vindo em ip_override e e descartado vindo
    em x-real-ip.
    Ate 0.9.3 o payload passava sem filtro, sob o argumento de que ip_override era uma
    declaracao EXPLICITA do produtor. O argumento caiu: o produtor e um script do GTM que
    interpola variavel e ja mandou undefined (dai o tratamento de placeholder da 0.9.2) e
    10.0.0.1. Agora um candidato invalido cede a vez ao proximo da cadeia em vez de virar
    coluna β€” mesmo padrao ja usado em genero. A precedencia entre candidatos validos nao
    mudou.

  • Manter Payload Original β€” anexa o body bruto em _original.

Allowlist x blocklist (eventos) e Saida x Excluir (campos): os dois pares sao
complementares e combinaveis. Eventos: passa se (permitidos vazio OU nome ∈ permitidos)
e nome βˆ‰ excluidos. Campos: parte de todos β†’ mantem so os de "Campos De Saida" (se
preenchido) β†’ remove os de "Campos A Excluir". Tudo vazio = comportamento original.

Cadeia de fontes (desde 0.9.2): topo do body β†’ user_data β†’
user_data.address / url_parameters β†’ query da page_location. O sGTM entrega o bloco
de usuario aninhado em user_data, e ate 0.9.1 o node lia so o topo do body β€” id, geo,
user agent, data de nascimento e o IP real eram descartados em silencio. Cada fonte nova
entra depois da que ja existia, entao nenhum payload que ja funcionava muda de resultado.
Um alias presente-porem-vazio deixou de mascarar um preenchido mais adiante na cadeia.

Placeholder do produtor vira NULL (desde 0.9.2): quando a camada de tracking nao resolve
uma variavel, o JavaScript do GTM interpola o texto undefined/null/NaN e ele chega como
valor do campo (payload real: sete colunas de UTM com a palavra "undefined"). Um campo
cujo valor INTEIRO seja um desses (em qualquer caixa) e tratado como ausente β†’ chave omitida
β†’ NULL no banco, em vez de sujeira que mente no relatorio e escapa de um IS NULL.

Mapeamento principal: event_name ← event_name;
senha_atendimento ← senha_de_atendimento;
st_user_id ← st_user_id e user_id ← user_id (id de tracking vs. id do usuario no
sistema β€” campos distintos desde 0.6.8; fallback user_data.* desde 0.9.2);
email ← email/email_address/user_data.email_address (normalizado: lowercase + trim);
telefone ← telefone/phone/phone_number/user_data.phone_number (normalizado: so digitos);
nome/sobrenome ← first_name/last_name (fallback user_data.address.*);
cidade/estado/pais ← campo homonimo em pt-BR no topo, fallback
user_data.address.city/.region/.state/.country (desde 0.9.2);
zipcode ← zipcode/cep ou user_data.address.postal_code/.zip/.zp (normalizado:
CEP brasileiro fica so com digitos, 26255-000 β†’ 26255000; codigo estrangeiro alfanumerico
e preservado em UPPERCASE);
data_nascimento ← data_nascimento/birth ou user_data.birth/.birthdate/.db
(normalizado para ISO YYYY-MM-DD; aceita YYYYMMDD do sGTM, ISO e DD/MM/YYYY como
padrao BR; data impossivel e hash SHA-256 β€” o db do Meta CAPI chega hasheado β€” sao
descartados, nunca "corrigidos");
user_agent ← user_agent (fallback user_data.user_agent);
language ← language;
genero ← gender/genero (topo ou user_data.gender/user_data.ge β€” ge e o
campo do Meta Conversions API), decodificado (desde 0.6.9): as plataformas de
tracking enviam o genero hasheado em SHA-256 (lowercase+trim), entao os dois hashes
conhecidos sao revertidos pra letra canonica β€” sha256("f") β†’ f, sha256("m") β†’ m.
Tambem aceita o valor ja em claro (f/m/female/male/feminino/masculino);
desconhecido/vazio β†’ chave omitida;
xcod ← xcod e sck ← sck (mesma hierarquia das UTMs no payload do GTM);
utm_*/ad_id/utm_id ← campo homonimo no topo, fallback url_parameters.* (desde 0.9.2);
fbc/fbp/ttp ← common_cookie._fbc/._fbp/._ttp (o common_cookie bruto nao vai pro
output β€” so serve de fonte; ttp e o cookie do TikTok Pixel, irmao do fbp, desde 0.9.2);
fbclid/gclid/gbraid/wbraid/ttclid/click_id ← campo direto, url_parameters ou query
da page_location. O fbclid e guardado cru: derivar fbc dele (fb.1.<ts>.<fbclid>)
produziria um valor que nao casa com o cookie que o pixel gravou no browser β€” pior que nao ter;
pagina_origem ← page_location.

ip β€” so o payload e fonte por padrao (desde 0.9.3). Ordem exata: ip_override (topo) β†’
ip (topo) β†’ user_data.ip_override β†’ user_data.ip. As duas fontes que ja existiam ficam na
frente das aninhadas, pra que um payload que ja resolvia por ip nao mude de resultado. Nenhuma
das quatro trouxe valor β†’ ip vazio β†’ NULL, que e melhor que um IP errado.

Desde 0.9.4 cada candidato do payload precisa ser utilizavel β€” endereco valido e fora de
faixa privada/reservada. Um candidato reprovado cede a vez ao proximo da cadeia (mesmo
padrao do genero), entao ip_override: "10.0.0.1" + ip: "189.4.20.7" grava 189.4.20.7,
e nao mais o endereco privado. Reprovados todos β†’ NULL. A precedencia entre candidatos
validos e exatamente a de antes.

O header x-real-ip/x-forwarded-for e opt-in pelo parametro Usar IP Do Header
(desligado por padrao). Atras de sGTM/Cloud Run/Traefik ele carrega o IP do proxy, nao o do
usuario: ate 0.9.1 todos os eventos desse arranjo gravavam o mesmo endereco de datacenter,
contaminando geo e atribuicao; 0.9.2 tentou corrigir filtrando faixas, mas manter lista de bloco
de nuvem drifta. Com o toggle ligado, o node percorre a cadeia cliente, proxy1, proxy2 da
esquerda pra direita e usa o primeiro endereco valido que nao seja privado/reservado nem
casar as faixas de "IPs/Faixas De Proxy a Ignorar".

Mudanca de comportamento em 0.9.3 (maior que a de 0.9.2): workflow que dependia do header
para preencher ip passa a gravar NULL β€” a chave e omitida do output. Quem recebe o webhook
direto, sem proxy, deve ligar "Usar IP Do Header" ao atualizar; quem esta atras de
proxy
deve mandar o IP do lead no payload via ip_override (e o que a camada de tracking
captura do usuario). Uma lista ipsProxy herdada de 0.9.2 fica inerte enquanto o toggle
estiver desligado β€” ela nao reativa o header.

O x-forwarded-for e um header controlado pelo cliente e o ip_override vem do mesmo
payload β€” a coluna ip e dado de atribuicao best-effort, nunca controle de seguranca.
Desde 0.9.3 isso vale ainda mais forte: com o header fora da jogada por padrao, a coluna
passa a ser 100% auto-declarada por quem POSTa o evento (o header ao menos era atribuido
pela infra). Quem alcancar a URL do Webhook node escolhe o valor β€” logo, autenticacao/validacao
de origem no webhook e responsabilidade da configuracao do n8n, e ip nunca deve alimentar
decisao de acesso, antifraude ou rate-limit. O crivo da 0.9.4 melhora a qualidade do dado
(lixo e faixa privada param de entrar); ele nao e controle de seguranca e nao autentica
nada β€” um endereco publico qualquer, forjado, continua passando por definicao.
Os campos *_entrada (primeiro toque) vem dos campos homonimos do payload
(utm_source_entrada, utm_medium_entrada, utm_campaign_entrada,
utm_content_entrada, utm_term_entrada, utm_id_entrada, ad_id_entrada),
persistidos pela camada de tracking (GTM/sGTM). Ficam vazios quando o payload
nao os envia
β€” desde 0.6.10 nao sao mais derivados da query da page_location
(que carrega o toque ATUAL/last-click, nao o primeiro toque; derivar de la duplicava
os UTMs atuais nas colunas _entrada).

data_criacao ← timestamp (desde 0.6.7; fuso de Sao Paulo desde 0.6.11): o
timestamp do evento (epoch em segundos, ms ou microssegundos β€” detectado pela
magnitude; ou uma string de data ISO com fuso explicito) e convertido para ISO 8601
no fuso America/Sao_Paulo
, com offset explicito (YYYY-MM-DDTHH:MM:SS.sss-03:00),
pronto para inserir numa coluna timestamp/timestamptz do Postgres. Numa coluna
timestamp (sem tz) o Postgres grava a hora local de Sao Paulo; numa timestamptz
grava o instante correto β€” nos dois casos o valor deixa de aparecer 3h adiantado
(antes saia em UTC). Assim o registro guarda a data do evento, nao a do insert. Se
nao houver timestamp valido, data_criacao sai vazio β†’ a chave e omitida β†’ o
DEFAULT da coluna no banco assume. O id continua gerado pelo banco (auto-id). O
campo timestamp cru permanece no output (use Campos A Excluir se quiser so o
data_criacao).

Colunas novas em 0.9.2 β€” o banco precisa acompanhar. data_nascimento, zipcode,
ttp, fbclid, click_id e language sao chaves novas no output. No modo
"Map Automatically" do node Postgres, chave sem coluna correspondente derruba o insert
inteiro
. DDL pronto em docs/sql/eventos-alter-0.9.2.sql;
enquanto o ALTER TABLE nao for aplicado, liste os campos em "Campos A Excluir".

LGPD β€” minimizacao na origem. A partir de 0.9.2 o node persiste dado pessoal que antes se
perdia: data de nascimento, CEP, IP real e user agent, alem de geo e ids. Colete so o que tiver
finalidade e base legal β€” o que nao for usar deve ser cortado em "Campos A Excluir"
(descartar na origem sai mais barato que expurgar depois). A saida carrega PII em claro,
apropriada pro banco analitico do proprio cliente; nao ligue esta saida direto em Meta CAPI /
Google EC / TikTok Events / Stape sem aplicar SHA-256 nos campos de identificacao.

Saida tudo em texto + chaves vazias omitidas: todo campo sai como string
("para facilitar manipulacao") β€” numero/boolean viram texto, recursivamente
(incl. valor, quantidade e os campos de itens dos gateways). Campo ausente
na entrada nao aparece no output (sem a chave, em vez de ""/null), em
qualquer nivel. Vale para GTM e para os 4 gateways. Quando o banco precisar do
campo sempre presente, use COALESCE/?? '' no insert.

Desenvolvimento

npm install
npm run dev                       # n8n-node dev: sobe n8n com o node + hot-reload
npm test                          # vitest (fixtures sanitizadas)
npm run smoke -- caminho.json     # roda os mappers contra um export real do n8n
npm run smoke:gtm -- evento.json  # linha de evento GTM ja na forma final do output
                                  # (IPS_PROXY="35.191.0.0/16" espelha o param do node)
npm run lint
npm run build
npm run release                   # publica no npm

Instalacao (instancia unica self-hosted)

  1. Settings β†’ Community Nodes β†’ Install
  2. Digitar n8n-nodes-conversor-plataformas
  3. Aceitar o aviso de risco β†’ Install

Licenca

MIT

Discussion