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

# Webhooks

> Notificações assinadas + verificação

Registre uma URL HTTPS e o SwytchPay faz `POST` nela quando um evento ocorre.

## Registrar

`POST /v1/webhooks`

```json theme={null}
{ "name": "minha corretora", "url": "https://suacorretora.com/webhooks/swytch", "events": ["deposit.paid", "payout.paid"] }
```

A resposta traz o `secret` (`whsec_…`) **uma única vez** — use-o para validar a assinatura.
Omita `events` para receber todos.

## Eventos

**Depósitos** — `deposit.pending` · `deposit.paid` · `deposit.expired`
**Saques** — `payout.pending` · `payout.processing` · `payout.paid` · `payout.failed` · `payout.canceled`
**MED (chargeback)** — `med.opened` · `med.under_analysis` · `med.won` · `med.lost`

```json theme={null}
{
  "event": "deposit.paid",
  "created_at": 1781319150,
  "environment": "test",
  "data": { "id": "dep_…", "external_id": "pedido-42", "status": "PAID", "amount_cents": 10000, "net_cents": 9650, "e2e_id": "E…" }
}
```

## Verificar a assinatura

Cada entrega traz dois headers:

```
X-Swytch-Timestamp: 1781319150
X-Swytch-Signature: sha256=<hex>
```

A assinatura é `HMAC-SHA256(secret, "{timestamp}.{corpo cru}")`. Valide sobre o **corpo cru**
(não re-serialize o JSON) e rejeite timestamps com mais de 5 minutos (anti-replay):

```js theme={null}
import { createHmac, timingSafeEqual } from "node:crypto"

function verify(rawBody, headers, secret) {
  const ts = Number(headers["x-swytch-timestamp"])
  if (Math.abs(Date.now() / 1000 - ts) > 300) return false
  const sig = (headers["x-swytch-signature"] || "").replace("sha256=", "")
  const expected = createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex")
  const a = Buffer.from(expected), b = Buffer.from(sig)
  return a.length === b.length && timingSafeEqual(a, b)
}
```

## Entrega e idempotência

* Responda `2xx` em até 7s. Falhas são re-tentadas (2 tentativas imediatas + redelivery por até 24h).
* O mesmo evento pode chegar mais de uma vez — implemente idempotência por `data.id` + `event`.
* `GET /v1/webhooks/:id/deliveries` mostra o log de entregas (status, latência, tentativas).
* `POST /v1/webhooks/:id/test` dispara um evento de exemplo assinado.

## Formato compatível (ZyroPay)

O webhook tem **dois formatos**, escolhidos por **como você registra**:

* **Nativo** (acima) — registrado por `POST /v1/webhooks`. Payload `{ event, data }` + assinatura HMAC.
* **Compat ZyroPay** — registrado por `PUT /compat/zyropay/cli/client/update/notification-url`
  (veja [Integração de corretora](/reference/compat-zyropay)). O payload sai no **formato do conector**
  (`status: CONFIRMED`, `type: PixIn`/`PixOut`, `externalId`, `paymentId`, `movId`, `amount`,
  `securityParaphrase`) — a plataforma que já consumia a ZyroPay recebe **sem mudar nada**.

Mapeamento no modo compat (o Zyro só notifica estados **terminais** — `status ∈ PENDING|CONFIRMED|EXPIRED|FAILED`):

```
deposit.paid       → { status: CONFIRMED, type: PixIn }
deposit.expired    → { status: EXPIRED,   type: PixIn }
deposit.canceled   → { status: FAILED,    type: PixIn }
payout.paid        → { status: CONFIRMED, type: PixOut }
payout.failed      → { status: FAILED,    type: PixOut }
payout.canceled    → { status: FAILED,    type: PixOut }
```

Eventos de andamento (`deposit.pending`, `payout.pending`, `payout.processing`) e as categorias MED/conversão/
estorno **não são enviados no modo compat** — são exclusivos do formato nativo (mais rico). No compat, só chegam
os estados terminais que o conector ZyroPay espera.
