npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

n8n-nodes-conversor-plataformas

v0.9.4

Published

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.

Readme

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.

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 | | 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.*, 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 tipoprincipal | 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_pricesettlement_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.

Parametros da action Converter Gateway:

  • PlataformaAuto-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).
  • 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 bodyuser_datauser_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_nameevent_name; senha_atendimentosenha_de_atendimento; st_user_idst_user_id e user_iduser_id (id de tracking vs. id do usuario no sistema — campos distintos desde 0.6.8; fallback user_data.* desde 0.9.2); emailemail/email_address/user_data.email_address (normalizado: lowercase + trim); telefonetelefone/phone/phone_number/user_data.phone_number (normalizado: so digitos); nome/sobrenomefirst_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); zipcodezipcode/cep ou user_data.address.postal_code/.zip/.zp (normalizado: CEP brasileiro fica so com digitos, 26255-00026255000; codigo estrangeiro alfanumerico e preservado em UPPERCASE); data_nascimentodata_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_agentuser_agent (fallback user_data.user_agent); languagelanguage; generogender/genero (topo ou user_data.gender/user_data.gege 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; xcodxcod e scksck (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/ttpcommon_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_origempage_location.

ip — so o payload e fonte por padrao (desde 0.9.3). Ordem exata: ip_override (topo) → ip (topo) → user_data.ip_overrideuser_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_criacaotimestamp (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. SettingsCommunity NodesInstall
  2. Digitar n8n-nodes-conversor-plataformas
  3. Aceitar o aviso de risco → Install

Licenca

MIT