# Webhooks

Receba atualizações de pagamento, verifique assinaturas e trate repetições.

Source: https://docs.invoise.me/pt-br/integration/webhooks/

Um webhook é uma requisição HTTP que o Invoise envia ao seu servidor quando um pagamento muda. Você pode começar com [verificações de status por GET](https://docs.invoise.me/pt-br/payments/status/) e adicionar webhooks quando precisar de atualizações automáticas.

## 1. Registre seu endpoint

```bash
curl -X POST 'https://platform.invoise.me/api/v1/shops/{shop_id}/webhooks' \
  -H 'Authorization: Bearer ivk_...' \
  -H 'Idempotency-Key: <saved-unique-key>' \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://example.com/invoise","filters":["transfer","invoice","payout"]}'
```

Use sua própria URL HTTPS pública. Endereços locais e privados são rejeitados. Salve o `secret` retornado quando o endpoint é criado: você precisa dele para verificar as requisições.

Uma loja pode ter até 10 endpoints. A URL de um endpoint pode ser alterada, então reutilize um em vez de adicionar outro. Veja [Limites](https://docs.invoise.me/pt-br/integration/idempotency-and-errors/#limites).

## 2. Escolha os eventos que você precisa

| Evento | Significado |
| --- | --- |
| `transfer.observed` | Uma transferência foi vista; ainda não está confirmada. |
| `transfer.confirmed` | Uma transferência recebida tem confirmações suficientes. Pode ser apenas um pagamento parcial da fatura. |
| `transfer.reverted` | Um evento de transferência anterior foi revertido. |
| `invoice.closed` | A fatura se fechou na blockchain. |
| `invoice.cancelled` | A página de pagamento foi cancelada ou a fatura expirou; isso não é um reembolso. |
| `payout.sent` | O repasse foi transmitido ou sua transferência de saída foi vista. |
| `payout.confirmed` | A contabilização completa da liquidação está disponível. |
| `payout.reverted` | Uma confirmação de repasse anterior foi revertida. |
| `payout.failed` | Uma falha terminal de repasse precisa de intervenção. |

Para `invoice.cancelled`, `data.reason` diz o motivo: `expired` quando a [vida útil da fatura](https://docs.invoise.me/pt-br/payments/deposits-and-invoices/#vida-útil-da-fatura) terminou, `merchant_blocked` quando a equipe do Invoise bloqueou o lojista. Um cancelamento pelo lojista não tem `reason`.

Assine por grupo: `transfer`, `invoice`, `payout`. Transferências recebidas e repasses de saída são eventos diferentes. Um atraso temporário de RPC ou gas não é `payout.failed`.

`gas.wait` e `sweep.failed` são aceitos como filtros, mas o Invoise não os envia ao seu endpoint. São sinais internos: `gas.wait` significa que um repasse está aguardando fundos de taxa de rede e continua por conta própria; `sweep.failed` significa que uma tentativa de mover os fundos ao seu destinatário falhou. Você não precisa agir em nenhum dos dois casos. Um repasse que não consegue se completar chega até você como `payout.failed`.

Em uma loja sandbox você pode simular os dois por `POST /shops/{shop_id}/sandbox/simulate` para verificar que um repasse atrasado não quebra seu fluxo. Eles alteram apenas o estado do repasse e não enviam webhook.

## 3. Verifique antes de processar

`Invoise-Signature` tem o formato `t=<timestamp>,v1=<hex>`. A assinatura é HMAC-SHA256 de `timestamp + "." + raw_body`, usando o segredo do seu webhook.

O exemplo em Node.js a seguir também rejeita timestamps com mais de cinco minutos de diferença do relógio do servidor. Mantenha esse relógio sincronizado e escolha sua tolerância deliberadamente.

```js
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(rawBody, header, secret) {
  const match = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(header ?? '');
  if (!match) return false;
  const [, timestamp, signature] = match;
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!Number.isFinite(age) || age > 300) return false;
  const expected = createHmac('sha256', secret)
    .update(timestamp + '.')
    .update(rawBody)
    .digest();
  return timingSafeEqual(Buffer.from(signature, 'hex'), expected);
}
```

Passe os bytes originais da requisição como `rawBody`. Não faça parse do JSON e stringify antes de verificar a assinatura. Rejeite assinaturas inválidas antes de armazenar ou agir sobre o conteúdo.

## 4. Processe uma vez

Um payload resumido se parece com isto:

```json
{
  "id": "<event-id>",
  "type": "transfer.confirmed",
  "created_at": "2026-09-19T10:00:00Z",
  "data": {
    "issuance_id": "<issuance-id>",
    "shop_id": "<shop-id>",
    "external_id": "order-123",
    "type": "invoice",
    "invoice_cancelled": false
  }
}
```

| Caso | Tratamento |
| --- | --- |
| Duplicatas | Deduplique usando `Invoise-Event-ID`, que corresponde ao `id` do envelope. Salve o ID e a atualização de negócio atomicamente. |
| Aceitar um evento | Salve eventos aceitos de forma durável, retorne 2xx prontamente e processe o trabalho enfileirado depois. |
| Repetições e reenvios | Repetições e reenvios manuais reutilizam o ID do evento. `Invoise-Attempt-ID` muda e não é uma chave de deduplicação. |
| Ordem dos eventos | Os eventos podem chegar fora de ordem. Use seu conteúdo e reconcilie com o [status atual](https://docs.invoise.me/pt-br/payments/status/) quando necessário. |

Para reversões, `chain_event.reference_event_id` aponta para o evento anterior na blockchain. Os payloads de repasse incluem um `payout_id` estável, hash de transação, valores e detalhes de taxa; valores desconhecidos são `null`. A lista de IDs de transferências recebidas deles é limitada a 100 e sinaliza truncamento.

Eventos armazenados antigos podem usar `project_id`; leia `shop_id` primeiro e use-o como alternativa se necessário. Reenvios mantêm os bytes originais.

## Inspecione entregas com falha

Liste com `GET /api/v1/shops/{shop_id}/deliveries`. Reenvie uma entrega com `POST /api/v1/shops/{shop_id}/deliveries/{id}/replay` e uma chave de idempotência salva. Reenviar não cria um novo evento de negócio.
