Gates2b

Webhooks

Receba no seu servidor, com assinatura HMAC, os eventos de pagamento, reembolso e saque da sua conta.

Um webhook avisa o seu sistema quando algo muda na sua conta: uma cobrança foi paga, um reembolso saiu, um saque foi concluído. Você cadastra uma URL com uma inscrição de webhook, escolhe os eventos e a Gates2b faz um POST assinado nessa URL a cada evento. Assim você não precisa consultar a API em loop.

Como receber eventos

Publique um endpoint https que aceite POST com corpo JSON e responda 2xx em até 10 segundos.

Crie a inscrição com Criar inscrição, informando a callbackUrl e os eventos em subscribedEvents.

Guarde o secret da resposta em um cofre de segredos. Ele só aparece nessa resposta.

Em cada entrega, verifique a assinatura, descarte duplicatas pelo id do evento e responda 2xx.

curl -X POST "https://api.gates2b.com/webhook-subscriptions" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "callbackUrl": "https://loja.example.com/webhooks/pagamentos",
    "subscribedEvents": ["charge.paid", "charge.refunded", "payout.paid", "payout.failed"]
  }'
Uma entrega: a Gates2b assina e envia o evento, o seu servidor valida e responde 2xx; sem 2xx, a mesma entrega volta mais tarde.

Cada inscrição tem uma URL. Para mandar eventos diferentes para URLs diferentes, crie uma inscrição por URL. Um evento assinado por mais de uma inscrição é entregue a cada uma delas, com assinatura e id próprios.

Para gerenciar as inscrições: Listar, Consultar, Atualizar, Rotacionar segredo, Pausar, Ativar e Revogar.

Status da inscrição

A inscrição nasce active e só recebe eventos nesse status. Você pausa para manutenção do endpoint e revoga para desligar a URL. A Gates2b também pausa a inscrição sozinha quando, na hora da entrega, a URL aponta para um endereço não permitido.

active

recebe eventos

Revogar
revoked

para todas as entregas

Pausar ou URL recusada na entrega
paused

nada é entregue nem guardado

Ativar
active

volta a receber eventos novos

Status de uma inscrição de webhook: só active recebe eventos; paused volta com Ativar; revoked desliga a URL.

O que acontece com os eventos em cada mudança:

  • Pausar: eventos gerados durante a pausa não são criados para a inscrição, e uma nova tentativa que vencer nesse intervalo é descartada. Ao ativar, a entrega recomeça só com eventos novos.
  • Revogar: as tentativas pendentes são descartadas. Para usar outra URL, crie uma inscrição nova.
  • Pausa automática: corrija a callbackUrl com Atualizar antes de Ativar; senão a próxima entrega pausa a inscrição de novo.

Eventos

Use em subscribedEvents o nome exato da primeira coluna. A API aceita um nome com erro de grafia, mas nenhum evento com esse nome existe, e a inscrição não recebe nada.

EventoQuando é enviadox-Webhook-Correlation-Id
charge.paidA cobrança passou para PAID.id da cobrança
charge.refundedUm reembolso da cobrança foi processado, parcial ou total.id da cobrança
invoice.paidUma invoice foi paga.id da invoice
invoice.refundedUma invoice foi reembolsada.id da invoice
payout.completedO saque foi aceito e debitado do saldo. O dinheiro ainda não saiu.id do saque
payout.paidO dinheiro do saque saiu.id do saque
payout.failedO saque foi recusado na criação ou falhou depois de aceito.id do saque
auto_payout.skippedO saque automático configurado na cobrança não chegou a ser agendado.id da cobrança
auto_payout.failedO saque automático foi agendado, mas não saiu.id da cobrança

Sobre os saques:

payout.completed

aceito e debitado; o dinheiro não saiu

falha depois de aceito
payout.failed
dinheiro enviado
payout.paid

o dinheiro saiu

revertido pela operação
payout.failed
Eventos de um saque: payout.completed quando é aceito, depois payout.paid ou payout.failed; o desfecho ainda pode mudar.
  • A sequência normal é payout.completed e depois payout.paid ou payout.failed. Alguns meios de saque pulam o payout.completed e enviam só o desfecho; um saque recusado na criação recebe só payout.failed.
  • O desfecho pode mudar: um saque dado como falho pode ser confirmado depois, e um saque pago pode ser revertido. Os dois eventos chegam. Vale o que tiver o payout.updatedAt mais recente.
  • Saques automáticos (settlementConfig na cobrança) não enviam payout.*; eles têm os eventos auto_payout.*.

Pagamentos em cripto

Há um evento por status do pagamento. O x-Webhook-Correlation-Id é o id do pagamento em cripto.

EventoQuando é enviado
crypto_payment.waitingAguardando o pagamento na rede.
crypto_payment.confirmingTransação vista, aguardando confirmações da rede.
crypto_payment.confirmedConfirmado na rede. Intermediário: o valor ainda não foi liquidado.
crypto_payment.sendingFundos em processamento.
crypto_payment.partially_paidPago abaixo do valor esperado.
crypto_payment.finishedLiquidado. É o único evento de sucesso final.
crypto_payment.failedO pagamento falhou.
crypto_payment.refundedO pagamento foi reembolsado.
crypto_payment.expiredO prazo para pagar acabou sem pagamento.

