> For the complete documentation index, see [llms.txt](https://docs.insurgrid.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.insurgrid.com/guides/signing-verification.md).

# Verify a webhook signature

Anyone can find your webhook URL and `POST` a fake payload to it. Signature verification is how your server tells a genuine InsurGrid delivery apart from an imposter — before you trust a single field in the body.

> **New term — signature verification:** recomputing a cryptographic fingerprint of an incoming request on your side, using a secret only you and the sender know, and checking it matches the one the sender attached. A match proves the request came from the sender and wasn't altered in transit.

This is the default auth scheme for every InsurGrid webhook (see [Register your endpoint & choose an auth type](/guides/register-endpoint-auth-types.md) if you've configured a different one — the rest of this page doesn't apply to those).

## How it works

Every delivery carries an `x-signature` header: an **HMAC-SHA256** of the **raw request body**, computed with your **signing secret** (from the **API section** — see [Authentication & tokens](/guides/authentication.md)), hex-encoded.

> **New term — HMAC-SHA256:** a hashing algorithm that mixes a secret key into the fingerprint it produces. Recomputing the same hash of the same bytes with the same secret always yields the same output — so if your recomputed value matches the header, the request is genuine.

The check has three parts:

1. Get the **exact raw bytes** InsurGrid sent — not the parsed-and-re-serialized JSON.
2. Compute `HMAC-SHA256(raw_body, signing_secret)`, hex-encoded.
3. Compare it to the `x-signature` header, using a **timing-safe** comparison. If it doesn't match, reject the request with `401`.

## ⚠️ The raw-bytes footgun

The single most common first-time bug: computing the HMAC over `JSON.stringify(parsedBody)` (or Python's `json.dumps(parsed)`) instead of the bytes as received.

Parsing JSON and re-serializing it does **not** reliably reproduce the original bytes — key order, whitespace, and number formatting can all differ. Even a one-byte difference produces a completely different hash, so the check fails even though the request is perfectly genuine.

**The fix:** capture the raw body *before* your framework parses it, and hash those bytes — never the parsed object.

## Verify it · *server*

**Node (Express):**

```js
const crypto = require("crypto");
const express = require("express");
const app = express();

// Keep the raw body — you need the exact bytes to verify the signature.
app.use(express.json({
  verify: (req, _res, buf) => { req.rawBody = buf; },
}));

function verify(rawBody, signature, secret) {
  const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(signature || "", "hex");
  // Lengths must match before timingSafeEqual — it throws on mismatched lengths.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post("/hooks/insurgrid", (req, res) => {
  const ok = verify(req.rawBody, req.get("x-signature"), process.env.INSURGRID_SIGNING_SECRET);
  if (!ok) return res.sendStatus(401);   // not from InsurGrid — reject

  res.sendStatus(200);                   // acknowledge fast
  handle(req.body);                      // process after responding
});
```

**Python (Flask):**

```python
import hmac, hashlib, os
from flask import Flask, request

app = Flask(__name__)

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

@app.post("/hooks/insurgrid")
def insurgrid_hook():
    raw = request.get_data()                  # exact bytes — before any parsing
    signature = request.headers.get("x-signature", "")

    if not verify(raw, signature, os.environ["INSURGRID_SIGNING_SECRET"]):
        return "", 401                         # not from InsurGrid — reject

    handle(request.get_json())                 # parse only after verifying
    return "", 200                             # acknowledge fast
```

Notice both handlers read the raw body **before** parsing JSON, verify first, and only trust `req.body` / `request.get_json()` after the check passes.

## Why timing-safe comparison

A plain `expected === signature` string comparison returns as soon as it finds the first mismatched character — which means it takes slightly longer to reject a guess that gets more characters right. An attacker who can measure that timing difference can, in theory, guess a valid signature one byte at a time. `crypto.timingSafeEqual` (Node) and `hmac.compare_digest` (Python) take the same amount of time regardless of where the mismatch is, closing that side channel. Always use them for this comparison instead of `===` or `==`.

## Reject on mismatch

If the signature doesn't match, return `401` and stop — don't process the body, don't log it as a near-miss with full contents, and don't fall back to trusting the request anyway. A mismatch means either the secret is wrong on one side (check you copied the current one) or the request isn't from InsurGrid.

## Using a different scheme instead

If signature verification doesn't fit your setup, you can configure your endpoint to authenticate deliveries a different way — HMAC-SHA256 with your own secret, Basic auth, a bearer token, or a static API-key header. See [Register your endpoint & choose an auth type](/guides/register-endpoint-auth-types.md) for all five options and when each makes sense. A static credential is simpler to check but, unlike an HMAC, it doesn't prove the body wasn't altered in transit.

## Related

* [Authentication & tokens](/guides/authentication.md)
* [Register your endpoint & choose an auth type](/guides/register-endpoint-auth-types.md)
* [Delivery & de-duplication](/guides/delivery-dedup.md)
* [Quickstart A — Receive policy data](/quickstarts/a-receive-policy-data.md)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.insurgrid.com/guides/signing-verification.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
