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

# Callback Signing

> Verify webhook authenticity using HMAC-SHA256 signatures by validating the Idenfy-Signature header against the raw HTTP request body.

Every webhook request from iDenfy includes an `Idenfy-Signature` header containing an **HMAC-SHA256** digest of the raw HTTP body. By validating this signature on your server you can confirm that iDenfy sent the payload and that no one tampered with it in transit.

## How It Works

<Steps>
  <Step title="Set a signing key">
    In the iDenfy dashboard, navigate to **Settings → Notifications → Headers** and assign a secret key to the webhook you want to protect.
  </Step>

  <Step title="iDenfy signs the request">
    When a webhook fires, iDenfy computes `HMAC-SHA256(secret, raw_body)` and attaches the hex-encoded result in the `Idenfy-Signature` header.
  </Step>

  <Step title="You verify the signature">
    On your server, compute the same HMAC over the raw request body and compare it to the header value using a **constant-time** comparison function.
  </Step>
</Steps>

## Quick Examples

<CodeGroup>
  ```python Python theme={"system"}
  import hmac, hashlib

  def verify(body: bytes, secret: str, signature: str) -> bool:
      expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
      return hmac.compare_digest(expected, signature)
  ```

  ```javascript JavaScript theme={"system"}
  const crypto = require("crypto");

  function verify(rawBody, secret, signature) {
    const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
    return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
  }
  ```

  ```csharp C# theme={"system"}
  using System.Security.Cryptography;
  using System.Text;

  bool Verify(byte[] body, string secret, string signature)
  {
      using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
      var expected = BitConverter.ToString(hmac.ComputeHash(body))
          .Replace("-", "").ToLowerInvariant();
      return CryptographicOperations.FixedTimeEquals(
          Encoding.UTF8.GetBytes(expected),
          Encoding.UTF8.GetBytes(signature));
  }
  ```
</CodeGroup>

## Full Server Examples

<AccordionGroup>
  <Accordion title="Python (Flask)">
    ```python theme={"system"}
    from flask import Flask, request, abort
    import hmac, hashlib

    app = Flask(__name__)
    SIGNING_SECRET = "your-signing-secret"

    @app.route("/webhook", methods=["POST"])
    def webhook():
        signature = request.headers.get("Idenfy-Signature", "")
        raw_body = request.get_data()  # raw bytes, not parsed JSON

        expected = hmac.new(
            SIGNING_SECRET.encode(), raw_body, hashlib.sha256
        ).hexdigest()

        if not hmac.compare_digest(expected, signature):
            abort(403, "Invalid signature")

        payload = request.get_json()
        # Process the verified payload ...
        return "OK", 200
    ```
  </Accordion>

  <Accordion title="JavaScript (Express)">
    ```javascript theme={"system"}
    const express = require("express");
    const crypto = require("crypto");

    const app = express();
    const SIGNING_SECRET = "your-signing-secret";

    // IMPORTANT: use express.raw(), NOT express.json()
    app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
      const signature = req.headers["idenfy-signature"] || "";
      const expected = crypto
        .createHmac("sha256", SIGNING_SECRET)
        .update(req.body) // req.body is a Buffer thanks to express.raw()
        .digest("hex");

      const valid = crypto.timingSafeEqual(
        Buffer.from(expected),
        Buffer.from(signature)
      );

      if (!valid) {
        return res.status(403).send("Invalid signature");
      }

      const payload = JSON.parse(req.body);
      // Process the verified payload ...
      res.sendStatus(200);
    });

    app.listen(3000);
    ```
  </Accordion>

  <Accordion title="C# (ASP.NET Core)">
    ```csharp theme={"system"}
    using Microsoft.AspNetCore.Mvc;
    using System.Security.Cryptography;
    using System.Text;

    [ApiController]
    [Route("webhook")]
    public class WebhookController : ControllerBase
    {
        private const string SigningSecret = "your-signing-secret";

        [HttpPost]
        public async Task<IActionResult> Receive()
        {
            using var reader = new StreamReader(Request.Body);
            var rawBody = await reader.ReadToEndAsync();
            var bodyBytes = Encoding.UTF8.GetBytes(rawBody);

            var signature = Request.Headers["Idenfy-Signature"].ToString();

            using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(SigningSecret));
            var expected = BitConverter.ToString(hmac.ComputeHash(bodyBytes))
                .Replace("-", "").ToLowerInvariant();

            if (!CryptographicOperations.FixedTimeEquals(
                    Encoding.UTF8.GetBytes(expected),
                    Encoding.UTF8.GetBytes(signature)))
            {
                return StatusCode(403, "Invalid signature");
            }

            // Process the verified payload ...
            return Ok();
        }
    }
    ```
  </Accordion>
</AccordionGroup>

## Important Security Notes

<Warning>
  **Always use constant-time comparison.** Using `==` or `===` to compare signatures exposes you to timing attacks. Use `hmac.compare_digest` (Python), `crypto.timingSafeEqual` (Node.js), or `CryptographicOperations.FixedTimeEquals` (C#).
</Warning>

<Warning>
  **Node.js: use `express.raw()`, not `express.json()`.** If you parse the body as JSON first, the re-serialized bytes may differ from the original payload, causing signature validation to fail every time.
</Warning>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Signature mismatch on every request">
    * Confirm you are reading the **raw** request body (bytes), not a parsed-and-re-serialized version.
    * In Node.js, make sure your route uses `express.raw()` instead of `express.json()`.
    * Verify the signing secret in your code exactly matches the value in **Settings → Notifications → Headers** (no trailing whitespace).
  </Accordion>

  <Accordion title="Incorrect signature length">
    The `Idenfy-Signature` header contains a **hex-encoded** HMAC (64 characters for SHA-256). If your computed value is a different length, check that you are using `.hexdigest()` / `.digest("hex")` rather than base64 encoding.
  </Accordion>

  <Accordion title="Missing Idenfy-Signature header">
    * Make sure a signing key is configured for the specific webhook endpoint in the iDenfy dashboard.
    * Check that your web framework is not stripping custom headers. Some frameworks normalize header names to lowercase.
  </Accordion>

  <Accordion title="Validation breaks behind a proxy or load balancer">
    Reverse proxies and load balancers can modify the request body (for example, re-encoding JSON or altering whitespace). Ensure your infrastructure passes the raw body through unmodified, or perform signature verification before any middleware that transforms the payload.
  </Accordion>
</AccordionGroup>
