# BlindPay Webhooks

[BlindPay](https://blindpay.com) is a stablecoin and fiat payins/payouts API. It pushes
customer, bank account, wallet, payin, payout, virtual account, transfer, limit-increase and
payable changes to endpoints you register per instance, delivered through
Svix, which implements Standard Webhooks. Every delivery is an HTTP
`POST` with a JSON body and three signature headers: `svix-id`, `svix-timestamp`, and
`svix-signature`. The body is **flat**: `webhook_event` holds the event name, followed by the
resource's own fields (no wrapper object).

## When to Use This Skill

* How do I receive BlindPay webhooks?
* How do I verify BlindPay webhook signatures (`svix-signature`)?
* How do I handle `payin.complete`, `payout.complete` or `customer.update` events?
* How do I detect a stablecoin deposit (`wallet.inbound`) or a finished transfer (`transfer.complete`)?
* Why is my BlindPay webhook signature verification failing, or rejecting Hookdeck retries?

## Verification (core)

Signed content is `{svix-id}.{svix-timestamp}.{raw body}`. Svix signs it with HMAC-SHA256 using
the base64-decoded secret (the part after `whsec_`), base64-encodes the result, and sends one or
more space-separated `v1,<signature>` entries. Use the official `svix` library, which compares in
constant time. Read the `svix-*` headers, and when `svix-id` is absent fall back to the `webhook-*`
headers (`webhook-id`, `webhook-timestamp`, `webhook-signature`), as Hookdeck does.

```javascript
import { Webhook } from 'svix'; // svix 2.x is ESM-only

const prefix = req.headers['svix-id'] ? 'svix' : 'webhook';
const id = req.headers[`${prefix}-id`];
const timestamp = req.headers[`${prefix}-timestamp`];
const signature = req.headers[`${prefix}-signature`];
// rawBody: the raw Buffer/string, never re-serialized JSON
new Webhook(process.env.BLINDPAY_WEBHOOK_SECRET).verify(rawBody, {
  'svix-id': id, 'svix-timestamp': timestamp, 'svix-signature': signature,
  'webhook-id': id, 'webhook-timestamp': timestamp, 'webhook-signature': signature,
}); // throws on a bad signature or a timestamp outside the window
const event = JSON.parse(rawBody.toString('utf8')); // parse only after verify passes

```

```python
from svix.webhooks import Webhook, WebhookVerificationError

wh = Webhook(os.environ["BLINDPAY_WEBHOOK_SECRET"])  # "whsec_..."
wh.verify(raw_body, {"svix-id": id, "svix-timestamp": ts, "svix-signature": sig,
                     "webhook-id": id, "webhook-timestamp": ts, "webhook-signature": sig})

```

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/blindpay-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/blindpay-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/blindpay-webhooks/examples/fastapi/).

The signing secret has the form `whsec_<base64>`. Fetch it with
`GET /v1/instances/{instance}/webhook-endpoints/{we_id}/secret` (returns `{ "key": "whsec_..." }`)
or from the dashboard (instance > **Webhooks** > the endpoint's menu > **Get secret**). Keep the
full `whsec_...` value in `BLINDPAY_WEBHOOK_SECRET`. Handlers fail closed: with the variable
unset they return 500 instead of accepting anything. BlindPay stresses using the exact raw body:
"Never modify the request body before verification".

### Hookdeck retries and replays hit the 5-minute window

The `svix` library always enforces a fixed 5-minute timestamp window, and it can't be configured.
Hookdeck retries and replays resend the **original** `svix-timestamp`, so the library rejects any
retry or replay that happens more than 5 minutes after the original delivery. If you verify
behind Hookdeck (or replay old events), use the manual verifier in
[references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/blindpay-webhooks/references/verification.md), where the window can be dropped or
widened. Deduplicate on `svix-id` (stable across retries) instead of relying on the timestamp.

## Event Types

The handlers route on the top-level `webhook_event` field. BlindPay documents 26 events (the
list may not be exhaustive, so acknowledge unknown events with `200`):

| Resource | Events |
| --- | --- |
| Customer | `customer.new`, `customer.update`, `customer.delete` |
| Bank account | `bankAccount.new` |
| Blockchain wallet | `blockchainWallet.new`, `blockchainWallet.update` |
| Terms of service | `tos.accept` |
| Payin | `payin.new`, `payin.update`, `payin.complete` |
| Payout | `payout.new`, `payout.update`, `payout.complete` |
| Virtual account | `virtualAccount.new`, `virtualAccount.complete` |
| Transfer | `transfer.new`, `transfer.update`, `transfer.complete` |
| Managed wallet | `wallet.new`, `wallet.update`, `wallet.inbound` |
| Limit increase | `limitIncrease.new`, `limitIncrease.update` |
| Payable | `payable.new`, `payable.update`, `payable.complete` |

`payin.complete` fires when a payin finishes in any way (sent, refunded or failed), while a
failed payout fires `payout.update`, not `payout.complete`. Check `status` on the payload. See
[references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/blindpay-webhooks/references/overview.md) for when each event fires and the payload shape.

## Environment Variables

```bash
# Signing secret for the webhook endpoint (Svix format, starts with whsec_)
BLINDPAY_WEBHOOK_SECRET=whsec_xxxxx

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 blindpay --path /webhooks/blindpay

```

Register the tunnel URL as a webhook endpoint on your instance (BlindPay requires `https`) and
copy the signing secret into `.env`. See [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/blindpay-webhooks/references/setup.md).

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/blindpay-webhooks/references/overview.md) - BlindPay webhook concepts, events, payload, delivery rules
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/blindpay-webhooks/references/setup.md) - Register an endpoint (API or dashboard), get the signing secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/blindpay-webhooks/references/verification.md) - Svix verification details, header fallback, manual verifier, timestamp window