RunaTrack: guia de integração
Uma única API de pagamentos. Você integra uma vez; a gateway usada por trás (Beehive, Velani, Pingu, Pague-X ou FreePay) é escolhida no painel e pode ser trocada sem alterar o seu código.
- Configuração
- Criar cobrança
- Gateway por funil
- Consultar cobrança
- Webhook (aviso de pagamento)
- Erros
- Diferenças entre gateways
- Boas práticas
- Capturar IP e token de cartão
- Exemplo em Python
- Usar com uma IA
Configuração
No projeto (variáveis de ambiente, somente servidor):
| Variável | Valor | Obrigatória |
|---|---|---|
HUB_URL |
URL do hub, sem barra final. Ex.: https://hub-de-gateways.vercel.app |
sim |
HUB_API_KEY |
Sua chave hub_live_... |
sim |
HUB_SLUG |
Nome do funil deste projeto (ex.: funil1), o mesmo cadastrado em Gateway por funil no painel. Define qual gateway cobra este projeto. |
não (sem ele usa a gateway ativa) |
HUB_WEBHOOK_SECRET |
Segredo de assinatura (whs_...), visível no painel |
só se receber webhook |
A chave é secreta. Nunca a exponha no navegador (não use
NEXT_PUBLIC_). Remova do projeto as variáveis das gateways antigas: elas agora ficam só no painel do hub.
Painel: acesse HUB_URL/login com a sua chave. Lá você cola as API keys das gateways, escolhe a gateway ativa e informa a URL do seu webhook.
Todas as chamadas levam o header:
Authorization: Bearer hub_live_...
Content-Type: application/json
Criar cobrança
POST /api/v1/charges
Corpo
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
amount |
inteiro | sim | Valor em centavos (1990 = R$ 19,90). |
method |
string | sim | pix, boleto ou card. |
customer.name |
string | sim | Nome do pagador. |
customer.email |
string | sim | E-mail do pagador. |
customer.document |
string | depende | CPF/CNPJ (só números). Obrigatório em Beehive, Pingu, Pague-X e FreePay. |
customer.phone |
string | depende | Telefone com DDD. Obrigatório na Pingu e na FreePay. |
customer.address |
objeto | só boleto na Pague-X | { zipCode, street, number, city, state, neighborhood? }. |
reference |
string | recomendado | Id do pedido no seu sistema. Volta na resposta e evita duplicidade em algumas gateways. |
description |
string | não | Descrição da cobrança. |
ip |
string | depende | IP do comprador. Obrigatório na Beehive. Se omitido, o hub usa o IP encaminhado na requisição (normalmente o do seu servidor, então prefira enviar). |
cardToken |
string | só card |
Token/hash gerado no front pelo SDK da gateway (Beehive: card.hash; Pague-X: card.token; FreePay: hash do SDK). O hub nunca recebe o número do cartão. |
installments |
inteiro | não | Parcelas do cartão (padrão 1). |
funnel |
string | não | Slug do funil (a-z, 0-9, ., _, -; até 60). Se você mapeou este funil a uma gateway no painel (aba Funis; no projeto, basta HUB_SLUG=<slug> no .env), a cobrança usa ela; sem mapeamento, usa a gateway padrão. Também aceito no header X-Hub-Funnel. Veja Gateway por funil. |
shopUrl, checkoutUrl |
string | não | URLs da loja/checkout (a Beehive as exige no metadata; padrão: URL do hub). |
items |
lista | não | Produtos da cobrança, para o relatório por produto e para a Meta: `[{ id, name, price (centavos, unitário), quantity? (padrão 1), kind?: "main" |
tracking |
objeto | não | Origem do comprador para o rastreamento: { visitorId?, attribution?, ua? }. Não é enviado à gateway. |
Exemplo
curl -X POST "$HUB_URL/api/v1/charges" \
-H "Authorization: Bearer $HUB_API_KEY" -H "Content-Type: application/json" \
-d '{
"amount": 1990,
"method": "pix",
"funnel": "'"$HUB_SLUG"'",
"reference": "pedido-123",
"description": "Plano mensal",
"customer": { "name": "Maria Silva", "email": "maria@ex.com", "document": "12345678900", "phone": "11999999999" },
"ip": "189.10.20.30"
}'
Resposta 201
{
"id": "velani_8f2a1c",
"gateway": "velani",
"status": "pending",
"amount": 1990,
"method": "pix",
"reference": "pedido-123",
"pix": { "copyPaste": "00020126...", "qrCode": "data:image/png;base64,..." },
"expiresAt": "2026-11-05T15:30:00.000Z"
}
| Campo | Descrição |
|---|---|
id |
Id da cobrança no hub (<gateway>_<idExterno>). Guarde-o: é com ele que você consulta. |
gateway |
Gateway que processou. |
status |
pending, paid, failed, canceled, refunded ou chargeback. |
pix.copyPaste |
Código "copia e cola". Sempre presente em Pix. |
pix.qrCode |
Imagem do QR quando a gateway fornece; vazio na Beehive e na FreePay (gere o QR a partir do copyPaste). |
boleto.url / boleto.barcode |
Link e linha digitável (boleto). |
expiresAt |
Expiração, quando informada pela gateway. |
checkoutUrl |
Quando presente, redirecione o comprador para essa URL (ex.: cartão com autenticação). |
Idempotência (evitar cobrar em dobro)
Mande o header Idempotency-Key em cada criação (recomendado: qualquer string única por requisição de checkout, ex. o id do pedido). Sem o header, o hub usa a reference do corpo como chave.
- Repetir a mesma requisição em até 24 h devolve a mesma cobrança, sem criar outra; a resposta traz o header
x-idempotent-replay: 1. - Se a criação falhar, a chave é liberada: pode repetir sem medo, nada foi criado.
- A
referencetambém trava 24 h: para gerar outra cobrança do mesmo pedido (ex. Pix expirado), mande umIdempotency-Keydiferente. - Sem header e sem
reference, cada chamada cria uma cobrança nova (comportamento antigo).
Gateway por funil
Use quando cada funil (projeto) deve cobrar por uma gateway diferente, e você quer trocar a gateway de um sem mexer nos outros.
- No painel: aba Funis. Digite o nome do funil (slug, ex.:
funil1), clique em Adicionar funil, escolha a gateway e clique em Salvar alterações. Sem salvar, a regra não vale. - No projeto do funil: coloque no
.envHUB_SLUG=funil1. - No código: mande esse valor ao criar a cobrança, no corpo (
"funnel": process.env.HUB_SLUG) ou no headerX-Hub-Funnel. Se vierem os dois, vale o do corpo.
Regras:
- O slug usa
a-z,0-9,.,_,-(até 60 caracteres) e precisa ser idêntico no painel e no.env. O hub ignora maiúsculas e espaços nas pontas, masfunil_1efunil-1são funis diferentes. - Funil sem regra, sem slug ou com slug inválido usa a gateway ativa, sem erro.
- Trocar a gateway de um funil vale só para as novas cobranças dele. Cobranças antigas continuam consultáveis e seus webhooks seguem na gateway em que nasceram.
- Os avisos de método ou valor mínimo não aceito (422) e de falta de API key (503) valem para a gateway do funil.
Consultar cobrança
GET /api/v1/charges/{id}
curl "$HUB_URL/api/v1/charges/velani_8f2a1c" -H "Authorization: Bearer $HUB_API_KEY"
Retorna o mesmo formato da criação, com o status atual lido direto na gateway. Funciona mesmo que a gateway ativa tenha sido trocada depois da criação.
Ids que começam com ext_ são vendas registradas por você (veja Vendas de outro checkout). Nesse caso não há gateway para consultar: o hub responde a partir do próprio rastreamento, com source: "external".
Adicione ?events=1 para receber junto a timeline da cobrança (últimos 100 eventos): criação, webhook recebido/confirmado/repassado, mudanças de status, envio ao Meta/TikTok/Google, reconciliações e, nas vendas externas, uma linha de auditoria por chamada de ingestão.
curl "$HUB_URL/api/v1/charges/velani_8f2a1c?events=1" -H "Authorization: Bearer $HUB_API_KEY"
Webhook (aviso de pagamento)
- No painel do hub, preencha Webhook do cliente com a URL do seu projeto (HTTPS).
- Quando uma cobrança mudar de status, o hub faz
POSTnessa URL.
O hub confirma o status na API da gateway antes de avisar, então o evento reflete o estado real.
Corpo
{ "event": "charge.updated", "charge": { "id": "velani_8f2a1c", "gateway": "velani", "status": "paid", "amount": 1990, "method": "pix", "reference": "pedido-123" } }
Headers
| Header | Conteúdo |
|---|---|
x-hub-signature |
sha256= + HMAC-SHA256 (hex) do corpo bruto usando HUB_WEBHOOK_SECRET. |
x-hub-gateway |
Gateway de origem. |
Validar a assinatura (Node)
import { createHmac, timingSafeEqual } from "crypto";
export function validHubSignature(rawBody: string, header: string | null, secret: string) {
const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(expected), b = Buffer.from(header ?? "");
return a.length === b.length && timingSafeEqual(a, b);
}
Use o corpo bruto (
await req.text()), antes de fazerJSON.parse. Rejeite com401se a assinatura não bater.
Resposta esperada
Responda HTTP 2xx rapidamente. Qualquer outro código é tratado como falha e a gateway tentará reenviar. O mesmo evento pode chegar mais de uma vez: processe de forma idempotente (ex.: ignore se o pedido já está pago).
Evento de teste ("Testar agora" no painel)
O painel tem um botão Testar agora que manda um webhook de verdade à URL salva — mesmo header de assinatura e corpo no mesmo formato:
{ "event": "webhook.test", "charge": { "id": "test_1a2b3c", "gateway": "test", "status": "pending", "amount": 100, "method": "pix" } }
Seu sistema pode simplesmente responder 2xx e ignorar (event !== "charge.updated"), mas processe-o idempotentemente, como qualquer outro. O teste prova: URL acessível, assinatura validável e resposta rápida — o resultado aparece no próprio painel (status + latência).
Se o seu sistema ficar fora do ar
O hub não desiste do aviso: o repasse entra numa fila de reenvio com espera crescente (30 s, 1 min, 2 min, ... até 30 min, até 12 tentativas). Antes de cada reenvio o hub relê o status na gateway, então quando o seu sistema volta, recebe o status mais recente — vale o mesmo processamento idempotente. Depois das 12 tentativas o evento é marcado como abandonado (fica na timeline da cobrança, visível em GET /api/v1/charges/{id}?events=1); se a gateway reenviar o webhook original, o repasse acontece na hora.
Requisito: sua URL precisa responder 2xx rápido (sem processar dentro da própria resposta).
Rastreamento (UTMs)
O hub registra de onde veio cada venda (substitui a Utmify). O painel mostra em Rastreamento a receita paga, as vendas e a conversão por campanha, origem, mídia, criativo e termo.
Cole o script em todas as páginas da loja e do checkout, antes do
</head>. O snippet pronto, com o seudata-tide a suadata-pk, está no painel:<script src="https://SEU-HUB/t.js" data-tid="t…" data-pk="pk_…" async></script>Ele guarda no navegador um
visitorIde a última origem com UTM (utm_*,fbclid,gclid,ttclid,_fbp/_fbc) e registra as visitas.Ao criar a cobrança, envie o visitante:
{ "amount": 4990, "method": "pix", "customer": { "…": "…" }, "tracking": { "visitorId": "<window.HubTrack.visitorId>" } }Se o checkout fica em outro domínio, leve a origem no link com
window.HubTrack.decorate(urlDoCheckout), que acrescentahub_vide as UTMs. Seu backend também pode mandartracking.attributiondireto ({ "utm_source": "…", "utm_campaign": "…" }).Quando a gateway confirma o pagamento, a venda vira paga no painel. Isso acontece mesmo sem URL de webhook configurada.
Eventos extras: HubTrack.track("checkout_start" | "lead" | "add_to_cart"), ou o atributo data-hub-event="checkout_start" em um botão.
Sem visitorId (plano B): se o seu backend mandar ip (IP do comprador) e tracking.ua (o user agent do navegador dele), o hub procura uma visita recente com o mesmo IP e navegador (até 30 min) ou só o mesmo IP (até 10 min). O IP que chega ao hub sem o campo ip é o do seu servidor, por isso ele não é usado nessa busca. Mandar o visitorId continua sendo o mais preciso.
Vendas de outro checkout (sem passar pelo hub)
Se o pagamento não é criado pelo hub (você usa outro checkout), mande a venda para POST /api/v1/tracking/sales. Ela entra na receita, no ROAS/lucro, na atribuição por UTM e no Meta (Purchase), igual a uma cobrança do hub.
curl -X POST https://SEU-HUB/api/v1/tracking/sales \
-H "Authorization: Bearer $HUB_API_KEY" -H "Content-Type: application/json" \
-d '{"reference":"pedido-123","amount":4990,"status":"paid","method":"pix",
"gateway":"appmax","visitorId":"<window.HubTrack.visitorId>",
"items":[{"name":"Curso","price":4990}],
"email":"maria@ex.com","phone":"11933332222"}'
| Campo | Obrigatório | O que é |
|---|---|---|
reference |
sim | Id do pedido no seu sistema. É a chave de deduplicação. |
amount |
sim | Centavos, inteiro (> 0). |
status |
sim | paid, pending, refunded ou chargeback. |
method |
sim | pix, boleto ou card. |
paidAt |
não | ISO de quando foi aprovada. Padrão: agora, quando status=paid. |
gateway |
não | Texto livre: quem processou a venda. Padrão: external. |
funnel |
não | Slug do funil (o mesmo de Gateway por funil), para filtrar o relatório. |
items ou product |
não | Produtos: { id?, name, price (centavos), quantity?, kind? }. Sem id, o name vira o id. |
email, phone |
não | Do comprador. O hub guarda só o hash (Meta) e não devolve na resposta. |
ip, ua |
não | Plano B de atribuição (mesmas janelas acima). |
visitorId |
não | window.HubTrack.visitorId: caminho mais preciso. |
attribution / utm_* |
não | Origem enviada por você; tem prioridade sobre o último toque. As UTMs também são aceitas no topo do corpo. |
Idempotência e mudança de status. A mesma reference cai sempre na mesma venda: reenviar com o mesmo status devolve 200 com duplicate: true e não duplica nada. Enviar outro status atualiza o mesmo registro — o fluxo normal é pending na criação, paid na aprovação e refunded/chargeback depois. O createdAt da venda não muda num reenvio (o dia do relatório continua sendo o da primeira venda).
Meta (Conversions API). Quando a venda está paid, o hub envia um Purchase (uma única vez por venda, event_id = id da venda, deduplicado com o Pixel do navegador). Não há InitiateCheckout para venda externa: ela já aconteceu.
Resposta (201 na primeira vez, 200 depois):
{ "id": "ext_9f2c…", "reference": "pedido-123", "status": "paid", "amount": 4990, "method": "pix",
"gateway": "appmax", "createdAt": "2026-11-06T17:00:00.000Z", "paidAt": "2026-11-06T17:00:00.000Z",
"attribution": { "utm_source": "facebook" }, "matchedBy": "visitor", "duplicate": false }
Erros: 401 chave inválida ou cliente desativado; 422 campo faltando ou inválido (a mensagem diz o que corrigir); 429 limite de requisições (veja Limites); 500 falha ao gravar (repita a mesma requisição — é idempotente).
Consulta e auditoria. O id devolvido (ext_...) funciona em GET /api/v1/charges/{id}. Como a venda não existe em gateway nenhuma, o hub responde a partir do próprio rastreamento (com source: "external") e, com ?events=1, devolve a timeline: a criação, cada mudança de status e uma linha de auditoria external.received por chamada aceita — quando, qual chave (hub_live_… só o prefixo), a reference e o status. Nenhum dado do comprador entra nesse log.
Servir pelo domínio da loja (recomendado)
Safari e iPhone apagam em até 7 dias (ou 24 h, quando a pessoa chega com fbclid) os cookies criados por script, e bloqueadores de anúncio barram domínios de rastreamento. Para a origem durar, sirva o rastreador pelo domínio da loja, com um proxy de /c/* para o hub. Assim o cookie hub_vid é gravado pelo servidor, no domínio e no IP da loja.
Use /c/s.js no lugar de /t.js, e o script envia os eventos para /c/e na mesma origem:
<script src="/c/s.js" data-tid="t…" data-pk="pk_…" async></script>
- Next.js (
next.config.js):async rewrites() { return [{ source: "/c/:path*", destination: "https://SEU-HUB/c/:path*" }]; } - Nginx:
location /c/ { proxy_pass https://SEU-HUB/c/; proxy_set_header Host SEU-HUB; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } - Cloudflare: um Worker na rota
loja.com.br/c/*que fazfetchparahttps://SEU-HUB+ o mesmo caminho, repassando o corpo e o headerX-Forwarded-Forcom oCF-Connecting-IP.
Repasse sempre o IP do visitante (X-Forwarded-For): ele é usado no plano B e no envio ao Meta. Um CNAME (t.loja.com.br apontando para o hub) não resolve o Safari, que trata como de terceiro o cookie de um subdomínio com IP diferente do site.
Sem proxy, o /t.js continua funcionando como antes, só com a duração menor no Safari.
Meta (Conversions API)
Em Rastreamento → Meta, cole o Pixel ID e um token de acesso da API de Conversões. A partir daí o hub envia ao Meta, direto do servidor:
| Quando | Evento | event_id |
|---|---|---|
| A cobrança é criada (Pix/boleto gerado) | InitiateCheckout |
<id da cobrança>:ic |
| A gateway confirma o pagamento | Purchase (valor em BRL) |
<id da cobrança> |
- Os dados do comprador vão em SHA-256, como a Meta exige: e-mail, telefone, nome, documento como
external_id, e cidade, UF e CEP quando houver. Também vão IP, user agent efbp/fbc. O hub guarda só os hashes, nunca o texto. - Se você também dispara o Pixel no navegador, use
HubTrack.checkout(idDaCobranca, valorEmReais)quando o Pix/boleto for gerado eHubTrack.purchase(idDaCobranca, valorEmReais)na página de obrigado. Eles chamam ofbqcom o mesmoeventIDque o hub manda pela API, e a Meta não conta duas vezes. - Cada evento é aceito uma vez; a coluna Meta de "Últimas cobranças" mostra se foi aceito ou o erro.
- Falhou? O hub tenta de novo sozinho: ao abrir o painel (até 3 vezes, com espera de 15 min × tentativas) e no cron diário, enquanto o evento tiver menos de 7 dias (limite da Meta). Corrigiu o Pixel ou o token? Os eventos com erro são reenviados no próximo cron.
- Para validar, preencha o código de teste (Gerenciador de Eventos → Eventos de teste) e apague depois.
Gasto, ROAS e lucro (Meta, Google e TikTok)
O hub importa o gasto das três redes e soma tudo no mesmo relatório. Cada uma é configurada no seu card do Rastreamento:
| Rede | O que configurar | Como é lido |
|---|---|---|
| Meta Ads | ID da conta (act_…) + token com ads_read (pode ser um token separado do de Conversões) |
Insights por anúncio/dia |
| Google Ads | Customer ID (10 dígitos), developer token e o OAuth das conversões (client id, client secret, refresh token) | GAQL (searchStream): cost_micros por anúncio/dia |
| TikTok Ads | Advertiser ID (Advertising Manager) + o access token já usado nas conversões | Reporting API integrada, por anúncio/dia |
- Sozinho: ao abrir o painel, se a última leitura tiver mais de 1 hora, e uma vez por dia pelo cron (relê os 3 últimos dias, que as plataformas ainda corrigem).
- Na hora: pelo botão Sincronizar 30 dias (use na primeira vez). A janela vai até 90 dias.
- Uma rede fora do ar não apaga nada: as linhas daquela rede ficam como estavam e as outras continuam sendo gravadas. O erro aparece por rede no painel.
- Google sem escopo/credencial responde com uma mensagem dizendo o que conferir (o token precisa de acesso à conta e o developer token precisa estar aprovado).
Em Custos, informe a taxa da gateway (%), a taxa fixa por venda (R$), os impostos (%) e o custo do produto (%). O painel mostra:
| Métrica | Conta |
|---|---|
| ROAS | receita paga ÷ gasto |
| CPA | gasto ÷ vendas pagas |
| Custos | receita × (taxa % + impostos % + produto %) + taxa fixa × vendas pagas |
| Lucro | receita − gasto − custos |
O gasto entra em cada linha da tabela quando o agrupamento é Campanha, Conjunto ou Criativo e a UTM leva o id do Meta (nome|{{campaign.id}}, {{adset.id}}, {{ad.id}}, como no modelo de UTM acima). Sem o id, o hub tenta casar pelo nome. Campanhas que gastaram e não venderam aparecem com lucro negativo. Nos demais agrupamentos, o gasto entra só no total.
O payload do painel traz spendByNetwork ({ meta, google, tiktok }, em centavos) e a soma dos três é exatamente o totals.spend. O gasto de uma conta que não está em BRL não é somado (o hub não converte moeda) e o painel avisa qual rede ficou de fora. Os dias do painel seguem o fuso da conta de anúncios, lido na sincronização (padrão: America/Sao_Paulo), para o dia das vendas bater com o do gasto.
Sem conectar a API? Use a importação manual (POST /api/panel/tracking/spend-import, veja abaixo).
Importar gasto manualmente (CSV ou JSON)
Para quem não conecta a API da rede: POST /api/panel/tracking/spend-import com a sessão do painel.
curl -X POST "$HUB_URL/api/panel/tracking/spend-import" -H "Content-Type: text/csv" --data-binary @gasto.csv
date,network,campaignId,campaignName,spend,currency
2026-11-01,google,1234567890,Campanha de busca,4990,BRL
2026-11-01,tiktok,1789...,Criativo TikTok,2500,BRL
| Coluna | Obrigatória | O que é |
|---|---|---|
date |
sim | Dia do gasto (YYYY-MM-DD). |
network |
não | meta, google ou tiktok. Ausente = meta. |
campaignId |
sim | Id da campanha na rede (é o que liga a campanha ao funil). |
campaignName |
não | Nome exibido. Ausente = o id. |
spend |
sim | Centavos inteiros (4990 = R$ 49,90). |
currency |
não | Moeda da conta. Ausente = BRL. Diferente de BRL é gravado, mas não somado. |
- Também aceita JSON:
{ "rows": [ { "date": "2026-11-01", "network": "google", "campaignId": "…", "spend": 4990 } ] }. - O CSV aceita
,ou;(Excel em português) e campos entre aspas. Até 5000 linhas por envio. - Reenviar o mesmo arquivo não duplica: cada (rede, dia) substitui o lote anterior daquela rede naquele dia, e as outras redes não são afetadas.
- Se a rede estiver conectada por API, a sincronização automática volta a sobrescrever aquele dia. A importação é o fallback.
ROAS por funil
Cada funil pode ter uma ou várias campanhas, de qualquer rede. Em Rastreamento → Funis das campanhas, escolha o funil de cada campanha — o hub já sugere o que consegue deduzir pela UTM.
- Com mapeamento: todo o gasto da campanha entra no funil escolhido.
- Sem mapeamento: o hub deduz o funil pela
utm_campaigndas vendas da campanha (nome|{{campaign.id}}), usando o funil que mais gerou receita paga naquela campanha. Essa dedução só vale para o Meta (a UTM carrega o id do Meta). - Sobrou gasto: campanha de Google/TikTok sem mapeamento aparece como gasto não atribuído (
unmappedSpendno payload) — nunca é distribuído no chute.
O painel recebe byFunnel (uma linha por funil: receita, gasto, lucro, ROAS e vendas pagas) e unmappedSpend. ROAS é null quando o funil não tem gasto atribuído (não é "infinito"). Com ?funnel= no relatório, os totais usam a receita e o gasto daquele funil.
GET /api/panel/tracking/funnels devolve o mapa e os funis cadastrados; PUT com { "campaignFunnels": { "<id da campanha>": "<slug>" } } substitui o mapa inteiro ({} apaga). Para campanhas fora do Meta, use a chave com a rede: "google:1234567890" ou "tiktok:1789…" — ids iguais de redes diferentes não se misturam, e a chave só com número continua valendo para o Meta.
Funil por etapa
O painel mostra quantas pessoas passaram por cada etapa: visitas → checkout iniciado → lead → cobranças criadas → vendas pagas, com a taxa de cada etapa contra a anterior.
- O hub guarda só contagens por dia (por campanha, origem, mídia, criativo, termo e funil), com 90 dias de retenção. Nenhum evento individual e nenhum dado do comprador é gravado.
- As etapas de navegação vêm do rastreador (
pageview,checkout_startviaHubTrack.track("checkout_start")ouHubTrack.checkout(...), elead). As etapas de cobrança vêm da própria API:charge_createdquando a cobrança é criada epaidquando a gateway confirma o pagamento (webhook ou reconciliação). - Quem começou a rastrear depois vê as etapas antigas zeradas — o hub não inventa número.
- O dia do contador segue o fuso do rastreamento (America/Sao_Paulo), o mesmo das visitas.
?funnel=no relatório filtra: visitas/checkout/lead pelas campanhas mapeadas ao funil, cobranças/vendas pelo funil em que foram criadas.
No payload, funnelSteps traz as cinco etapas (key, label, count, rate) e cada linha de UTM pode trazer steps com os mesmos contadores.
Modelo de atribuição
Por padrão a venda fica com o último toque (comportamento histórico). Dá para escolher em Rastreamento → Atribuição:
| Modelo | Como a venda é atribuída |
|---|---|
last (padrão) |
Último toque com origem antes da cobrança. |
first |
Primeiro toque com origem do visitante (o que o rastreador gravou na primeira visita). |
linear |
Receita dividida igualmente entre os toques distintos dentro da janela. Hoje o hub guarda o primeiro e o último toque: se forem o mesmo, é um toque só. |
- Janela (
windowDays, 1 a 90 dias, padrão 30): toque mais antigo que isso não conta e a venda cai em "(sem origem)". - No modelo
lineara divisão é feita em centavos inteiros (maior resto): a soma das partes é sempre o valor da venda — R$ 9,99 vira 5,00 + 4,99, nunca 4,99 + 4,99. A venda é contada uma vez na tabela (na maior parte); cada linha recebe a sua fatia de receita. GET /api/panel/tracking/attributiondevolve{ model, windowDays }e as opções;PUTcom{ "model": "linear", "windowDays": 30 }salva. O relatório devolve o modelo em uso no campoattribution.
Lucro real (líquido, lucro, ROAS, ROI e margem)
O relatório calcula tudo em centavos e devolve os componentes separados em totals, rows, daily e byFunnel:
líquido (net) = vendas pagas − taxa da gateway − coprodutor/afiliado − imposto
− custo do produto − multa de chargeback
imposto do gasto = gasto com anúncios × adsTaxPct
lucro (profit) = líquido − gasto com anúncios − imposto do gasto − despesas
costs = taxas + afiliado + produto + imposto + chargeback + imposto do gasto
ROAS = receita bruta ÷ gasto com anúncios (N/A sem gasto)
ROI = líquido ÷ (gasto + imposto do gasto + despesas) (N/A sem investimento)
margem = lucro ÷ líquido (N/A só quando o líquido é zero)
- Custo por venda: taxa da gateway (percentual + fixa, com taxa própria por gateway), coprodutor/afiliado (%), imposto sobre a receita (%), custo do produto (% e fixo) e multa de chargeback. O custo de produto e a comissão podem ser definidos por produto (
byProduct, chave = id do item da cobrança). - Imposto sobre o gasto (
adsTaxPct): incide sobre o gasto com anúncios (ex.: imposto retido pela plataforma) e entra no investimento do ROI. - Despesas adicionais (editor, ferramenta, aluguel): cadastradas em Rastreamento → Despesas, podem ser
daily,monthly(÷30) ouonce(numa data). São rateadas nos dias do período e, na tabela por origem/funil, pela receita de cada linha — a soma da coluna de lucro fecha com o lucro total. - Nenhuma divisão devolve
Infinity: quando o denominador é zero, o campo vemnull(N/A).
PUT /api/panel/tracking com { "costs": { ... } } salva os custos (aceita feePct, feeFixed, taxPct, productPct, productFixed, affiliatePct, adsTaxPct, otherFees, chargebackFee, byGateway e byProduct).
Despesas: GET /api/panel/tracking/expenses (lista + total dos últimos 30 dias), POST (cria), POST/PUT em /api/panel/tracking/expenses/<id> (edita) e DELETE (apaga). Corpo: { "name": "Editor", "amount": 50000, "period": "monthly" } (amount em centavos; once exige date).
Filtros do relatório (?account=, ?source=, ?platform=, ?product= em GET /api/panel/tracking):
source,producteplatformfiltram receita e gasto na mesma dimensão. Oplatformda venda é inferido pelo identificador de clique (gclid→ Google,ttclid→ TikTok,fbclid/fbp/fbc→ Meta); venda sem identificador não é atribuível a nenhuma rede e fica de fora do filtro de plataforma. Com isso ROAS/ROI/margem continuam comparando receita e gasto da mesma rede.accountfiltra só o gasto (a venda não guarda conta de anúncios). Nesse caso os índices que cruzam receita e gasto vêmnull(não compare receita total com gasto filtrado) efiltersMeta.spendOnlydiz o que foi aplicado.- Gasto sem conta identificada (importação manual) não desaparece com filtro de conta: entra no total e aparece em
filtersMeta.spendWithoutAccount. byFunnelinclui a linha "Não atribuído" quando há gasto de campanha sem funil mapeado, para a soma do gasto por funil fechar com o total (o valor continua emunmappedSpend).
Listagens: pedidos e eventos de conversão
GET /api/panel/orders?from=YYYY-MM-DD&to=YYYY-MM-DD&status=paid&source=hub|external&platform=<slug>&q=<busca>&limit=50&offset=0→{ orders, total, hasMore }. Cada pedido trazid,reference,status,amount,method,gateway,platform(venda externa),source,funnel,createdAt,paidAte as UTMs. Sem dados pessoais.GET /api/panel/pixel-events?from&to&network=meta|tiktok|google&event=Purchase&status=sent|error|pending|disabled&limit&offset→{ events, total, hasMore }. Uma linha por evento enviado (InitiateCheckout/Purchase do Meta, CompletePayment do TikTok, conversão do Google), com o pixel configurado, oevent_id(dedupe) e o erro quando a rede recusou.GET /api/panel/tracking/events?from&to→{ steps }: o mesmo funil por etapa do relatório, numa rota só dele.
Webhooks de plataformas de checkout
Estrutura pronta para receber a venda de plataformas de terceiros (Hubla, Kiwify, Hotmart, Ticto, Perfect Pay, Eduzz). Configure em Rastreamento → Plataformas e use a URL mostrada:
POST /api/webhooks/platform/<slug>?t=<id do cliente>
x-platform-signature: sha256=<HMAC-SHA256 do corpo bruto com o segredo>
x-platform-timestamp: <epoch em segundos> (opcional, recomendado)
- Prefira a assinatura HMAC (
x-platform-signature). Comx-platform-timestamp, o HMAC é calculado sobre"<timestamp>.<corpo>"e o hub só aceita ±5 minutos — isso impede reapresentar um corpo antigo (replay). Sem o timestamp, o HMAC é sobre o corpo bruto. x-platform-token: <segredo>continua aceito para plataformas que só sabem mandar um token fixo (comparação em tempo constante). É o caminho mais fraco: use só se a plataforma não assinar.- Segredo: 32 a 128 caracteres de
A-Z a-z 0-9 _ -. O painel pode gerar um (generate: true, mostrado uma única vez). Segredo mais curto é recusado ao salvar. - Ordem das checagens: tamanho (413 antes de ler) → limite de requisições (429) → cliente/plataforma (404) → plataforma ligada (403) → assinatura (401) → corpo.
- Respostas:
201criada,200reenvio idempotente,401assinatura ausente/inválida,403plataforma desligada no painel,404cliente ou plataforma inexistente (resposta uniforme: não confirma se o cliente existe),413corpo acima de 64 KB,429limite de requisições,501adaptador ainda não implementado. - Limites: corpo de até 64 KB; 60 requisições/min por cliente e 120/min por IP (env
HUB_RATE_LIMIT_WEBHOOKS_PER_MIN). - Status monotônico: depois de aprovada, a venda não volta para pendente. Um webhook atrasado (
pending) é registrado na timeline como ignorado e não derruba a receita nem apaga oPurchase. Reembolso e chargeback continuam valendo. paidAt: precisa ser ISO, não pode estar no futuro (tolerância de 5 min) nem mais de 7 dias no passado; fora disso é ignorado e vale o horário de chegada.- Hoje só o adaptador
genericestá implementado (aceita o mesmo corpo doPOST /api/v1/tracking/sales). Os das plataformas reais respondem 501 até o formato ser confirmado — o hub nunca grava venda a partir de um adaptador não implementado. - A venda entra pelo mesmo caminho da venda externa (dedupe por
reference, atribuição, Meta/TikTok/Google e timeline), com a plataforma gravada emgateway(aparece em/api/panel/orders).
Webhooks das gateways: limites e assinatura
O webhook que a gateway chama (/api/webhooks/<cliente>/<gateway>) também é público:
- corpo de até 64 KB (
413acima disso) e limite de 120 req/min por IP e 60 req/min por cliente+gateway (429); - a Velani exige o
whsec_configurado: sem ele o evento é recusado com401. Um POST anônimo dispararia consulta paga na API dela e repasse ao seu sistema; - Beehive, Pingu e FreePay não assinam: o hub continua confirmando o status na API da gateway antes de repassar, então um corpo forjado não marca pedido como pago.
Rastreamento no navegador: atribuição e privacidade
O /t.js (e o /c/s.js atrás de proxy) manda o evento para /c/e:
- IP de origem é lido só de cabeçalho de proxy confiável (
x-vercel-forwarded-for, depoisx-real-ip, depois o ÚLTIMO salto dox-forwarded-for) — nunca da primeira posição do XFF, que o cliente controla. O IP indexa a visita apenas para o "plano B" da atribuição. hub_vidna URL (link decorado porHubTrack.decorate) só é aceito quando combina com um visitante já conhecido do hub e com o mesmo navegador; caso contrário o evento entra sem origem (um link de terceiro não injeta a UTM dele na sua venda).- CORS: a resposta não usa credenciais e não reflete o
Origin(access-control-allow-origin: *, semallow-credentials). OvisitorIddo próprio script continua no cookie/localStorage da loja. - Uma venda aprovada não volta a pendente por webhook atrasado, e
paidAtsó é aceito entre 7 dias atrás e agora.
TikTok (Events API)
Em Rastreamento → TikTok, cole o Pixel code e um access token do Events API (Gerenciador de Eventos do TikTok → API). O Advertiser ID (Advertising Manager) é opcional: só é usado para importar o gasto. Quando a venda é paga, o hub envia CompletePayment com:
- o
ttcliddo navegador quando existir (é o identificador mais forte); - e-mail e telefone em SHA-256 (o mesmo hash do Meta),
external_id(documento), IP e user agent; event_id= id da venda — reenviar não conta duas vezes.
O hub não envia o evento quando a venda não tem nenhum identificador (ttclid, e-mail ou telefone): não casaria e só gastaria quota. O código de teste é opcional (aparece em Eventos de teste).
Google Ads (conversões offline)
Em Rastreamento → Google Ads, informe o Customer ID (10 dígitos, com ou sem traços), a conversion action (o id numérico ou customers/<id>/conversionActions/<id>) e o acesso OAuth: client id, client secret e refresh token, além do developer token. Se a conta está dentro de um MCC, informe também o login customer id.
As mesmas credenciais servem para importar o gasto da conta (a conversion action não é necessária para isso).
Quando a venda é paga e tem gclid, o hub chama uploadClickConversions com valor em reais, moeda BRL, a data da aprovação no fuso configurado e orderId = id da venda (o Google não conta o mesmo pedido duas vezes). Sem gclid nada é enviado.
Retry e isolamento das conversões
| Rede | Estado na venda | Reenvio |
|---|---|---|
| Meta | capi |
painel (3 tentativas, 15 min × tentativas) e cron diário, até 7 dias |
| TikTok | tiktok.sentAt / tiktok.error |
mesmo esquema |
| Google Ads | google.sentAt / google.error |
mesmo esquema |
Uma rede que falha não impede as outras: cada envio é independente e a timeline da cobrança registra tiktok.sent, tiktok.error, google.sent e google.error. Nenhum token aparece em log, resposta ou mensagem de erro.
Produtos, aprovação, reembolsos e período
- Produtos e order bump: mande
itemsna cobrança (veja a tabela do corpo). O painel mostra cobranças, pagas, conversão e receita por produto, e a taxa de order bump: das vendas pagas com produto principal, quantas levaram bump. Os itens também vão para a Meta noPurchase(content_ids,contents,num_items). - Aprovação: cobranças geradas × pagas por método (Pix, boleto, cartão) e por gateway.
- Reembolso e chargeback: quando a gateway avisa, a venda sai da receita e aparece em "Reembolsos e chargebacks", com a taxa de cada um (o hub alerta quando o chargeback passa de 1%).
- Custos: além da taxa padrão, informe a taxa própria de cada gateway (% e fixo) e a multa por chargeback. O lucro usa a taxa da gateway que processou cada venda.
- Contas de anúncio: informe várias, separadas por vírgula (até 10). O gasto soma todas, e a moeda e o fuso são os da primeira.
- Período: além de hoje/7/30/90 dias, escolha de/até (até 366 dias). Os cartões comparam com o período anterior de mesmo tamanho. A tabela por agrupamento sai em CSV pelo botão Exportar.
Avisos de venda no celular (Telegram)
Em Rastreamento → Avisos de venda, conecte um bot do Telegram seu. Cada venda chega no celular na hora em que a gateway confirma, inclusive no iPhone.
- No Telegram, fale com o @BotFather, envie
/newbote cole o token no painel. - Abra o bot (ou adicione-o a um grupo da equipe) e envie
/start. - Clique em Conectar e depois em Enviar teste.
Você escolhe o que avisar: venda aprovada, reembolso e chargeback (ligados por padrão) e Pix/boleto gerado (desligado). O aviso traz valor, método, produto, order bump e a origem (UTM). Nenhum dado pessoal do comprador vai na mensagem. Cada mudança de status avisa uma vez, mesmo que a gateway repita o webhook.
Alerta de gateway fora (operacional)
No mesmo chat, com o mesmo bot, o hub também avisa quando uma gateway de pagamento para de responder: 3 falhas de comunicação seguidas (criação, confirmação na API, reenvio ou reconciliação) geram um aviso _🚨 Gateway fora do ar_ com o último erro — um só aviso por período de queda, sem spam. Quando a gateway volta a responder, chega _✅ Gateway normalizada_. Ligue/desligue no painel de Avisos (card "Alertas operacionais"; padrão: ligado). Falha do webhook do SEU sistema não dispara esse alerta — o alerta é sobre a gateway, não sobre sua URL.
Painel no celular (instalar como app)
No celular, abra o painel no navegador e use Adicionar à tela inicial (Chrome: menu ⋮ → Instalar app; Safari: Compartilhar → Adicionar à Tela de Início). Ele abre direto no Rastreamento, em tela cheia, como um app.
Pausar, ativar e mudar orçamento
Agrupando por Campanha, Conjunto ou Criativo, a tabela mostra o status e o orçamento diário que estão no Meta, com os botões Pausar / Ativar e Orçamento. O token precisa da permissão ads_management (ler o gasto pede só ads_read).
- O hub só age em campanhas, conjuntos e anúncios que existem na conta de anúncios configurada.
- O orçamento só muda em quem tem orçamento diário próprio: na campanha com orçamento de campanha, no conjunto sem ele. O mínimo é R$ 1,00.
- Tudo fica em Histórico de ações: quando, quem (painel, suporte ou regra), o que mudou (de → para) e o resultado ou erro do Meta.
Regras automáticas
Regras pausam, reativam ou mudam o orçamento sozinhas, a partir do gasto e das vendas reais do hub (não das conversões que o Meta reporta).
- Como funciona uma regra: cada uma tem nível (campanhas, conjuntos ou anúncios), período (hoje, 3 ou 7 dias), até 4 condições que precisam valer juntas, uma ação e uma espera.
- Condições: gasto, receita, lucro, vendas pagas, cobranças, ROAS ou CPA.
- Ação: pausar, ativar ou mudar o orçamento de −90% a +100%.
- Espera: depois de agir numa entidade, a regra não age nela de novo antes de N horas.
- Como as métricas são calculadas: ROAS sem gasto não conta; CPA com gasto e nenhuma venda conta como infinito (então "CPA > X" pega quem gastou e não vendeu).
- Modelos prontos: "Pausar sem venda", "Pausar CPA alto" e "Escalar vencedora".
- Antes de ligar: use Simular, que mostra o que a regra faria agora sem alterar nada.
- Chave geral: com "Desligar regras", nenhuma regra roda.
- Limite de segurança: no máximo 20 ações por execução.
Quando rodam: no cron diário da Vercel (o mesmo do gasto) e pelo botão Rodar agora. Para rodar a cada 15–30 min, que é o que faz sentido para regras de "hoje", use um agendador externo chamando:
curl -X POST https://SEU-HUB/api/cron/rules -H "Authorization: Bearer $CRON_SECRET"
Pode ser o QStash da Upstash (o mesmo provedor do banco), o cron-job.org ou um cron do plano Pro da Vercel. Depois de configurar, defina RULES_SCHEDULER=external na Vercel para o painel parar de avisar que as regras rodam só uma vez por dia.
Cuidado com regras que se contradizem (uma pausa, outra reativa a mesma campanha): cada regra respeita a própria espera, então elas podem se alternar a cada ciclo.
UTMs sugeridas para o Meta Ads (copie no painel):
utm_source=facebook&utm_medium={{placement}}&utm_campaign={{campaign.name}}|{{campaign.id}}&utm_content={{ad.name}}|{{ad.id}}&utm_term={{adset.name}}|{{adset.id}}.
Os ids servem para cruzar com o gasto dos anúncios quando essa etapa entrar.
Erros
Todas as falhas retornam { "error": "mensagem" }.
| HTTP | Significado |
|---|---|
400 |
Corpo inválido ou campo obrigatório ausente. |
401 |
Chave ausente, inválida ou cliente desativado. |
404 |
Cobrança não encontrada. |
422 |
Pedido recusado: método não suportado pela gateway ativa, valor abaixo do mínimo, dado obrigatório ausente (documento, telefone, ip) ou recusa da gateway. A mensagem explica o motivo. |
429 |
Limite de requisições por cliente (veja abaixo). Espere o Retry-After. |
502 |
A gateway não respondeu ou deu erro. Pode tentar de novo. |
503 |
A gateway ativa está sem API key configurada no painel. |
Limites de requisições
Cada cliente tem 120 requisições por minuto em cada endpoint de escrita: POST /api/v1/charges e POST /api/v1/tracking/sales. Os dois têm orçamentos separados — um pico de cobranças não bloqueia a ingestão de vendas (e vice-versa).
- A janela é deslizante: cada requisição conta por 60 s a partir de quando chegou.
- Ao passar do limite, a resposta é
429comRetry-After(segundos) eX-RateLimit-Limit/X-RateLimit-Remaining. Repita depois desse tempo; o corpo diz quantos segundos faltam. - A leitura (
GET /api/v1/charges/{id}), o webhook para o seu sistema e o rastreador (/t.js,/c/e) não entram nesse limite. - Quem decide o valor é o dono do hub (variável
HUB_RATE_LIMIT_PER_MINno servidor; padrão 120). O seu sistema não precisa configurar nada.
Diferenças entre gateways
| Beehive | Velani | Pingu | Pague-X | FreePay | |
|---|---|---|---|---|---|
| Métodos | pix, boleto, card | pix | pix | pix, boleto, card¹ | pix, boleto, card |
| Valor mínimo | – | R$ 5,00 | R$ 1,00 | boleto: R$ 5,00 | R$ 1,00² |
customer.document |
obrigatório | opcional | obrigatório | obrigatório | obrigatório |
customer.phone |
opcional | opcional | obrigatório | opcional | obrigatório |
customer.address |
– | – | – | boleto | – |
ip do comprador |
obrigatório | – | – | – | recomendado |
pix.qrCode (imagem) |
vazio | sim | sim | sim | vazio |
Idempotência por reference |
– | sim | sim | não | – |
¹ Cartão e boleto na Pague-X exigem adquirente habilitada na conta; senão a gateway recusa. ² Mínimo conservador do hub (a doc da FreePay não informa).
Se o seu sistema aceita vários métodos, trate o 422 e ofereça ao cliente os métodos que a gateway ativa suporta.
Boas práticas
- Guarde o
idretornado junto ao pedido. - Não dependa só do webhook: consulte
GET /charges/{id}como reserva (por exemplo, quando o cliente voltar à página). - Gere uma
referenceúnica por pedido. Reenviar o mesmo pedido com a mesmareferenceevita cobrança em duplicidade na Velani e na Pingu. Na Pague-X cada chamada cria uma cobrança nova: em caso de timeout, consulte antes de reenviar. - Chame o hub sempre do servidor.
- Defina um timeout (ex.: 25 s) na chamada; as gateways podem demorar.
Capturar IP e token de cartão
IP do comprador (ip). Envie o IP do visitante, não o do seu servidor. Atrás de proxy/CDN, ele vem no primeiro valor do header x-forwarded-for:
// Next.js / Node (rota do seu servidor)
const buyerIp = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "";
Cartão (cardToken). O hub nunca recebe número, validade nem CVV. O formulário de cartão fica no navegador do seu cliente, que usa o SDK da gateway ativa para gerar um token/hash de uso único (curta duração), e o seu servidor envia só esse token em cardToken:
| Gateway | Como gerar o token |
|---|---|
| Beehive | BeehivePay.encrypt(...) (script https://api.conta.paybeehive.com.br/v1/js), usa o hash em cardToken. |
| Pague-X | Hosted Fields / SDK paguex.js, usa card.token. |
| FreePay | encrypt() do SDK da FreePay (ver "Cartão com 3DS" na doc dela). |
Como o SDK depende da gateway ativa, só ofereça cartão se o seu sistema souber qual está ativa (ou fixe uma gateway para cartão). Quando a resposta trouxer checkoutUrl, redirecione o comprador.
Exemplo em Python
import os, requests
def criar_cobranca(valor_centavos, pedido, cliente, ip):
r = requests.post(
f"{os.environ['HUB_URL']}/api/v1/charges",
headers={"Authorization": f"Bearer {os.environ['HUB_API_KEY']}"},
json={
"amount": valor_centavos, "method": "pix", "reference": pedido,
"funnel": os.environ.get("HUB_SLUG"), # funil deste projeto (opcional)
"customer": cliente, # {"name","email","document","phone"}
"ip": ip,
},
timeout=25,
)
r.raise_for_status() # 422 traz {"error": "..."} explicando o que faltou
return r.json() # guarde r.json()["id"]
Validar o webhook em Python:
import hmac, hashlib
def assinatura_valida(corpo_bruto: bytes, header: str, segredo: str) -> bool:
esperado = "sha256=" + hmac.new(segredo.encode(), corpo_bruto, hashlib.sha256).hexdigest()
return hmac.compare_digest(esperado, header or "")
Usar com uma IA
Para uma IA gerar o código de integração, entregue um destes (do mais simples ao mais completo):
- Este guia em texto puro:
/docs.md(ou o botão ⬇ Baixar .md no topo da página) e cole no chat. - A URL
/llms.txt, para ferramentas que leem esse índice. - O contrato OpenAPI:
/openapi.json, para Cursor, ChatGPT/Actions e geradores de cliente.
Prompt sugerido:
Usando a API do documento anexo, crie o serviço de pagamentos no meu projeto (informe a linguagem/framework): função para criar cobrança Pix, função para consultar por
id, e uma rota de webhook que validex-hub-signaturecom o corpo bruto e marque o pedido como pago quandocharge.status === "paid"(idempotente). Use as variáveisHUB_URL,HUB_API_KEY,HUB_SLUG(mande o valor emfunnelao criar a cobrança; ou no headerX-Hub-Funnel) eHUB_WEBHOOK_SECRET; nunca exponha a chave no front.
Revise o que a IA gerar e teste com a gateway Simulado (teste) antes de usar uma gateway real.