Agent skill

Crossmint Webhooks Skill

Receive and verify Crossmint webhooks, which are delivered by Svix. Use when setting up a Crossmint webhook endpoint, getting the whsec_ signing secret, debugging svix-id / svix-timestamp / svix-signature verification, or handling wallet, checkout, NFT minting, credential and offramp events like wallets.transfer.in, orders.delivery.completed, nfts.create.succeeded and purchase.succeeded.

Install this skill

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


Crossmint is a wallets, checkout, NFT-minting, verifiable-credential and stablecoin offramp platform. One project-level webhook system carries events from every product: you add an endpoint in the Crossmint console and receive signed JSON POSTs. Deliveries are sent through Svix, so they carry svix-id, svix-timestamp and svix-signature headers and the endpoint's signing secret starts with whsec_.

When to Use This Skill

  • How do I receive Crossmint webhooks?
  • How do I verify a Crossmint webhook signature (svix-signature)?
  • Where do I find my Crossmint webhook signing secret (whsec_...)?
  • How do I handle wallets.transfer.in, orders.delivery.completed or nfts.create.succeeded events?
  • Why do Crossmint webhook bodies have different shapes (id vs actionId)?
  • Why is my Crossmint webhook verification failing?

Verification (core)

HeaderValue
svix-idIdentifies the message; retries reuse it
svix-timestampUnix seconds
svix-signatureOne or more space-separated v1,<base64> entries

Signed content is {svix-id}.{svix-timestamp}.{raw body}, HMAC-SHA256, base64 output. The key is the base64-decoded part of the whsec_... secret after the prefix. Compare against every v1, entry; ignore other versions. If svix-id is absent, read webhook-id, webhook-timestamp and webhook-signature instead, as Hookdeck does. The examples use the svix library (npm and PyPI):

import { Webhook } from 'svix';

// rawBody: exact request body (Buffer or string), NOT re-serialised JSON
function verifyCrossmint(rawBody, headers, secret = process.env.CROSSMINT_WEBHOOK_SECRET) {
  const p = headers['svix-id'] ? 'svix' : 'webhook'; // fall back to the Standard Webhooks names
  const [id, timestamp, signature] = ['id', 'timestamp', 'signature'].map((k) => headers[`${p}-${k}`]);
  new 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 stale timestamp → respond 400
}

The library always enforces a fixed 5-minute timestamp window. Hookdeck retries and replays resend the original svix-timestamp, so the library rejects them once they are more than 5 minutes old. If you receive through Hookdeck and need late retries to pass, use the manual verifier in references/verification.md, which can drop or widen the window. In Express, read the body with express.raw({ type: '*/*' }) so any content type verifies, and parse JSON only after the check.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Event Type and Payload

The event name is the top-level type in every product's body. Only type and data are common; the envelope around them differs:

ProductEnvelope
Wallets{ id: "evt_...", type, data }
Checkout V3{ actionId, type, data }
Checkout V2 (purchase.succeeded)Flat: { type, status, walletAddress, txId, ... }, no data
NFT minting{ actionId, startedAt, completedAt, type, data, resource }
Verifiable credentials{ type, data } (data.credential, data.error)
Offramp{ id: "pm-status-...", type, data, timestamp }
{ "id": "evt_1b2c3d", "type": "wallets.transfer.in",
  "data": { "status": "succeeded", "token": { "symbol": "USDC", "amount": "10", "decimals": 6 } } }

Common Event Types

ProductEvents
Walletswallets.transfer.in, wallets.transfer.out (also sent for failed transfers), wallets.signer.exported
Checkout V2purchase.succeeded
Checkout V3orders.quote.created, orders.quote.updated, orders.payment.succeeded, orders.payment.failed, orders.delivery.initiated, orders.delivery.completed, orders.delivery.failed
NFT mintingcollections.create.succeeded, collections.update.succeeded, nfts.create.succeeded, nfts.create.failed, nfts.update.succeeded
Credentialscredential.creation.succeeded, credential.creation.failed
Offramppayment_methods.status.updated

These are the 19 documented types; the list may not be exhaustive, so ignore types you don't recognise. See references/overview.md.

Delivery Semantics

Return a 2xx within 15 seconds. Crossmint makes 8 attempts (immediately, then after 5 s, 5 min, 30 min, 2 h, 5 h, 10 h and 10 h), after which the message is marked failed and can be resent from the console. Delivery is at-least-once and offramp events can arrive out of order, so process asynchronously, deduplicate on svix-id (stable across retries) and compare timestamps before overwriting state. Return 400 for missing headers or a bad signature.

Environment Variables

CROSSMINT_WEBHOOK_SECRET=whsec_xxxxx   # full value from the endpoint's Signing Secret in the console; handlers fail closed (500) without it

Local Development

# Express / Next.js (use 8000 for FastAPI). No install or account required.
npx hookdeck-cli listen 3000 crossmint --path /webhooks/crossmint

Add the printed URL as an endpoint in the Crossmint console (Webhooks > Add Endpoint).

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 12, 2026

View on GitHub →