> ## Documentation Index
> Fetch the complete documentation index at: https://docs.beinfi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Como saber que você foi pago: webhook assinado em produção, polling em sandbox.

Cobrança é assíncrona: você cria a fatura, a pessoa paga minutos (ou dias) depois,
em outra aba, no app do banco. Existem dois jeitos de descobrir isso — e em
sandbox só um deles funciona.

## Em sandbox: polling

Registrar webhook com chave `sk_test_` responde:

```
503 secret_store_unavailable
"Webhook secrets cannot be stored right now. Please try again later."
```

Não é bug seu e não é intermitente: o cofre de segredos não está disponível pra
tenant de sandbox, então não existe segredo pra assinar entrega. Em sandbox, o
caminho é perguntar:

```ts theme={null}
// browser ou servidor — endpoint público, sem secret key
const pago = await infi.pay.waitForPaid({ slug, invoiceId });
// true = pagou, false = timeout (default: 3s de intervalo, 10min de teto)

// servidor, leitura pontual
const inv = await infi.invoices.get(invoiceId);
inv.status; // "open" | "paid" | …
```

`waitForPaid` aceita `intervalMs`, `timeoutMs`, `onTick` (pra atualizar contador na
tela) e `signal`. É o que você quer rodando enquanto o QR do Pix está na tela.

<Info>
  **O que funciona em sandbox.** `webhooks.list()` e `webhooks.listDeliveries()` respondem `200` (com lista vazia).
  Só o `create` — que precisa gravar segredo — é que para no `503`.
</Info>

## Em produção: registrar o endpoint

```ts theme={null}
const endpoint = await infi.webhooks.create({
  url: "https://seu-app.com/api/webhooks/infi",
  events: ["payment.confirmed", "invoice.finalized"],
});

endpoint.secret; // ⚠️ só aparece aqui. Guarde no seu secret manager.
```

O `secret` vem **uma vez**, na criação. Perdeu? `infi.webhooks.rotateSecret(id)`
emite outro, e a troca é imediata: as entregas seguintes já vêm assinadas com o
novo, e o anterior para de verificar na hora. Atualize o segredo no seu servidor
logo em seguida, ou as entregas desse intervalo falham na verificação (e são
reenviadas). Também existem `list`, `get`,
`patch(id, { isActive, events })`, `delete` e `listDeliveries()` pra auditar o que
saiu.

Se você usa <a href="/company-as-code">company as code</a>, o mesmo
endpoint pode ser declarado em `webhooks[]` no `infi.company.ts` — em sandbox o
`sync` vai bater no mesmo `503`.

## Os eventos

Nomes vêm do header `X-Webhook-Event-Type`, **não** do corpo:

| Evento                                                 | Quando                                                                  | `data`                                                                           |
| ------------------------------------------------------ | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| `payment.confirmed`                                    | Pagamento liquidado — é este que libera acesso                          | `paymentId`, `invoiceId`, `amount`, `currency`, `customerId?`, `payerId?`        |
| `payment.failed`                                       | Tentativa falhou                                                        | `paymentId`, `invoiceId`                                                         |
| `payment.refunded`                                     | Reembolso registrado                                                    | `paymentId`, `invoiceId`, `amount`, `currency`, `accessRevoked`                  |
| `payment.chargeback`                                   | Chargeback registrado                                                   | `paymentId`, `invoiceId`, `amount`, `currency`, `accessRevoked`                  |
| `invoice.finalized`                                    | Fatura fechada e cobrável                                               | `invoiceId`, `total`, `currency`                                                 |
| `invoice.sent`                                         | Fatura enviada por email                                                | `invoiceId`, `total`, `currency`                                                 |
| `invoice.paid`                                         | Fatura liquidada (sem `paymentId`: pode fechar em mais de um pagamento) | `invoiceId`, `amount`, `currency`, `customerId?`, `payerId?`                     |
| `invoice.voided`                                       | Fatura cancelada                                                        | `invoiceId`                                                                      |
| `invoice.uncollectible`                                | Desistiu de cobrar                                                      | `invoiceId`                                                                      |
| `invoice.auto_collection_failed`                       | Fatura saiu da cobrança automática                                      | `invoiceId`                                                                      |
| `checkout.session.created` / `.completed` / `.expired` | Sessão do checkout por link                                             | `sessionId`, `linkId`                                                            |
| `usage.threshold_reached`                              | Alerta de uso disparou                                                  | `subscriptionId`, `meterId?`, `thresholdAmount`                                  |
| `customer.created`                                     | Cliente criado (inclui quem pagou por link)                             | `customerId`, `externalId`, `createdAt`, `name?`, `email?`, `taxId?`, `country?` |

