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

# Webhooks

> HMAC-signed payment events pushed to your server

## Add an endpoint

Dashboard → **Webhooks** → add your HTTPS URL and choose events (start with
`payment.confirmed`). You'll get a signing secret (`whsec_…`).

## Payload

```json theme={null}
POST https://yourserver.com/webhooks/puffin
X-Puffin-Signature: 3f5a…   // HMAC-SHA256 of the raw body

{
  "event": "payment.confirmed",
  "data": {
    "paymentIntentId": "9b1f…",
    "amount": "25.00",
    "token": "USDC_SOL",
    "txHash": "5j8K…"
  },
  "timestamp": "2026-07-04T12:00:00.000Z"
}
```

## Verify the signature

Always verify before trusting anything:

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

function verify(rawBody, signature, secret) {
  const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
  return expected.length === signature.length &&
    timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
```

## Events

| Event                                                                             | Fires when                                                                                                         |
| --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `payment.confirmed` / `payment.confirmed_late`                                    | A payment intent's received amount covers its target                                                               |
| `payment.dispute_opened`                                                          | A payer reports sending funds that didn't match an intent                                                          |
| `payment.late_returned` / `payment.late_recovery_queued` / `payment.late_expired` | Late-payment handling per your `latePaymentPolicy`                                                                 |
| `wallet.deposit.detected` / `wallet.deposit.confirmed`                            | A deposit lands on a [WaaS wallet](/wallets) you created via the API — distinct from the payment-intent flow above |
| `bridge.completed` / `bridge.failed`                                              | A [bridge transfer](/bridging) reaches a terminal state                                                            |

`wallet.*` and `bridge.*` payloads include `walletId`/`bridgeTransferId`,
`chain`/`route`, `amount`, and the relevant transaction hash(es) — shaped
the same way as their API responses in [Wallets](/api/wallets) and
[Bridge](/api/bridge).

## Delivery & retries

* Respond `2xx` within 10 seconds; anything else counts as a failure.
* We retry up to **3 times** with backoff; deliveries and status codes are
  visible in the dashboard.
* Handlers must be **idempotent** — key on `paymentIntentId`.
