Skip to main content
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 the payload was sent by iDenfy and has not been tampered with in transit.

How it works

1

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.
2

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.
3

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.

Quick examples

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)

Full server examples

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
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);
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();
    }
}

Important security notes

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#).
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.

Troubleshooting

  • 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).
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.
  • 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.
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.