Agent skill
MoonPay Webhooks Skill
Receive and verify MoonPay webhooks. Use when setting up MoonPay webhook endpoints, debugging Moonpay-Signature-V2 verification, or handling crypto on/off ramp events like transaction_created, transaction_updated, transaction_failed, sell_transaction_created, identity_check_updated, and customer.identity_status_updated.
Install this skill
npx skills add hookdeck/webhook-skills --skill moonpay-webhooks
When to Use This Skill
- How do I receive MoonPay webhooks?
- How do I verify the
Moonpay-Signature-V2header? - How do I handle
transaction_created,transaction_updated, ortransaction_failedevents? - How do I react to
sell_transaction_createdoridentity_check_updatedwebhooks? - How do I handle the Platform
customer.identity_webhook?status_ updated - Why is my MoonPay webhook signature verification failing?
Verification (core)
MoonPay sends a Moonpay-Signature-V2 header of the form t=<unix seconds>,s=<lowercase hex>. s is the HMAC-SHA256 hex of "<t>.<raw body>", keyed with the webhook API key (Developers page, e.g. wk_test_...) as a string. Not the legacy Moonpay-Signature. This covers POST webhooks only: GET callbacks sign the query string instead and are not covered here. Always sign over the raw body.
Timestamp window is opt-in. Hookdeck retries and replays resend the original timestamp, so a 5-minute window rejects late retries. Set MOONPAY_ to a positive number to enable it (default 0 = off). references/verification.md shows where the window can be dropped or widened.
const crypto = require('crypto');
// toleranceSeconds = 0 (default) skips the timestamp window; a positive value opts in
function verifyMoonpayWebhook(rawBody, header, apiKey, toleranceSeconds = 0) {
if (!header) return false;
const parts = header.split(',').map((p) => p.split('='));
const t = parts.find(([k]) => k === 't')?.[1];
const s = parts.find(([k]) => k === 's')?.[1];
if (!t || !/^\d+$/.test(t) || !s) return false;
if (toleranceSeconds > 0 && Math.abs(Date.now() / 1000 - Number(t)) > toleranceSeconds) return false; // opt-in
const expected = crypto.createHmac('sha256', apiKey).update(`${t}.`).update(rawBody).digest('hex');
try { return crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)); }
catch { return false; } // length mismatch
}
In Express read the body with express.raw({ type: '*/*' }) so any content type verifies. MoonPay has no verification SDK; verification is manual in every framework.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Common Event Types
Widget webhooks have a body of { "type", "data", "externalCustomerId"? }, or { "type", "body" } for virtual account events; the Platform KYC webhook is { "id", "type", "timestamp", "data" }. Route on type:
| Event | Triggered When | Common Use Cases |
|---|---|---|
transaction_created | A customer creates a Buy transaction | Record the order |
transaction_updated | A Buy transaction's status or details change | Track status |
transaction_failed | A Buy transaction fails | Notify the user |
sell_transaction_created | A customer creates a Sell transaction | Await deposit |
sell_transaction_updated | A Sell transaction changes | Track payout |
sell_transaction_failed | A Sell transaction fails | Refund handling |
sell_ | Price moved before the deposit arrived | Prompt for a new quote |
identity_check_updated | A customer's KYC status changes (widget) | Unlock limits |
customer.identity_ | A customer's KYC status transitions (Platform) | Sync KYC state |
More types (virtual_, ...) are listed in references/overview.md.
Environment Variables
MOONPAY_WEBHOOK_API_KEY=wk_test_xxxxx # Webhook API key from the Developers page
MOONPAY_TIMESTAMP_TOLERANCE_SECONDS=0 # Optional replay window; a positive value opts in
Local Development
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 moonpay --path /webhooks/moonpay
Reference Materials
- references/overview.md - MoonPay webhook concepts, events, delivery
- references/setup.md - Register an endpoint and get the webhook API key
- references/verification.md - Signature verification details and gotchas