# Webhooks

Cadastre uma URL sua para receber um POST assim que o status de uma cobrança ou saque mudar sem precisar ficar consultando a API.

## Cadastrar um endpoint

`POST /webhooks`

| Campo | Tipo |  | Descrição |
| --- | --- | --- | --- |
| `url` | string | obrigatório | HTTPS obrigatório. |
| `label` | string | opcional | Nome para identificar o endpoint no painel. |
| `events` | array | opcional | Lista de eventos (veja abaixo). Omitido = recebe todos. |

> ⚠️ O `secret` só aparece nesta resposta, na criação. Guarde-o é o que assina os eventos recebidos, e não há como recuperá-lo depois.

_json_

```json
{
  "object": "webhook_endpoint",
  "id": "whe_1",
  "url": "https://minhaloja.com/webhooks/stacepay",
  "events": null,
  "active": true,
  "last_delivery_at": null,
  "last_status_code": null,
  "consecutive_failures": 0,
  "created_at": "2026-08-11T21:25:50-03:00",
  "secret": "052742b9296fda91caa59228e6939bbc1e651fc"
}
```

## Listar, remover e ver entregas

`GET /webhooks`

`DELETE /webhooks/{id}`

`GET /webhook-deliveries?webhook_id={id}&limit=20&offset=0`

## Eventos disponíveis

| Evento | Quando dispara |
| --- | --- |
| `charge.paid` | Cobrança confirmada. |
| `charge.authorized` | Cartão autorizado (antes da captura). |
| `charge.processing` | Em confirmação com o parceiro. |
| `charge.refused` | Recusada. |
| `charge.failed` | Falhou antes do parceiro. |
| `charge.refunded` | Estornada. |
| `charge.chargeback` | Contestada pelo pagador. |
| `charge.disputed` | Em disputa. |
| `charge.blocked` | Bloqueada por análise de risco. |
| `withdrawal.paid` | Saque concluído. |
| `withdrawal.processing` | Enviado ao parceiro. |
| `withdrawal.approved` | Aprovado, aguardando envio. |
| `withdrawal.rejected` | Recusado. |
| `withdrawal.failed` | Falhou no envio. |
| `withdrawal.refunded` | Valor devolvido ao saldo. |
| `withdrawal.canceled` | Cancelado. |
| `withdrawal.blocked` | Bloqueado por análise. |

## Formato do evento

_json_

```json
{
  "id": "evt_9e2f...",
  "object": "event",
  "type": "charge.paid",
  "created_at": "2026-08-11T21:40:02-03:00",
  "data": { "object": "charge", "id": "sp1_d9c51fe9", "status": "paid", "...": "..." }
}
```

## Verificando a assinatura

Todo envio traz o cabeçalho `Novex-Signature: t=<timestamp>,v1=<hmac>`. O HMAC-SHA256 é calculado sobre a string `"{timestamp}.{corpo cru}"` usando o `secret` do endpoint.

_node_

```js
const crypto = require('crypto');

function verificar(header, body, secret) {
  const [tPart, vPart] = header.split(',');
  const timestamp = tPart.split('=')[1];
  const assinaturaRecebida = vPart.split('=')[1];
  const esperada = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${body}`)
    .digest('hex');
  return crypto.timingSafeEqual(Buffer.from(assinaturaRecebida), Buffer.from(esperada));
}
```

```php
function verificar(string $header, string $rawBody, string $secret): bool {
    [$tPart, $vPart] = explode(',', $header, 2);
    $timestamp = substr($tPart, 2);
    $recebida = substr($vPart, 3);
    $esperada = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
    return hash_equals($esperada, $recebida);
}
```

> 🚫 Sempre valide contra o corpo **cru** da requisição, antes de decodificar o JSON reserializar altera espaçamento e ordem de chaves, e a assinatura deixa de bater.

## Reenvio

Uma resposta fora da faixa `200–299` conta como falha. Reenviamos com backoff crescente (~1min, 5min, 30min, 2h, 6h) por até 6 tentativas. Depois de 20 falhas seguidas, o endpoint é desativado automaticamente reative cadastrando de novo.
