> For the complete documentation index, see [llms.txt](https://docs-sdk.crypto-chief.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs-sdk.crypto-chief.com/processing/js/guides/webhooks.md).

# Webhooks

Verify and handle Crypto Chief webhooks in Node.js with typed event payloads.

Outbound webhooks are signed with the **same algorithm** as outgoing requests, so the same API key verifies them. The verification re-canonicalizes the body, so it tolerates key-order drift — but you must pass the **raw** request body, not a re-serialized object.

## Express

Mount `express.raw` so the body reaches you untouched, then verify and parse in one step:

```ts
import express from 'express';
import { parseWebhookEvent, WebhookSignatureError, type PayoutWebhookEvent } from '@cryptochiefs/cryptochief-crypto-processing-node';

app.post('/webhook/payout', express.raw({ type: '*/*' }), (req, res) => {
  try {
    const evt = parseWebhookEvent<PayoutWebhookEvent>(apiKey, req.body, req.header('Signature'));
    console.log(`payout ${evt.uuid} → ${evt.status}`);
    res.sendStatus(200);
  } catch (err) {
    if (err instanceof WebhookSignatureError) return res.sendStatus(401);
    throw err;
  }
});
```

## Plain Node `http`

`createWebhookHandler` reads the raw body, verifies the signature, parses the typed event, and replies `200` for you:

```ts
import { createServer } from 'node:http';
import { createWebhookHandler, type WebhookEvent } from '@cryptochiefs/cryptochief-crypto-processing-node';

const handler = createWebhookHandler<WebhookEvent>(apiKey, (evt, { res }) => {
  console.log(evt.event); // e.g. "payout.paid"
  res.writeHead(200).end('ok');
});

createServer((req, res) => {
  if (req.method === 'POST' && req.url === '/webhook') return handler(req, res);
  res.writeHead(404).end();
}).listen(3000);
```

## Manual verification

For any other stack, verify the raw bytes yourself (constant-time, returns a boolean):

```ts
import { verifyWebhookSignature } from '@cryptochiefs/cryptochief-crypto-processing-node';

if (!verifyWebhookSignature(apiKey, rawBody, signatureHeader)) {
  // reject with 401
}
```

## Event types

Typed payloads: `PayoutWebhookEvent`, `TransactionWebhookEvent`, `PayInWebhookEvent`, `StaticDepositWebhookEvent`, `SweepWebhookEvent`. Event-name prefixes are `payout.*`, `transaction.*`, `invoice.*` (pay-ins), `static_deposit.*`, and `sweep.*`. Payout and transaction webhooks fire **only on terminal status**.

`sweep.confirmed` is the moment swept funds are confirmed in your master wallet — the other half of a deposit's life, and what treasury reporting should key off. See [Sweep callbacks](/processing/js/guides/sweep-callbacks.md).

{% hint style="info" %}
`WEBHOOK_SENDER_IPS` lists the addresses webhooks are delivered from — whitelist them at your edge for defence in depth.
{% endhint %}

## Empty string or absent

"This field has no value" is not spelled the same way on every payload, and in TypeScript the two spellings land on different sides of a `??` — so test for the one the payload actually uses.

On a **static-deposit** event, `contract` is an **empty string** for a native coin transfer, not an absent key:

```ts
import { parseWebhookEvent, type StaticDepositWebhookEvent } from '@cryptochiefs/cryptochief-crypto-processing-node';

const evt = parseWebhookEvent<StaticDepositWebhookEvent>(apiKey, rawBody, signatureHeader);

const isNative = !evt.contract; // '' on a native coin, a contract address on a token
if (evt.status === 'paid' && evt.paidAt) {
  // credited - confirmedAt and paidAt are absent until the deposit reaches those points
}
```

`amountFiat` may be empty when no conversion rate was available, and `blockNumber` appears only once the transaction is in a block.

On a **sweep** event the convention is reversed: optional fields are omitted rather than emptied, so `assetContract` is `undefined` on a native sweep. Sweep *history*, a plain API response rather than a webhook, goes back to the empty string. [Sweep callbacks](/processing/js/guides/sweep-callbacks.md) and [Auto-sweep settings](/processing/js/guides/auto-sweep-settings.md) each spell out their own side.

Wallet responses are the strict case: `masterWalletAddress`, `callbackUrl` and `label` are always present and `null` when unset, never `''` — see [Wallets](/processing/js/guides/wallets.md#what-the-three-return).