`customerId` é a **inscrição** (`ProductCustomer.id`) e vem em fatura de
assinatura; `payerId` é o cliente do tenant e vem em fatura avulsa. Um dos dois
está ausente conforme o caso — por isso os dois são opcionais.

O despacho casa por nome, sem allowlist: qualquer evento acima pode ser
assinado, e a lista cresce.

Corpo é JSON plano, decimais e uuids como string, campo opcional ausente (não
`null`).

## Verificar a assinatura

```ts theme={null}
// app/api/webhooks/infi/route.ts
import { verifyWebhook, InfiError } from "@beinfi/sdk";
import type { PaymentConfirmedData } from "@beinfi/sdk";

export async function POST(req: Request) {
  const body = await req.text(); // texto cru: JSON.parse quebra a assinatura

  try {
    const event = verifyWebhook<PaymentConfirmedData>(
      {
        id: req.headers.get("x-webhook-id")!,
        timestamp: req.headers.get("x-webhook-timestamp")!,
        signature: req.headers.get("x-webhook-signature")!,
        eventType: req.headers.get("x-webhook-event-type")!,
        body,
      },
      process.env.INFI_WEBHOOK_SECRET!,
    );

    if (event.type === "payment.confirmed") {
      // event.data.invoiceId → libere o acesso (idempotente!)
    }
    return new Response("ok");
  } catch (err) {
    if (err instanceof InfiError) return new Response(err.code, { status: 400 });
    throw err;
  }
}
```

`verifyWebhook` joga `InfiError` com `code`:

* `invalid_webhook_signature` — assinatura não bate (segredo errado, ou o corpo
  foi reserializado no caminho).
* `webhook_expired` — timestamp fora da janela de 5min (proteção de replay). Dá
  pra afrouxar passando o terceiro argumento em segundos.
* `invalid_webhook` — timestamp ou JSON inválido.

<Warning>
  **Não parseie antes de verificar.** A assinatura é sobre os **bytes exatos** do corpo. Qualquer framework que faça
  `JSON.parse` e reserialize antes de você verificar invalida tudo. Leia raw.
</Warning>

Fora de Node/TS, o esquema é simples: `HMAC-SHA256(secret, "{id}.{timestamp}.{body}")`
em hex, comparado em tempo constante com o header `X-Webhook-Signature` (que pode
vir prefixado, `v1=abc…`). Aceite qualquer uma das assinaturas separadas por
vírgula: hoje vem uma só, e o formato deixa espaço para mais.

## Entrega é "pelo menos uma vez"

Trate o handler como idempotente — e **a chave óbvia é a errada**. Deduplicar
por `event.id` deixa passar duas entregas distintas para a mesma fatura, que
continuam sendo uma compra só. Chaveie pela **fatura**:

```ts theme={null}
const chave = `invoice:${event.data.invoiceId}`;
if (await jaProcessado(chave)) return ok();
await marcarProcessado(chave);   // marque ANTES do efeito
await liberarAcesso(...);
```

Marque antes do efeito, não depois: uma falha no meio custa um efeito perdido,
que se recupera. A ordem inversa custa um efeito duplicado — num fluxo de
crédito, saldo de graça.

Reentrega depois de um `5xx` seu é
comportamento esperado, não anomalia.

<CardGroup cols={2}>
  <Card title="Testar no sandbox" href="/testar-no-sandbox">
    O que o sandbox é de verdade, e onde o loop fecha.
  </Card>

  <Card title="SDK" href="/sdk">
    Cliente, inscrição, medição de uso.
  </Card>
</CardGroup>
