> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stablemesh.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive signed events when your cards, payments, withdrawals and deposits change.

Webhooks notify your server when something changes, so you don't have to poll.

***

## Set up an endpoint

Each account has one endpoint. Register it with `webhook/set`:

```bash theme={null}
curl -X POST https://api.stablemesh.io/v1/webhook/set \
  -H "X-API-KEY: smk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/stablemesh/webhooks"}'
```

The first response includes `secret`, which starts with `whsec_`. **Store it now**, because it isn't shown again. Use `webhook/rotateSecret` to replace it.

The URL must be `https://`, and private or loopback addresses are refused. Pause delivery with `{"enabled": false}`.

***

## Delivery

Each event is a `POST` with a JSON body:

```json theme={null}
{
  "id": "evt_Zb3kQ...",
  "type": "card.activated",
  "created": 1791100800,
  "data": {
    "cardId": "2026100117908000004821",
    "binId": "VISA-WSB-49372410",
    "status": "ACTIVE",
    "last4": "4242"
  }
}
```

The request has these headers:

| Header | Value |
| - | - |
| `StableMesh-Signature` | `t=<unix seconds>,v1=<hex HMAC-SHA256>` |
| `StableMesh-Event-Id` | The event `id` |
| `StableMesh-Event-Type` | The event `type` |
| `User-Agent` | `StableMesh-Webhooks/1` |

Reply with any `2xx` within 10 seconds. Anything else, including a timeout, is retried after about 1 minute, 5 minutes, 30 minutes, 2 hours and 5 hours. After 6 failed attempts the event is dropped.

An event can arrive more than once, and events can arrive out of order. Use `id` to drop duplicates, and re-read the object with the API if the order matters.

***

## Verify the signature

The signature is an HMAC-SHA256 of `<t>.<raw body>`, keyed with your endpoint secret. Always verify it on the **raw** request body, before parsing it.

<CodeGroup>
  ```javascript Node.js theme={null}
  import crypto from "node:crypto";

  function verify(rawBody, header, secret, toleranceSec = 300) {
    const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
    const t = Number(parts.t);
    if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
    const expected = crypto.createHmac("sha256", secret)
      .update(`${t}.${rawBody}`).digest("hex");
    const a = Buffer.from(expected), b = Buffer.from(parts.v1 || "");
    return a.length === b.length && crypto.timingSafeEqual(a, b);
  }
  ```

  ```python Python theme={null}
  import hashlib, hmac, time

  def verify(raw_body: bytes, header: str, secret: str, tolerance_sec: int = 300) -> bool:
      parts = dict(p.split("=", 1) for p in header.split(","))
      t = int(parts.get("t", "0"))
      if not t or abs(time.time() - t) > tolerance_sec:
          return False
      expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
      return hmac.compare_digest(expected, parts.get("v1", ""))
  ```
</CodeGroup>

The timestamp check, five minutes in these examples, protects you from replayed requests.

***

## Events

| Type | When | `data` |
| - | - | - |
| `card.created` | A card was issued | `cardId`, `binId`, `status`, `last4` |
| `card.activated` | The card is `ACTIVE` and `last4` is known | `cardId`, `binId`, `status`, `last4` |
| `card.frozen` | The card was frozen | `cardId`, `binId`, `status`, `last4` |
| `card.unfrozen` | The card was unfrozen | `cardId`, `binId`, `status`, `last4` |
| `card.cancelled` | The cancellation completed | `cardId`, `status`, `returnedAmount`, `currency` |
| `card.withdrawal.completed` | A card withdrawal reached your USD account | A [withdrawal object](/api-reference/cards/get-withdrawal-status) |
| `card.withdrawal.failed` | A card withdrawal failed. The hold was released | A withdrawal object |
| `transaction.created` | A new card payment | A [transaction object](/api-reference/cards/list-card-transactions) |
| `transaction.updated` | A payment changed status | A transaction object, plus `previousStatus` |
| `deposit.credited` | A deposit was credited to your USD account | `amount`, `currency` (`USD`), `asset`, `network`, `txHash` |

Objects in webhook payloads have the same fields as the same objects in API responses.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.