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

# Verify signatures

> HMAC-SHA256 of the raw body, compared in constant time.

Every webhook delivery includes a `x-klikit-signature` header. You **must**
verify it before processing the payload — otherwise anyone who knows your
URL can forge orders.

## The recipe

```
expected = hex( HMAC-SHA256( your_webhook_secret, raw_request_body ) )

if expected != x-klikit-signature: reject
```

Three things to keep straight:

* **HMAC-SHA256**, not plain SHA-256. The secret is the key, the body is the
  message.
* **Raw request body**, not the parsed-and-reserialised JSON. Any whitespace
  or key-order difference changes the bytes and breaks the hash.
* **Lowercase hex** encoding, not base64.

## Worked examples

<CodeGroup>
  ```js Node (Express) theme={null}
  const crypto = require("crypto");
  const express = require("express");

  const app = express();

  // IMPORTANT: capture the raw body BEFORE JSON parsing.
  app.use(express.json({
    verify: (req, _res, buf) => { req.rawBody = buf; },
  }));

  app.post("/webhooks/klikit", (req, res) => {
    const expected = crypto
      .createHmac("sha256", process.env.KLIKIT_WEBHOOK_SECRET)
      .update(req.rawBody)
      .digest("hex");

    const given = req.get("x-klikit-signature") || "";
    const ok = given.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));

    if (!ok) return res.status(401).json({ error: "invalid signature" });
    res.status(200).json({ status: "received" });
  });
  ```

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

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

  @app.post("/webhooks/klikit")
  def receive():
      raw = request.get_data(cache=True)  # raw bytes, not parsed JSON
      expected = hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
      given = request.headers.get("x-klikit-signature", "")
      if not hmac.compare_digest(given, expected):
          return jsonify(error="invalid signature"), 401
      return jsonify(status="received"), 200
  ```

  ```go Go (net/http) theme={null}
  package main

  import (
      "crypto/hmac"
      "crypto/sha256"
      "encoding/hex"
      "io"
      "net/http"
      "os"
  )

  var secret = []byte(os.Getenv("KLIKIT_WEBHOOK_SECRET"))

  func receive(w http.ResponseWriter, r *http.Request) {
      raw, _ := io.ReadAll(r.Body)
      mac := hmac.New(sha256.New, secret)
      mac.Write(raw)
      expected := hex.EncodeToString(mac.Sum(nil))

      given := r.Header.Get("x-klikit-signature")
      if !hmac.Equal([]byte(given), []byte(expected)) {
          http.Error(w, "invalid signature", http.StatusUnauthorized)
          return
      }
      w.WriteHeader(http.StatusOK)
  }
  ```

  ```php PHP theme={null}
  <?php
  $secret = getenv('KLIKIT_WEBHOOK_SECRET');
  $raw = file_get_contents('php://input');
  $expected = hash_hmac('sha256', $raw, $secret);

  $given = $_SERVER['HTTP_X_KLIKIT_SIGNATURE'] ?? '';
  if (!hash_equals($expected, $given)) {
      http_response_code(401);
      echo json_encode(['error' => 'invalid signature']);
      exit;
  }
  http_response_code(200);
  echo json_encode(['status' => 'received']);
  ```

  ```bash Shell (for testing) theme={null}
  # Recreate the signature from a captured body
  SECRET="your-webhook-secret"
  BODY='{"brand_id":1,"branch_id":2,"orders":[{"id":42}]}'
  printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}'
  ```
</CodeGroup>

## Generating a test delivery yourself

To validate your verifier before klikit is wired up to your URL, build a
test request locally:

```bash theme={null}
SECRET="your-webhook-secret"
BODY='{"brand_id":1,"branch_id":2,"orders":[{"id":42}]}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')

curl -X POST http://localhost:8080/webhooks/klikit \
  -H "Content-Type: application/json" \
  -H "x-klikit-event-id: test-$(uuidgen)" \
  -H "x-klikit-event-type: klikit.order.created.v2" \
  -H "x-klikit-signature: $SIG" \
  -d "$BODY"
```

Expected response: `200 {"status":"received"}`.

To confirm verification works, mangle the body or the signature and re-send.
You should now get `401 {"error":"invalid signature"}`.

## Where to get your `webhook_secret`

Your klikit integration contact provides it during onboarding, alongside
your partner API key. Don't commit the secret. Rotate it via the operator if
you suspect it's been exposed.
