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:

Cadastrar webhook
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

  1. Configure a URL de webhook no dashboard ou via API
  2. Quando um evento ocorre, enviamos um POST para sua URL
  3. O payload é assinado com HMAC-SHA256 usando seu secret
  4. Responda com HTTP 2xx em até 10 segundos para confirmar
  5. Se sua URL não responder com 2xx, tentamos 5 vezes, com espera de 2s, 4s, 8s, 16s e 32s
  6. 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
  7. 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.paid

Cobrança paga com sucesso

charge.refunded

Cobrança reembolsada (estorno)

charge.expired

Cobrança expirada sem pagamento

dispute.opened

Nova disputa aberta

dispute.resolved

Disputa resolvida (favorável ou contra)

withdrawal.completed

Saque processado com sucesso pelo provedor

withdrawal.failed

Saque falhou no provedor (saldo devolvido)

withdrawal.rejected

Saque 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.

charge.paid: quando o pagamento é confirmado
JSON
{
  "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"
}
charge.refunded: quando o estorno é concluído
JSON
{
  "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"
}
charge.expired: quando o prazo de pagamento expira
JSON
{
  "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"
}
dispute.opened: quando uma disputa é aberta
JSON
{
  "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"
}
dispute.resolved: quando a disputa é finalizada
JSON
{
  "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"
}
withdrawal.completed: quando o saque é processado pelo provedor
JSON
{
  "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"
}
withdrawal.failed: quando o saque falha no provedor
JSON
{
  "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"
}
withdrawal.rejected: quando o saque é rejeitado pelo admin
JSON
{
  "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-Signature

HMAC-SHA256 no formato sha256=<hex>

X-Webhook-Id

ID da entrega (UUID). O MESMO em todas as tentativas dela: use como chave de deduplicação

Content-Type

application/json

User-Agent

Plataforma-Webhook/1.0

Verificando a assinatura

Node.js
javascript
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.

POST/v1/webhook-config

Autenticação

X-API-Keystringheaderobrigatório

Sua chave de API no header X-API-Key: sk_live_.... Gere em Dashboard → Integrações.

Body (JSON)

urlstringobrigatório

URL HTTPS que receberá os webhooks

secretstringopcional

Secret HMAC (mín. 16 chars). Auto-gerado se omitido.

eventsstring[]opcional

Filtro de eventos. Se omitido, recebe todos os eventos.

/v1/webhook-config
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.

GET/v1/webhook-config

Autenticação

X-API-Keystringheaderobrigatório

Sua chave de API no header X-API-Key: sk_live_.... Gere em Dashboard → Integrações.

/v1/webhook-config
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"
    }
  ]
}