iHubGamesiHubGamesdocs

Webhooks

Modelos de webhook: V1 (postbackUrl) e V2 (global)

A iHubGames oferece dois modelos de webhook que rodam em paralelo — não são mutuamente exclusivos. Você pode usar um, outro, ou os dois ao mesmo tempo:

ModeloComo configuraQuando disparaDistinção no payload
V1 — postbackUrl per-transaçãoPassa o campo postbackUrl no body de cada POST /transactions/v2/purchase ou POST /withdraws/cash-outStatus muda nessa transação específica (uma URL por tx)Payload camelCase plano, sem campo event
V2 — Webhook global por UserCadastra uma URL única em Webhooks > Endpoints (rota /auth/webhooks) com a lista de eventos que você quer receberTODOS os eventos do tipo subscrito disparam, independente da transaçãoPayload tem campos event + environment no topo

⚠️ Coexistência: se você tem AMBOS configurados (passou postbackUrl numa transação + tem um webhook global V2 ativo subscrito ao evento correspondente), seu endpoint recebe os dois POSTs — um pra cada modelo. Sua URL pode ser a mesma; basta seu handler distinguir pelo formato do payload.

✅ O formato é definido pelo canal, não pela conta. Todo webhook cadastrado em Webhooks > Endpoints (CRUD) é entregue sempre no formato V2 — envelope { event, environment, payload }. O formato V1 (camelCase plano) é exclusivo do postbackUrl legado. Ou seja: se você usa só o CRUD (sem postbackUrl), sempre recebe V2.

Recomendamos V2 pra novas integrações: um único setup cobre todas as transações automaticamente, tem rastreio de origem (source_type, invoice_id, etc), e suporta eventos extras (subscriptions, infrações) que V1 não cobre.

Quando escolher v1

  • Você já tem uma integração v1 funcionando em produção. Não há urgência para migrar; v1 é suportada indefinidamente.
  • Você tem dependência rígida do formato camelCase plano por causa de outro sistema que consome seus webhooks downstream.
  • Você só precisa de atualizações básicas de status de transação e não se importa com ISPB, separação de pagador/recebedor ou eventos de infração.

Se nada disso se aplica, escolha v2.

Quando escolher v2

  • Você está começando uma integração nova hoje.
  • Você precisa de informações de ISPB / instituição para conciliação contra extratos bancários.
  • Você quer roteamento por tipo de evento no seu handler (switch (event) { case "cashin.paid": ... }) em vez de inferir a partir de combinações de type e status.
  • Você quer webhooks de infração entregues como seu próprio tipo de evento em vez de embutidos dentro do payload da transação.

Escolha v2 por padrão para qualquer coisa nova.

Eventos disponíveis (V2)

Em V2 (Webhook global), você seleciona em Configurações > Webhooks a lista exata de eventos que cada URL deve receber. Os 15 eventos abaixo são todos os que o sistema dispara hoje:

Eventos de cash-in (transação):

  • cashin.paid — Transação aprovada (PIX confirmado ou cartão capturado).
  • cashin.refunded — Estorno concluído.
  • deposit.credited — dinheiro recebido direto numa chave sua, sem cobrança pré-criada: alguém te pagou sem passar por um checkout ou QR gerado por você. Mesmo payload de cashin.paid. Ocorre em qualquer conta que tenha uma chave Pix (BRL) ou CLABE (MXN) de recebimento cadastrada — não é exclusivo de conta BaaS.

Eventos de cash-out (saque):

  • cashout.success — Saque concluído (SPI confirmou).
  • cashout.returned — Saque devolvido pelo banco do recebedor (chave inválida, etc).
  • cashout.failed — Saque falhou tecnicamente.

Disputas / Bloqueios:

  • infraction.updated — Chargeback (disputa de cartão), bloqueio cautelar de compliance, ou MED criado/atualizado. Use o payload pra distinguir o tipo.

Subscription lifecycle (assinatura recorrente):

  • subscription.created — Nova subscription criada.
  • subscription.activated — Status virou ACTIVE (criação direta ou retomada de PAUSED).
  • subscription.paused — Status virou PAUSED.
  • subscription.canceled — Cancelamento manual (cliente ou sistema).
  • subscription.past_due — Cliente atrasou pagamento (invoice OVERDUE).
  • subscription.expired — Atingiu maxCycles ou expiresAt.

