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

# Verify webhooks

> Check that a webhook really came from Bridge.

Anyone who knows your webhook URL could send it a fake message. To protect you, we **sign** every webhook with your endpoint's secret. You check the signature, and ignore any message where it doesn't match.

## How the signature works

1. We take the raw request body, exactly as sent.
2. We create an HMAC-SHA256 of it using your endpoint's signing secret.
3. We put the result, as lowercase hex, in the `X-Webhook-Signature` header.

You do the same calculation and compare.

> [!WARNING]
>
> Use the **raw** body, before your framework turns it into JSON. If the body is parsed and re-encoded, even a changed space breaks the signature.

## Code examples

Get your signing secret from the Bridge app: open your endpoint under **Webhooks**.

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

const app = express();
const SECRET = process.env.BRIDGE_WEBHOOK_SECRET;

// Keep the raw body so the signature can be checked
app.post("/webhooks/bridge", express.raw({ type: "application/json" }), (req, res) => {
  const received = req.get("X-Webhook-Signature") ?? "";
  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(req.body)
    .digest("hex");

  const valid =
    received.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(received, "hex"), Buffer.from(expected, "hex"));

  if (!valid) return res.status(401).send("Invalid signature");

  const event = JSON.parse(req.body.toString("utf8"));
  // Save the event, then reply quickly
  res.sendStatus(200);
});
```

```python Python (Flask) theme={null}
import hashlib
import hmac
import os

from flask import Flask, request, abort

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

@app.post("/webhooks/bridge")
def bridge_webhook():
    received = request.headers.get("X-Webhook-Signature", "")
    expected = hmac.new(SECRET, request.get_data(), hashlib.sha256).hexdigest()

    if not hmac.compare_digest(received, expected):
        abort(401)

    event = request.get_json()
    # Save the event, then reply quickly
    return "", 200
```

```php PHP theme={null}
<?php
$secret = getenv('BRIDGE_WEBHOOK_SECRET');
$body = file_get_contents('php://input');
$received = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $body, $secret);

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

$event = json_decode($body, true);
// Save the event, then reply quickly
http_response_code(200);
```

> [!TIP]
>
> Compare signatures with a constant-time function, as in the examples above (`timingSafeEqual`, `compare_digest`, `hash_equals`). A normal `==` comparison can leak information to an attacker.

## If the signature doesn't match

* Check you're using the secret for **this** endpoint, and for the right environment (sandbox or live).
* Check you're using the raw body.
* If you recently reset the secret, update your server with the new one.

Reply with `401` and don't process the message.


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