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

# Verify webhook signatures

> Use HMAC-SHA256 and the X-Minisend-Signature header to confirm webhook requests came from Minisend. Includes examples in JavaScript, Python, and PHP.

Each request includes `X-Minisend-Signature`: an HMAC-SHA256 hex of the raw JSON body, keyed with your `webhook_secret`. Verify it before processing.

<Note>
  Find your `webhook_secret` in **Settings**. Treat it like a password.
</Note>

<Warning>
  Use a timing-safe comparison. `===` is vulnerable to timing attacks.
</Warning>

## Sign over the raw body

The signature covers the **exact bytes Minisend sent**. Re-stringifying the parsed body (`JSON.stringify(req.body)`) can produce different bytes and break verification.

Capture the raw body before parsing. In Express, use `express.raw({ type: 'application/json' })` for the webhook route.

## Verify

<CodeGroup>
  ```javascript JavaScript theme={null}
  const crypto = require('crypto');

  function verifyWebhook(rawBody, signature, secret) {
    const expected = crypto
      .createHmac('sha256', secret)
      .update(rawBody, 'utf8')
      .digest('hex');

    const a = Buffer.from(signature, 'hex');
    const b = Buffer.from(expected, 'hex');

    if (a.length !== b.length) return false;
    return crypto.timingSafeEqual(a, b);
  }
  ```

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

  def verify_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
      expected = hmac.new(
          secret.encode('utf-8'),
          raw_body,
          hashlib.sha256,
      ).hexdigest()
      return hmac.compare_digest(signature, expected)
  ```

  ```php PHP theme={null}
  <?php
  function verifyWebhook(string $rawBody, string $signature, string $secret): bool {
      $expected = hash_hmac('sha256', $rawBody, $secret);
      return hash_equals($expected, $signature);
  }
  ```
</CodeGroup>

## Full Express handler

```javascript theme={null}
const express = require('express');
const crypto = require('crypto');
const app = express();

// Raw body required for signature verification
app.use(
  '/webhooks/minisend',
  express.raw({ type: 'application/json' }),
);

function verifyWebhook(rawBody, signature, secret) {
  const expected = crypto.createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex');
  const a = Buffer.from(signature, 'hex');
  const b = Buffer.from(expected, 'hex');
  if (a.length !== b.length) return false;
  return crypto.timingSafeEqual(a, b);
}

app.post('/webhooks/minisend', (req, res) => {
  const signature = req.headers['x-minisend-signature'];
  if (!signature) return res.status(401).json({ error: 'Missing signature' });

  if (!verifyWebhook(req.body, signature, process.env.MINISEND_WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const payload = JSON.parse(req.body.toString('utf8'));

  // Acknowledge immediately, then process
  res.status(200).send('OK');

  switch (payload.event) {
    case 'checkout.completed':
      markOrderPaid(payload.external_id, {
        session_id: payload.session_id,
        receipt: payload.receipt,
        amount_local: payload.amount_local,
        currency: payload.currency,
      });
      break;
    case 'checkout.failed':
      handleFailedPayment(payload.external_id, payload.session_id);
      break;
    case 'checkout.expired':
      handleExpiredSession(payload.external_id, payload.session_id);
      break;
  }
});

app.listen(3000);
```

<Tip>
  Return `2xx` before running business logic. Anything taking >10s triggers a retry.
</Tip>
