# Crossmint Webhooks

[Crossmint](https://crossmint.com) 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 `POST`s. 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)

| Header | Value |
| --- | --- |
| `svix-id` | Identifies the message; retries reuse it |
| `svix-timestamp` | Unix seconds |
| `svix-signature` | One 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`](https://www.npmjs.com/package/svix) library (npm and PyPI):

```javascript
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](https://github.com/hookdeck/webhook-skills/blob/main/skills/crossmint-webhooks/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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/crossmint-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/crossmint-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/crossmint-webhooks/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:

| Product | Envelope |
| --- | --- |
| 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 }` |

```json
{ "id": "evt_1b2c3d", "type": "wallets.transfer.in",
  "data": { "status": "succeeded", "token": { "symbol": "USDC", "amount": "10", "decimals": 6 } } }

```

## Common Event Types

| Product | Events |
| --- | --- |
| Wallets | `wallets.transfer.in`, `wallets.transfer.out` (also sent for failed transfers), `wallets.signer.exported` |
| Checkout V2 | `purchase.succeeded` |
| Checkout V3 | `orders.quote.created`, `orders.quote.updated`, `orders.payment.succeeded`, `orders.payment.failed`, `orders.delivery.initiated`, `orders.delivery.completed`, `orders.delivery.failed` |
| NFT minting | `collections.create.succeeded`, `collections.update.succeeded`, `nfts.create.succeeded`, `nfts.create.failed`, `nfts.update.succeeded` |
| Credentials | `credential.creation.succeeded`, `credential.creation.failed` |
| Offramp | `payment_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](https://github.com/hookdeck/webhook-skills/blob/main/skills/crossmint-webhooks/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

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

```

## Local Development

```bash
# 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

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/crossmint-webhooks/references/overview.md) - Event catalog and payload shapes per product
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/crossmint-webhooks/references/setup.md) - Add the endpoint, find the signing secret, test
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/crossmint-webhooks/references/verification.md) - Library and manual verification, gotchas, debugging