Saldo (transferência interna e câmbio):

  • transfer.internal — Saldo movido entre contas iHubGames. Chega nos dois lados com o mesmo transfer_id; o campo direction (sent/received) diz qual é o seu. Não exige módulo: qualquer conta pode receber. Ver payload.
  • balance.converted — Saldo em moeda estrangeira convertido para a sua moeda base numa liquidação de câmbio. Só ocorre em conta que mantém saldo em outra moeda. Ver payload.

Em V1 (postbackUrl per-tx), os toggles legacy continuam controlando: sendTransactionPaidWebhook, sendTransactionRefundedWebhook, sendTransactionChargebackWebhook, sendTransactionDisputeWebhook. Esses toggles só afetam V1 — V2 usa a array events do model Webhook em vez de flags booleanas.

Desabilite o que você não processa. Cada webhook que você recebe mas ignora é um para o qual seu endpoint ainda precisa responder 200 — é uma carga pequena mas desnecessária.

Distinguindo V1 e V2 no mesmo endpoint

Se você manda V1 e V2 pra mesma URL, distinguir no handler é trivial — V2 tem o envelope { event, environment, ... }; V1 não:

TypeScript
function handleWebhook(body: unknown) {
  if (typeof body === "object" && body !== null && "event" in body) {
    // V2: { event: "cashin.paid", environment: "TEST", payload: { ... } }
    return handleV2(body as V2Webhook);
  }
  // V1: { type: "TRANSACTION", status: "APPROVED", ...campos planos }
  return handleV1(body as V1Webhook);
}

Idempotência: se você se inscreveu em V2 cashin.paid E também passou postbackUrl em uma transação, vai receber 2 POSTs pro mesmo evento. Garanta que seu handler é idempotente — armazene o transactionId (V1) ou payload.transaction_id (V2) e dedup.

Migrando v1 → v2

Uma receita segura:

Passo 1 — Escreva o handler v2 em um controller dual-mode.

Detecte a versão pela estrutura: v2 sempre tem campos event e payload no topo; v1 tem type e campos planos.

TypeScript
function handleWebhook(body: unknown) {
  if (typeof body === "object" && body !== null && "event" in body && "payload" in body) {
    return handleV2(body as V2Webhook);
  }
  return handleV1(body as V1Webhook);
}

Passo 2 — Deploya o handler dual em produção, ainda configurado como v1.

Nada muda operacionalmente. Seu caminho v1 continua rodando como antes; o caminho v2 é código dormente, nunca executado.

Passo 3 — Teste v2 contra um ambiente de staging.

No ambiente TEST (sandbox), cadastre um endpoint V2 apontando pra uma URL de staging e dispare eventos de teste. Rode sua suite, verifique equivalência campo-a-campo com o que seu handler v1 gravava antes.

Passo 4 — Cadastre um endpoint V2 em Webhooks > Endpoints.

Vá em Webhooks > Endpoints (/auth/webhooks), clique em criar, informe sua URL e marque os eventos que quer receber (cashin.paid, cashout.success, etc). A partir desse cadastro, esses eventos passam a chegar em V2 naquela URL, com envelope { event, environment, payload }. Guarde o secret mostrado na criação — é com ele que você valida a assinatura HMAC desse endpoint.

⚠️ Não existe uma "flag" que te migra pra V2. V2 é um canal (o endpoint cadastrado), não um modo da conta. A opção "formato do payload" que aparece na aba Legado muda apenas o formato do postbackUrl legado — ela não cadastra endpoint nem passa a te mandar os eventos V2. Pra receber V2 de fato, o que vale é cadastrar o endpoint acima.

Passo 5 — Pare de usar o postbackUrl legado depois de estabilizar.

Quando o endpoint V2 estiver validado em produção, pare de enviar o campo postbackUrl nas chamadas de transação/saque (e desligue os toggles legacy). Até lá pode manter os dois — se aparecer regressão, é só desativar o endpoint V2 e o postbackUrl volta a ser sua única fonte. Lembre da idempotência enquanto os dois coexistem.

A migração inteira costuma ser menos de um dia de trabalho de engenharia se seus testes de webhook estiverem decentes. O grosso é o mapeamento de campos camelCase → snake_case.