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.
In the iDenfy dashboard, navigate to Settings → System notifications, click the webhook you want to protect to expand it, then go to the Headers section and set the Signing key field.
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.
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));}
from flask import Flask, request, abortimport hmac, hashlibapp = 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
JavaScript (Express)
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);
C# (ASP.NET Core)
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(); }}
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.
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 → System notifications → Headers (no trailing whitespace).
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.
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.
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.