<!-- Source: https://docs.infraio.xyz/pt-BR/webhooks/overview -->
<!-- Last updated: 2026-10-04 -->

# Webhooks — Visão geral

Webhooks são o sinal **oficial**. Callbacks do navegador (`onSuccess`)
e visões do dashboard são conveniência; webhooks são a verdade.

## Garantias de entrega

- **Pelo menos uma vez.** Um único evento pode ser entregue até **6
  vezes** se o seu servidor não retornar 2xx dentro do timeout.
  Torne o seu handler idempotente — deduplique em `X-Delivery` (o
  payload não tem campo `event_id`; o UUID estável da entrega é a
  chave de idempotência).
- **Um evento por requisição HTTP.** Sem batching.
- **Isolamento por endpoint.** Se você tem múltiplos endpoints
  registrados, cada um tem a própria trilha de entrega e retry. Um
  endpoint lento não atrasa os outros.
- **Assinado.** Todo payload leva um header `X-Signature` (e durante
  a janela de 24 horas depois de uma rotação, também um
  `X-Signature-Prev`). Verifique antes de fazer qualquer coisa com
  o body. Veja [Verificação de assinatura](https://docs.infraio.xyz/pt-BR/webhooks/signature-verification).

## Tipos de evento que se pode assinar

| Evento | Dispara quando… |
| --- | --- |
| `payment.settled` | A transferência on-chain atingiu a contagem de confirmações da rede. **Use isto para marcar pedidos como pagos.** |
| `payment.failed` | Um pagamento fiat foi rejeitado pelo provedor de pagamento. Não dispara para timeouts de cripto — esses afloram como `checkout.expired`, e pagamentos cripto curtos afloram como `payment.underpaid`. |
| `payment.underpaid` | Fundos chegaram, mas abaixo do total do pedido (típico: taxa de transferência de stablecoin descontada do valor). |
| `payment.overpaid` | Fundos chegaram em excesso ao total do pedido. O excedente é registrado mas não é auto-reembolsado. |
| `order.created` | Um novo pedido foi aberto — pela sua chamada de API B2B ou por uma conversão de checkout-session. |
| `order.canceled` | Um pedido foi movido para cancelado. O `data.reason` do payload distingue cancel manual de `payment_timeout` (um pedido não pago expirou por timeout). |
| `order.resolved` | Um pedido `PARTIAL_PAID` foi resolvido para `PAID` — o lojista aceitou o déficit. |
| `order.reopened` | Um pedido previamente auto-cancelado (`canceled_reason=payment_timeout`) foi reaberto pelo lojista. |
| `checkout.created` | Um comprador abriu o checkout para um pedido. |
| `checkout.completed` | O fluxo do lado do comprador terminou (não implica liquidação on-chain — use `payment.settled` para isso). |
| `checkout.expired` | O comprador abandonou e o TTL da sessão acabou. |
| `payment.refund.requested` | Um registro de reembolso foi criado — seja a partir de uma chamada de API iniciada pelo lojista, seja a partir de um formulário de pedido de reembolso submetido pelo cliente. |
| `payment.refund.approved` | Um reembolso pendente passou pelo seu workflow de aprovação. |
| `payment.refund.rejected` | Um reembolso pendente foi negado. |
| `payment.refund.executed` | A transferência on-chain do reembolso foi confirmada e o registro foi para o estado terminal `executed`. |
| `refund_request.created` | Um token de pedido de reembolso foi criado. `data.source` é `b2b` / `dashboard` / `renewal`. Opcional inscrever-se — útil para pipelines de auditoria que acompanham qual token está ativo no momento por pedido. |
| `refund_request.renewal_requested` | Um comprador clicou em "Pedir link novo" depois que o token expirou. **Fortemente recomendado inscrever-se** — é o sinal de que o widget de renovação tem um novo item para agir. |
| `refund_request.renewed` | Uma renovação foi aprovada e um novo token substituiu o antigo. `data.old_token` / `data.new_token` formam a cadeia de auditoria. |
| `refund_request.canceled` | Um lojista moveu um token para `CANCELED` pelo dashboard (por exemplo, negou um pedido de renovação, matou um link vivo). Idempotente — só a primeira transição emite. `data.reason` é a nota opcional do lojista. |

### Planejados (em breve)

> **Note:**
>
> **Em breve.** Esses eventos pertencem a faturas recorrentes e
> assinaturas, que ainda não estão disponíveis. Eles **não** estão na
> tabela de eventos assináveis acima e não podem ser assinados hoje. Veja
> [Faturas recorrentes](https://docs.infraio.xyz/pt-BR/guides/recurring-invoices).

| Evento planejado | Dispara quando… |
| --- | --- |
| `subscription.created` | Uma assinatura é criada. |
| `invoice.created` | Uma fatura de um ciclo de cobrança é criada. |
| `invoice.paid` | Uma fatura é paga. |
| `subscription.past_due` | Uma fatura continua sem pagamento após o vencimento. |
| `subscription.canceled` | Uma assinatura é cancelada. |

O formulário de endpoint do dashboard lista os mesmos eventos. Tentar
se inscrever em um evento que não existe é rejeitado quando você salva
o endpoint.

> **Note:**
>
> **Eventos de teste não são assináveis.** O botão **Send Test** por
> endpoint no dashboard envia um evento `webhook.test.ping` para aquele
> único endpoint imediatamente, sem retentativas. Ele não aparece no
> catálogo acima: você o recebe por ter um endpoint registrado, não por
> ter se inscrito.

> **Note:**
>
> Inscreva-se só nos eventos que você trata. Cada endpoint tem o
> próprio filtro de evento; o wildcard `"*"` significa "todo evento,
> incluindo os adicionados no futuro". Inscrever-se em menos eventos
> deixa o seu handler mais simples e significa menos retentativas quando
> o seu endpoint tem erros.

## Payload + headers

**O corpo HTTP é o objeto de dados específico do evento, direto.**
Sem envelope externo estilo Stripe — campos como tipo do evento, ID
da entrega e timestamp de emissão ficam nos **headers**. Para
`payment.settled` o body fica assim:

```json
{
  "receipt_id":        "rcp_…",
  "order_id":          "ord_…",
  "payment_intent_id": "pin_…",
  "checkout_session_id": "cst_…",
  "merchant_id":       "mer_…",
  "customer_id":       "cus_…",
  "total":             "49.00",
  "currency":          "USD",
  "payment_method":    "crypto",
  "token":             "USDC",
  "network":           "polygon",
  "tx_hash":           "0x…",
  "deposit_address":   "0x…",
  "treasury_address":  "0x…",
  "amount_received":   "49.00",
  "confirmations":     5,
  "metadata":          { /* por evento */ }
}
```

Outros eventos carregam os próprios campos. Nomes de campo são
estáveis (lower snake_case); o tx hash on-chain é sempre `tx_hash`.

`tx_hash` é o identificador da transação no formato próprio da rede (`0x…` em redes EVM; o hash ou a assinatura nativos em TRON, Solana e TON). `deposit_address` pode estar ausente em TRON, Solana e TON, porque lá os compradores pagam diretamente a sua carteira de Tesouraria, e `confirmations` segue [Redes e ativos](https://docs.infraio.xyz/pt-BR/concepts/chains).

### Headers na requisição de entrada

```http
Content-Type:      application/json
X-Event:           payment.settled
X-Delivery:        7c9e6679-7425-40de-944b-e07fc1f90ae7
Idempotency-Key:   7c9e6679-7425-40de-944b-e07fc1f90ae7
X-Timestamp:       1729536000
X-Signature:       sha256=9a8b7c…
X-Signature-Prev:  sha256=fa31b2…    (só durante uma janela de tolerância de rotação)
```

| Header | O que é |
| --- | --- |
| `X-Event` | O tipo do evento (por exemplo, `payment.settled`). Rotear nisso na camada de proxy se você quer pular o parse do JSON. |
| `X-Delivery` | UUID que identifica a linha de entrega. **Estável em todas as retentativas** do mesmo par `(event, endpoint)` — use como sua chave de idempotência. |
| `Idempotency-Key` | Espelha o `X-Delivery` (mesmo valor). Definido em toda entrega. |
| `X-Timestamp` | Unix-segundos do momento em que a tentativa foi enviada. Assinado dentro do payload para que um par `(body, X-Signature)` capturado não possa ser replayado indefinidamente — rejeite entregas cujo timestamp esteja fora da sua janela de tolerância. |
| `X-Signature` | `sha256=<hex>` de `HMAC-SHA256(secret, X-Timestamp + "." + raw_body)`. Veja [Verificação de assinatura](https://docs.infraio.xyz/pt-BR/webhooks/signature-verification). |
| `X-Signature-Prev` | Mesmo algoritmo com o segredo **anterior**. Presente só na janela de 24 horas depois que você rotaciona — deixa verificadores rodando qualquer das duas chaves continuarem aceitando entregas durante o cutover. Depois da janela fechar o header para de ser enviado. |

## Cronograma de retry

Se o seu endpoint não retornar `2xx` dentro do timeout, a gente
retenta neste cronograma (timestamps relativos à primeira tentativa):

| Tentativa | Atraso | Acumulado |
| --- | --- | --- |
| 1 | 0s | 0s |
| 2 | +1 min | 1m |
| 3 | +5 min | 6m |
| 4 | +15 min | 21m |
| 5 | +1 hora | 1h 21m |
| 6 | +6 horas | 7h 21m |

Depois da tentativa 6 falhar, a entrega é marcada como **Failed** e o
e-mail da sua conta é notificado. Você pode reenviar eventos com falha
pelo painel **Developers → Webhooks → Delivery
history** do dashboard. Cada reenvio é uma nova entrega com o próprio
`X-Delivery`.

## Registrar um endpoint

No [dashboard do lojista](https://app.infraio.xyz):

1. **Developers → Webhooks** → **+ Add endpoint**
2. Cole a sua URL — só `https://…` (HTTP puro é rejeitado; o
   formulário de criação também bloqueia `localhost`, ranges de IP
   privados e URLs com userinfo)
3. Escolha os eventos para assinar (ou `*` para todos)
4. Escolha o ambiente — **test** ou **live** (cada um tem o próprio
   segredo; eles nunca se cruzam)
5. Salve → o dashboard mostra o segredo de assinatura (`whsec_…`)
   **uma vez**. Guarde do lado do servidor; você vai precisar para
   as próximas duas features.

Você pode registrar até **10 endpoints por ambiente por lojista**
(por exemplo, um para entrega em produção, um para espelhamento em
staging, um para um notificador no Slack). Cada um mantém o próprio
estado de retry e segredo.

## Ações de ciclo de vida em cada endpoint

O menu ⋮ em cada card de endpoint expõe:

- **Edit** — muda a URL, descrição ou lista de inscrições. A URL
  nova é re-validada com as mesmas regras `https://`/SSRF da
  criação.
- **Send Test** — faz POST síncrono de um envelope
  `webhook.test.ping` assinado com o seu segredo atual. O dashboard
  mostra o status HTTP, latência e um snippet de 512 bytes da sua
  resposta. Pings de teste não são retentados, então você recebe a resposta imediatamente.
- **Rotate Secret** — gera um segredo novo. O anterior permanece
  válido por **24 horas** (entregas levam tanto `X-Signature` quanto
  `X-Signature-Prev` durante a janela para que verificadores
  rodando qualquer das duas chaves continuem aceitando eventos
  enquanto você redeploya).
- **Reveal Secret** — re-exibe o segredo existente. Condicionado a
  verificação 2FA recente e registrado no log de auditoria; use só
  quando você perdeu a sua cópia e o Rotate não é aceitável.
- **Enable / Disable** — liga ou desliga o endpoint sem perder o histórico
  de entrega. Endpoints desabilitados ficam no dashboard mas não
  recebem novas entregas.
- **Delete** — permanente. Use Disable se você pode reabilitar
  depois.

## Dicas para handlers

1. **Retorne 2xx rápido.** Confirme com `200 OK` antes de fazer
   trabalho pesado — mova a entrega para um job em background.
   O timeout por tentativa é de **10 segundos**; segurar a resposta
   mais do que isso dispara um retry. O timeout é do lado da
   plataforma e não é configurável pelo lojista — contate o suporte
   se o seu handler genuinamente precisa de mais tempo.
2. **Deduplique em `X-Delivery`** (ou `Idempotency-Key` — mesmo
   valor). Mesmo que você retorne 2xx, um proxy upstream pode
   derrubar a conexão e disparar um retry; o ID da entrega é
   estável em toda retentativa da mesma linha de entrega, então é a
   chave certa.
3. **Tolere tipos de evento desconhecidos.** Eventos novos podem
   aparecer; retorne 200 e no-op no lugar de 4xx, ou essas entregas
   vão continuar sendo retentadas.
4. **Logue `X-Delivery` junto com a sua lógica de negócio.** Quando
   algo dá errado, essa é a chave de junção entre o nosso lado e o
   seu.

## Próximos passos

- [Verificação de assinatura](https://docs.infraio.xyz/pt-BR/webhooks/signature-verification)
  — o algoritmo exato + padrões de proteção contra replay.
- [Conceitos → Sessões](https://docs.infraio.xyz/pt-BR/concepts/sessions) — em que estado
  uma sessão está quando cada evento dispara.
