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"]
}'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.
activerecebe eventos
revokedpara todas as entregas
pausednada é entregue nem guardado
activevolta a receber eventos novos
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
callbackUrlcom 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.
| Evento | Quando é enviado | x-Webhook-Correlation-Id |
|---|---|---|
charge.paid | A cobrança passou para PAID. | id da cobrança |
charge.refunded | Um reembolso da cobrança foi processado, parcial ou total. | id da cobrança |
invoice.paid | Uma invoice foi paga. | id da invoice |
invoice.refunded | Uma invoice foi reembolsada. | id da invoice |
payout.completed | O saque foi aceito e debitado do saldo. O dinheiro ainda não saiu. | id do saque |
payout.paid | O dinheiro do saque saiu. | id do saque |
payout.failed | O saque foi recusado na criação ou falhou depois de aceito. | id do saque |
auto_payout.skipped | O saque automático configurado na cobrança não chegou a ser agendado. | id da cobrança |
auto_payout.failed | O saque automático foi agendado, mas não saiu. | id da cobrança |
Sobre os saques:
payout.completedaceito e debitado; o dinheiro não saiu
payout.failedpayout.paido dinheiro saiu
payout.failed- A sequência normal é
payout.completede depoispayout.paidoupayout.failed. Alguns meios de saque pulam opayout.completede 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.updatedAtmais recente. - Saques automáticos (
settlementConfigna cobrança) não enviampayout.*; eles têm os eventosauto_payout.*.
Pagamentos em cripto
Há um evento por status do pagamento. O x-Webhook-Correlation-Id é o id do pagamento em cripto.
| Evento | Quando é enviado |
|---|---|
crypto_payment.waiting | Aguardando o pagamento na rede. |
crypto_payment.confirming | Transação vista, aguardando confirmações da rede. |
crypto_payment.confirmed | Confirmado na rede. Intermediário: o valor ainda não foi liquidado. |
crypto_payment.sending | Fundos em processamento. |
crypto_payment.partially_paid | Pago abaixo do valor esperado. |
crypto_payment.finished | Liquidado. É o único evento de sucesso final. |
crypto_payment.failed | O pagamento falhou. |
crypto_payment.refunded | O pagamento foi reembolsado. |
crypto_payment.expired | O 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.
| Header | Conteúdo |
|---|---|
x-Webhook-Signature | Assinatura no formato t=<timestamp>,v1=<hmac>. Veja Verificar a assinatura. |
x-Webhook-Event-Id | Id da entrega. O mesmo em todas as tentativas; use para idempotência. |
x-Webhook-Event-Type | Nome do evento, por exemplo charge.paid. |
x-Webhook-Timestamp | Momento da assinatura, em segundos Unix. Igual ao t da assinatura. |
x-Webhook-Correlation-Id | Id do recurso do evento (veja a tabela de eventos). Ausente quando o evento não tem um. |
X-Gates2b-Environment | production 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:
- Leia
tev1do headerx-Webhook-Signature(t=1718460605,v1=5f2b…). - Rejeite a entrega se
testiver 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. - Calcule
HMAC-SHA256(secret, t + "." + corpo)em hexadecimal minúsculo.corposão os bytes exatos recebidos, antes de qualquer parse de JSON. - Compare o resultado com
v1em 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.na horaTentativa 1logo depois do evento
- 2.+30 s, +1 min, +2 minTentativas 2 a 4cada espera conta da tentativa anterior
- 3.+4, +8, +16, +32 minTentativas 5 a 8
- 4.+1 h 4, +2 h 8, +4 h 16Tentativas 9 a 11
- 5.+6 h cadaTentativas 12 a 15cerca de 32 h após o evento
- 6.15 tentativas ou 7 diasEntrega abandonadasem nova tentativa automática
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.
failureCountda inscrição soma cada tentativa que falha e volta a0na primeira entrega aceita.- Pausar ou revogar a inscrição descarta as tentativas pendentes. Eventos gerados enquanto ela não está
activenã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-Idde 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 (
idda cobrança, do saque…). Um reenvio manual gera uma entrega nova, com outrox-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.