Agent skill

BlindPay Webhooks Skill

Receive and verify BlindPay webhooks (stablecoin and fiat payins/payouts API, delivered via Svix). Use when setting up BlindPay webhook handlers, debugging svix-signature verification, or handling events like payin.complete, payout.complete, customer.update, transfer.complete and wallet.inbound.

Install this skill

npx skills add hookdeck/webhook-skills --skill blindpay-webhooks


BlindPay 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.

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
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/, examples/nextjs/, 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, 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):

ResourceEvents
Customercustomer.new, customer.update, customer.delete
Bank accountbankAccount.new
Blockchain walletblockchainWallet.new, blockchainWallet.update
Terms of servicetos.accept
Payinpayin.new, payin.update, payin.complete
Payoutpayout.new, payout.update, payout.complete
Virtual accountvirtualAccount.new, virtualAccount.complete
Transfertransfer.new, transfer.update, transfer.complete
Managed walletwallet.new, wallet.update, wallet.inbound
Limit increaselimitIncrease.new, limitIncrease.update
Payablepayable.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 for when each event fires and the payload shape.

Environment Variables

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

Local Development

# 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.

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 11, 2026

View on GitHub →