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

# Signing

> Check that a request came from hooksnode.

When signing is on, hooksnode adds this header to each request to the destination:

```http theme={null}
X-Hooksnode-Signature: sha256=<hex>
```

The value is the HMAC-SHA256 of the raw request body, with the destination's signing secret as the key. The body is the final body, after the transform.

## Turn on signing

1. Open the destination and turn on **Signing**. hooksnode makes a 64-character secret.
2. Copy the secret into your endpoint's settings, for example an environment variable.
3. To change the secret, press **Regenerate**. The old secret stops at once.

Signing is off by default. Each destination has its own secret.

## Verify the signature

Always compute the HMAC over the **raw** body bytes, before you parse the JSON. Compare in constant time.

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

  const app = express();

  app.post("/webhooks/hooksnode", express.raw({ type: "*/*" }), (req, res) => {
    const expected =
      "sha256=" +
      crypto.createHmac("sha256", process.env.HOOKSNODE_SECRET).update(req.body).digest("hex");
    const given = req.get("X-Hooksnode-Signature") ?? "";

    const ok =
      given.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
    if (!ok) return res.status(401).end();

    const event = JSON.parse(req.body);
    // Handle the event.
    res.sendStatus(200);
  });
  ```

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

  from flask import Flask, abort, request

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


  @app.post("/webhooks/hooksnode")
  def hooksnode():
      body = request.get_data()
      expected = "sha256=" + hmac.new(SECRET, body, hashlib.sha256).hexdigest()
      given = request.headers.get("X-Hooksnode-Signature", "")
      if not hmac.compare_digest(given, expected):
          abort(401)
      event = request.get_json()
      # Handle the event.
      return "", 200
  ```

  ```go Go theme={null}
  func handler(w http.ResponseWriter, r *http.Request) {
  	body, err := io.ReadAll(r.Body)
  	if err != nil {
  		http.Error(w, "bad body", http.StatusBadRequest)
  		return
  	}
  	mac := hmac.New(sha256.New, []byte(os.Getenv("HOOKSNODE_SECRET")))
  	mac.Write(body)
  	expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
  	given := r.Header.Get("X-Hooksnode-Signature")
  	if !hmac.Equal([]byte(expected), []byte(given)) {
  		w.WriteHeader(http.StatusUnauthorized)
  		return
  	}
  	// Handle the event.
  	w.WriteHeader(http.StatusOK)
  }
  ```

  ```php PHP theme={null}
  <?php
  $body = file_get_contents('php://input');
  $expected = 'sha256=' . hash_hmac('sha256', $body, getenv('HOOKSNODE_SECRET'));
  $given = $_SERVER['HTTP_X_HOOKSNODE_SIGNATURE'] ?? '';

  if (!hash_equals($expected, $given)) {
      http_response_code(401);
      exit;
  }
  $event = json_decode($body, true);
  // Handle the event.
  http_response_code(200);
  ```
</CodeGroup>

## Replay protection

The signature has no timestamp. To stop an attacker who copies a signed request and sends it again, make your handler idempotent. See [Idempotency](/delivery/idempotency).

## Signatures from the provider

hooksnode sends the incoming headers on unchanged. If the event came from Paystack, Stripe or GitHub, your endpoint still gets the provider's signature header, such as `x-paystack-signature`, `Stripe-Signature` or `X-Hub-Signature-256`. The body is also unchanged, unless you added a transform. You can check the provider's signature as well.

<Warning>A transform changes the body. The provider's signature then no longer matches. Check `X-Hooksnode-Signature` instead.</Warning>
