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

# Verify webhooks

> Check the signature header before trusting a webhook.

Your webhook URL is public, so anyone could send it a fake "payment successful" event. Check every request before you act on it.

Every webhook from Tanda carries an `X-Webhook-Signature` header. It must match the webhook secret you generated in the dashboard.

## Set up

1. In the [dashboard](https://app.tandapay.io), go to **Developers → API keys** and generate a webhook secret.
2. Store it as an environment variable, for example `TANDA_WEBHOOK_SECRET`. Keep it out of source control.
3. In your handler, read `X-Webhook-Signature` and compare it with the secret using a constant-time comparison.
4. If they don't match, return `401` and do nothing else.

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

function isValidSignature(req) {
  const received = Buffer.from(req.get("X-Webhook-Signature") ?? "");
  const expected = Buffer.from(process.env.TANDA_WEBHOOK_SECRET);
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

app.post("/webhooks/tanda", express.json(), (req, res) => {
  if (!isValidSignature(req)) return res.sendStatus(401);
  queue.add("tanda-event", req.body);
  res.sendStatus(200);
});
```

```python Python (Flask) theme={null}
import hmac, os
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ["TANDA_WEBHOOK_SECRET"]

@app.post("/webhooks/tanda")
def tanda_webhook():
    received = request.headers.get("X-Webhook-Signature", "")
    if not hmac.compare_digest(received, SECRET):
        abort(401)
    enqueue(request.get_json())
    return "", 200
```

```php PHP theme={null}
<?php
$secret   = getenv('TANDA_WEBHOOK_SECRET');
$received = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';

if (!hash_equals($secret, $received)) {
    http_response_code(401);
    exit;
}

enqueue(json_decode(file_get_contents('php://input'), true));
http_response_code(200);
```

> [!NOTE]
>
> Use a constant-time comparison (`timingSafeEqual`, `compare_digest`, `hash_equals`) rather than `==`. A normal string comparison can leak the secret through response timing.

## Good practice

* Verify every request, in sandbox too, so the code path is tested before you go live.
* Accept webhooks over HTTPS only.
* Rotate the secret periodically, and immediately if you think it has leaked.
* Even with a valid signature, confirm high-value outcomes with [Get transaction](https://api.tandapay.io/api-reference/endpoint/get-transaction.md).


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