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

# Signature and security

> Verify the HMAC-SHA256 signature in the X-Signature header so you only accept payloads actually sent by Exoid.

Your webhook endpoint is public: anyone who knows the URL can send you forged requests. Exoid signs every request with HMAC-SHA256 and you have to verify the signature before trusting the content.

## How Exoid signs requests

* Exoid signs the **unparsed request body** (the raw body, byte for byte) with HMAC-SHA256, using the campaign's Webhook Secret.
* The signature travels in the `X-Signature` header, in the format `sha256=<hex>`.
* Your endpoint has to recreate the signature and compare it with the one received. If they don't match, reject the request.

## Verification procedure

<Steps>
  <Step title="Read the raw body">
    Receive the body as an unparsed Buffer, without applying any JSON middleware.
  </Step>

  <Step title="Extract the header">
    Take the value of `X-Signature` from the incoming request.
  </Step>

  <Step title="Recompute the HMAC">
    Compute `HMAC-SHA256` of the raw body with the campaign's Webhook Secret and format the result as `sha256=<hex>`.
  </Step>

  <Step title="Compare">
    If the computed signature doesn't match the one received, answer `401` and stop processing.
  </Step>

  <Step title="Process the payload">
    Only at this point run `JSON.parse` on the body and use the data.
  </Step>
</Steps>

## Node.js example (Express)

```js title="verify-signature.js" theme={"dark"}
const crypto = require("crypto");
const express = require("express");
const app = express();

// Use express.raw to access the unparsed body and verify the signature
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const sig = req.headers["x-signature"];
  const expected =
    "sha256=" +
    crypto
      .createHmac("sha256", process.env.WEBHOOK_SECRET)
      .update(req.body) // raw Buffer
      .digest("hex");

  if (sig !== expected) return res.status(401).send("Invalid signature");

  const payload = JSON.parse(req.body);
  console.log("Webhook received:", payload);

  res.sendStatus(200);
});

app.listen(3000, () => console.log("Webhook listener running on :3000"));
```

<Warning>
  You have to compute the HMAC on the **unparsed** body. If you run `JSON.parse` (or middleware like `express.json()`) before verifying and then re-serialize the object, the bytes change — spacing, key order, escaping — and the comparison fails even on perfectly legitimate requests. Always use the raw Buffer, for example with `express.raw`.
</Warning>

<Tip>
  Compare signatures in constant time (for example with `crypto.timingSafeEqual`) so you don't leak information through response timing. And never write the Webhook Secret to your application logs.
</Tip>

<Note>
  An endpoint that answers `401` gets no retries: `401` is a permanent error. Make sure verification is correct before going to production, otherwise you lose events.
</Note>

## Next steps

<CardGroup cols={2}>
  <Card title="Payload reference" icon="file-json" href="/en/webhooks/payload">
    The TypeScript data model, the response types and a sample payload.
  </Card>

  <Card title="Configuration" icon="settings" href="/en/webhooks/configuration">
    Where you set the secret and how to fire a test request.
  </Card>
</CardGroup>


## Related topics

- [Payload reference](/en/webhooks/payload.md)
- [Configuration](/en/webhooks/configuration.md)
- [How they work](/en/webhooks/introduction.md)
