Webhooks
Configure URLs de notificação, valide assinaturas HMAC e gerencie reenvios.
Referência rápida
Quick start
Cadastre um webhook e comece a receber notificações em tempo real:
curl -X POST https://api.pagniv.com/v1/webhook-config \
-H "X-API-Key: sk_live_sua_chave" \
-H "Content-Type: application/json" \
-d '{
"url": "https://seusite.com/webhooks/pagniv",
"events": ["charge.paid"]
}'Como funciona
- Configure a URL de webhook no dashboard ou via API
- Quando um evento ocorre, enviamos um POST para sua URL
- O payload é assinado com HMAC-SHA256 usando seu secret
- Responda com HTTP 2xx em até 10 segundos para confirmar
- Se sua URL não responder com 2xx, tentamos 5 vezes, com espera de 2s, 4s, 8s, 16s e 32s
- O MESMO evento pode chegar mais de uma vez (reentrega, ou sua resposta passou de 10s). Trate o handler como idempotente, usando o X-Webhook-Id como chave: ele NÃO muda entre as tentativas da mesma entrega
- As 5 tentativas cabem em cerca de 1 minuto. Se o seu endpoint estiver fora por mais do que isso, o evento NÃO é reenviado depois, e você não tem como pedir o reenvio: reconcilie consultando GET /v1/charges/{id}. GET /v1/webhook-deliveries mostra quais entregas falharam
Como testar antes de produção
Em sandbox, simule o pagamento de uma cobrança com POST /v1/charges/{id}/simulate-payment para disparar o webhook charge.paid e validar o seu handler de ponta-a-ponta.
Eventos disponíveis
charge.paidCobrança paga com sucesso
charge.refundedCobrança reembolsada (estorno)
charge.expiredCobrança expirada sem pagamento
dispute.openedNova disputa aberta
dispute.resolvedDisputa resolvida (favorável ou contra)
withdrawal.completedSaque processado com sucesso pelo provedor
withdrawal.failedSaque falhou no provedor (saldo devolvido)
withdrawal.rejectedSaque rejeitado pelo admin (saldo devolvido)
Exemplos de payload
Todo evento segue o mesmo envelope (event, data, timestamp). O conteúdo de data varia conforme o evento.
{
"event": "charge.paid",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"txid": "pagniv_abc123def456",
"amount": 15000,
"netAmount": 14901,
"feeAmount": 99,
"status": "PAID",
"paidAt": "2026-01-15T11:05:00Z",
"externalId": "pedido-1234"
},
"timestamp": "2026-01-15T11:05:01Z"
}{
"event": "charge.refunded",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"txid": "pagniv_abc123def456",
"amount": 15000,
"netAmount": 14901,
"feeAmount": 99,
"status": "REFUNDED",
"paidAt": "2026-01-15T11:05:00Z",
"refundedAmount": 14901,
"refundReason": "Solicitação do cliente",
"externalId": "pedido-1234"
},
"timestamp": "2026-01-16T09:32:11Z"
}{
"event": "charge.expired",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"txid": "pagniv_abc123def456",
"amount": 15000,
"netAmount": 14901,
"feeAmount": 99,
"status": "EXPIRED",
"paidAt": null,
"externalId": "pedido-1234"
},
"timestamp": "2026-01-15T12:00:01Z"
}{
"event": "dispute.opened",
"data": {
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"chargeId": "550e8400-e29b-41d4-a716-446655440000",
"amount": 14901,
"reason": "FRAUD",
"description": "Não reconheço esta transação no meu extrato",
"status": "OPEN",
"deadline": "2026-01-22T11:05:00Z",
"createdAt": "2026-01-15T14:22:00Z"
},
"timestamp": "2026-01-15T14:22:01Z"
}{
"event": "dispute.resolved",
"data": {
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"chargeId": "550e8400-e29b-41d4-a716-446655440000",
"amount": 14901,
"reason": "FRAUD",
"description": "Não reconheço esta transação no meu extrato",
"status": "RESOLVED_MERCHANT",
"deadline": "2026-01-22T11:05:00Z",
"createdAt": "2026-01-15T14:22:00Z",
"resolvedAt": "2026-01-18T10:11:00Z",
"resolution": "Evidência de entrega aceita; merchant venceu."
},
"timestamp": "2026-01-18T10:11:01Z"
}{
"event": "withdrawal.completed",
"data": {
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"amount": 50000,
"fee": 200,
"netAmount": 49800,
"pixKey": "12345678900",
"pixKeyType": "CPF",
"status": "COMPLETED",
"providerTxId": "E60746948202601151200ABC123",
"createdAt": "2026-01-15T11:30:00Z"
},
"timestamp": "2026-01-15T11:30:15Z"
}{
"event": "withdrawal.failed",
"data": {
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"amount": 50000,
"fee": 200,
"netAmount": 49800,
"pixKey": "12345678900",
"pixKeyType": "CPF",
"status": "FAILED",
"providerTxId": null,
"createdAt": "2026-01-15T11:30:00Z"
},
"timestamp": "2026-01-15T11:30:20Z"
}{
"event": "withdrawal.rejected",
"data": {
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"amount": 50000,
"fee": 200,
"netAmount": 49800,
"pixKey": "12345678900",
"pixKeyType": "CPF",
"status": "REJECTED",
"providerTxId": null,
"createdAt": "2026-01-15T11:30:00Z",
"rejectedAt": "2026-01-15T13:00:00Z",
"rejectedReason": "Chave Pix divergente do cadastro"
},
"timestamp": "2026-01-15T13:00:01Z"
}Headers enviados
X-Webhook-SignatureHMAC-SHA256 no formato sha256=<hex>
X-Webhook-IdID da entrega (UUID). O MESMO em todas as tentativas dela: use como chave de deduplicação
Content-Typeapplication/json
User-AgentPlataforma-Webhook/1.0
Verificando a assinatura
const crypto = require('crypto');
// rawBody e um Buffer com os bytes CRUS da requisicao, nunca o JSON
// reserializado: reserializar muda espacamento e ordem de chaves.
function verifyWebhook(rawBody, signatureHeader, secret) {
if (!signatureHeader) return false;
const recebida = Buffer.from(signatureHeader.replace('sha256=', ''), 'hex');
const esperada = crypto.createHmac('sha256', secret).update(rawBody).digest();
// timingSafeEqual LANCA quando os tamanhos diferem, e uma assinatura
// malformada chega de fora: sem esta guarda, o handler quebra com 500 em vez
// de recusar com 401.
if (recebida.length !== esperada.length) return false;
return crypto.timingSafeEqual(recebida, esperada);
}Criar webhook
Cadastra uma URL para receber notificações. Máximo 5 webhooks por organização. O secret para verificar assinaturas só é retornado nesta resposta.
/v1/webhook-configAutenticação
Sua chave de API no header X-API-Key: sk_live_.... Gere em Dashboard → Integrações.
Body (JSON)
urlstringobrigatórioURL HTTPS que receberá os webhooks
secretstringopcionalSecret HMAC (mín. 16 chars). Auto-gerado se omitido.
eventsstring[]opcionalFiltro de eventos. Se omitido, recebe todos os eventos.
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
url | string | Sim | URL HTTPS que receberá os webhooks |
secret | string | Não | Secret HMAC (mín. 16 chars). Auto-gerado se omitido. |
events | string[] | Não | Filtro de eventos. Se omitido, recebe todos os eventos. |
curl -X POST https://api.pagniv.com/v1/webhook-config \
-H "X-API-Key: sk_live_sua_chave" \
-H "Content-Type: application/json" \
-d '{
"url": "https://seusite.com/webhooks/pagniv",
"events": ["charge.paid", "charge.expired"]
}'{
"success": true,
"data": {
"id": "550e8400-...",
"url": "https://seusite.com/webhooks/pagniv",
"secret": "84bddbc2e93d00b77c9315ba2a2ca92a0a7e1b014971ad9f75b4dc995e74ed9e",
"events": ["charge.paid", "charge.expired"],
"isActive": true,
"createdAt": "2026-01-10T08:00:00Z"
}
}O secret só é retornado nesta resposta. Salve-o em local seguro para verificar assinaturas. Para editar, remover ou testar webhooks, acesse Dashboard → Integrações → Webhooks. PARA TROCAR O SECRET sem perder evento: crie um SEGUNDO webhook na mesma URL com o secret novo, aceite as duas assinaturas no seu handler por um período, e só então remova o antigo. Cada cópia é assinada com o secret da sua própria config, então nessa janela você recebe cada evento duas vezes (deduplique pelo X-Webhook-Id). NÃO autentique por IP de origem: não publicamos os IPs de saída, eles mudam sem aviso, e a assinatura é justamente a prova que não depende de onde a chamada veio.
Listar webhooks
Retorna todos os webhooks cadastrados. O secret não é exibido. Se você perdeu, crie outro webhook na mesma URL e remova este depois (ver Criar webhook): assim nenhum evento se perde no meio.
/v1/webhook-configAutenticação
Sua chave de API no header X-API-Key: sk_live_.... Gere em Dashboard → Integrações.
curl "https://api.pagniv.com/v1/webhook-config" \
-H "X-API-Key: sk_live_sua_chave"{
"success": true,
"data": [
{
"id": "550e8400-...",
"url": "https://seusite.com/webhooks/pagniv",
"events": ["charge.paid", "charge.expired"],
"isActive": true,
"createdAt": "2026-01-10T08:00:00Z"
}
]
}