Libere o pedido só em crypto_payment.finished. crypto_payment.confirmed não é pagamento concluído.

Formato da entrega

A Gates2b faz um POST na callbackUrl com Content-Type: application/json. O corpo é o objeto do evento; os headers abaixo vêm em toda entrega.

HeaderConteúdo
x-Webhook-SignatureAssinatura no formato t=<timestamp>,v1=<hmac>. Veja Verificar a assinatura.
x-Webhook-Event-IdId da entrega. O mesmo em todas as tentativas; use para idempotência.
x-Webhook-Event-TypeNome do evento, por exemplo charge.paid.
x-Webhook-TimestampMomento da assinatura, em segundos Unix. Igual ao t da assinatura.
x-Webhook-Correlation-IdId do recurso do evento (veja a tabela de eventos). Ausente quando o evento não tem um.
X-Gates2b-Environmentproduction ou sandbox.

Nomes de header não diferenciam maiúsculas de minúsculas. A maioria dos frameworks os entrega em minúsculas (x-webhook-signature).

Use o header x-Webhook-Event-Type para decidir como tratar a entrega. O corpo muda de evento para evento, e nem todo corpo traz o nome do evento.

{
  "event": "charge.paid",
  "charge": {
    "id": "019ecb7a-2ab0-7744-982f-e9d951d61746",
    "status": "PAID",
    "grossAmount": "100.00",
    "currency": "BRL",
    "externalReference": "pedido-123",
    "description": "Assinatura mensal",
    "customer": {
      "name": "Maria Silva",
      "email": "cliente@example.com",
      "document": "12345678909",
      "phone": "11999998888",
      "country": null
    },
    "paymentMethod": {
      "type": "PIX",
      "brand": null,
      "authorizationCode": null,
      "expMonth": null,
      "expYear": null,
      "funding": null,
      "last4": null,
      "bankIspb": "00000000"
    },
    "paidAt": "2026-06-15T14:10:05.094Z",
    "createdAt": "2026-06-15T14:00:00.000Z"
  },
  "attempt": {
    "id": "019ecb7b-0c41-7a5e-8d0f-3c2a9e1b7f42",
    "paymentMethod": "PIX",
    "idempotencyKey": "pedido-123-tentativa-1",
    "payer": {
      "name": "Maria Silva",
      "email": null,
      "document": "12345678909",
      "phone": null,
      "country": null
    }
  }
}

charge.customer é quem você (ou o comprador, no checkout) informou. attempt.payer é quem de fato pagou, segundo a instituição de origem. Os dois podem ser pessoas diferentes, e isso não é erro. Os três blocos vêm sempre com todas as chaves; o que não se sabe vem null.

Os eventos invoice.* trazem o mesmo objeto da notificação legada.

O corpo de cada evento, campo a campo, está em Eventos de webhook.

Verificar a assinatura

Toda entrega é assinada com HMAC-SHA256 usando o secret da inscrição. Verifique antes de confiar no corpo:

  1. Leia t e v1 do header x-Webhook-Signature (t=1718460605,v1=5f2b…).
  2. Rejeite a entrega se t estiver a mais de 300 segundos (5 minutos) do relógio do seu servidor, para cima ou para baixo. Isso barra o reenvio de uma entrega capturada.
  3. Calcule HMAC-SHA256(secret, t + "." + corpo) em hexadecimal minúsculo. corpo são os bytes exatos recebidos, antes de qualquer parse de JSON.
  4. Compare o resultado com v1 em tempo constante.

Reserializar o JSON muda espaços e ordem das chaves e quebra a assinatura. Configure o framework para entregar o corpo cru nessa rota. Mantenha o relógio do servidor sincronizado (NTP), ou a janela de 5 minutos rejeita entregas válidas.

import crypto from 'node:crypto';
import express from 'express';

const TOLERANCE_SECONDS = 300;

function verifySignature(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(',').map((part) => {
      const [key, ...value] = part.trim().split('=');
      return [key, value.join('=')];
    }),
  );
  const timestamp = Number.parseInt(parts.t, 10);
  if (!Number.isFinite(timestamp) || !parts.v1) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > TOLERANCE_SECONDS) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${parts.t}.`)
    .update(rawBody) // Buffer com o corpo cru
    .digest();
  const received = Buffer.from(parts.v1, 'hex');
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

const app = express();

// express.raw: req.body chega como Buffer, sem parse
app.post('/webhooks/pagamentos', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.get('x-webhook-signature') ?? '';
  if (!verifySignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }

  const eventId = req.get('x-webhook-event-id');
  const eventType = req.get('x-webhook-event-type');
  const payload = JSON.parse(req.body.toString('utf8'));

  // Grave eventId como processado e trate o evento fora da requisição.
  res.sendStatus(200);
});

Guardar o segredo

O secret vem em texto puro só na resposta de Criar inscrição e de Rotacionar segredo. Nenhuma outra rota o devolve. Guarde-o em um cofre de segredos ou variável de ambiente, nunca no repositório.

Se perder o segredo ou suspeitar de vazamento, rotacione. O anterior deixa de valer na hora, inclusive para novas tentativas de eventos já gerados. A página de Rotacionar segredo mostra como trocar sem rejeitar entregas.

Novas tentativas

A entrega conta como recebida quando a sua URL responde 2xx em até 10 segundos. Qualquer outro caso é falha: status 3xx (redirecionamentos não são seguidos), 4xx, 5xx, tempo esgotado ou erro de conexão. Depois de uma falha, a Gates2b tenta de novo com intervalo crescente:

  1. 1.na horaTentativa 1logo depois do evento
  2. 2.+30 s, +1 min, +2 minTentativas 2 a 4cada espera conta da tentativa anterior
  3. 3.+4, +8, +16, +32 minTentativas 5 a 8
  4. 4.+1 h 4, +2 h 8, +4 h 16Tentativas 9 a 11
  5. 5.+6 h cadaTentativas 12 a 15cerca de 32 h após o evento
  6. 6.15 tentativas ou 7 diasEntrega abandonadasem nova tentativa automática
Novas tentativas de uma entrega: o intervalo dobra a partir de 30 s até 6 h; depois da 15ª tentativa ou de 7 dias, a entrega é abandonada.

Cada espera varia até 25% para mais ou para menos. Depois da 15ª tentativa (cerca de 32 horas após o evento), ou 7 dias após o evento, o que vier primeiro, a entrega é abandonada e não há nova tentativa automática. Nesse caso, consulte o estado do recurso pela API.

  • failureCount da inscrição soma cada tentativa que falha e volta a 0 na primeira entrega aceita.
  • Pausar ou revogar a inscrição descarta as tentativas pendentes. Eventos gerados enquanto ela não está active não são entregues depois.
  • Se, no momento da entrega, a URL apontar para um endereço não permitido (IP privado, porta fora de 80, 443, 8080 e 8443), a entrega é abandonada e a inscrição passa para paused.

Responda 2xx assim que validar a assinatura e processe o evento depois. Um processamento lento que estoura os 10 segundos vira falha e gera nova tentativa.

Idempotência

A entrega é pelo menos uma vez: o mesmo evento pode chegar mais de uma vez, e a ordem de chegada não é garantida.

  • Guarde o x-Webhook-Event-Id de cada entrega processada e ignore as que repetirem o id. Novas tentativas da mesma entrega usam o mesmo id.
  • Torne a ação do seu lado idempotente também pelo recurso (id da cobrança, do saque…). Um reenvio manual gera uma entrega nova, com outro x-Webhook-Event-Id.
  • Não confie só no valor do webhook para liberar um pedido. Compare com o pedido do seu lado ou consulte a cobrança pela API.

Para pedir de novo a notificação de uma invoice paga, use Reenviar webhook. O reenvio chega às inscrições que assinam invoice.paid.

Webhook legado configurado no painel

Contas antigas podem ter uma URL de notificação cadastrada no painel, em Integração. Essa URL recebe um POST quando o status de uma invoice muda, criada pelos endpoints legados de /invoices.

Ela continua funcionando, mas tem limites que a inscrição de webhook não tem:

  • Sem assinatura. Não há x-Webhook-Signature; você não consegue provar que a chamada veio da Gates2b.
  • Sem a política de novas tentativas. Se o seu servidor falhar, a notificação pode se perder. Confira o status pela API ou use Reenviar webhook.
  • Sem escolha de eventos. Toda mudança de status chega na mesma URL.

Para integração nova, use inscrição de webhook com invoice.paid e invoice.refunded. Se você já usa a URL do painel, mantenha as duas até migrar o seu endpoint para a verificação de assinatura; depois apague a URL do painel para não processar o mesmo pagamento duas vezes.

Exemplo de corpo, com a invoice paga:

{
  "id": 72276,
  "id_transaction": "26182891-7569-414f-aa60-4f2b8e1c9d3a",
  "transaction_pix_id": "e3c1a2b4-50d3-4de7-911d-8a7b6c5d4e3f",
  "external_reference": "pedido-123",
  "emv": "00020101021226...6304ABCD",
  "amount": 120,
  "status": "Pagamento realizado",
  "international_status": "PAYMENT_RECEIVED",
  "name_client": "MARIA SILVA",
  "document_client": "12345678909",
  "createdAt": "2026-06-15T12:24:49.056Z",
  "user": {}
}

international_status é o campo estável para o seu código. Valores: WAITING_PAYMENT, WAITING_PROCESSING, REVIEW_REQUIRED, PAYMENT_RECEIVED, PARTIALLY_PAID, PAYMENT_EXPIRED, PAYMENT_FAILED, WITHDRAWAL_IN_PROGRESS, WITHDRAWAL_IN_PROCESSING, WITHDRAWAL_COMPLETED, WITHDRAWAL_FAILED, REFUND_REQUESTED e BALANCE_REMOVED. O pagamento confirmado é PAYMENT_RECEIVED. status é o mesmo estado em texto, em português, e pode mudar.

Nesta página