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

# Webhook Verification

> Step-by-step HMAC verification for inbound Liquidramp webhook deliveries.

Every outbound webhook is HMAC-signed. Verify before processing to prevent spoofed callbacks.

## Headers

| Header                 | Value             |
| ---------------------- | ----------------- |
| `liquidramp-signature` | `sha256=<hex>`    |
| `liquidramp-timestamp` | Unix milliseconds |

## Canonical string

Same algorithm as inbound API signing, but the path is **your webhook URL path**:

```
clientId|timestamp|POST|/webhooks/liquidramp|{"event":"order.created","data":{...}}
```

| Segment     | Rule                                                    |
| ----------- | ------------------------------------------------------- |
| `clientId`  | Your partner reference                                  |
| `timestamp` | `liquidramp-timestamp` header value                     |
| `POST`      | Always `POST`                                           |
| `path`      | URL pathname + query of your webhook endpoint (no host) |
| `body`      | Raw POST body string                                    |

HMAC-SHA256 with your `enc_*` encryption key.

## Node.js verification

```javascript theme={null}
import crypto from "node:crypto";

function verifyWebhook({ rawBody, signature, timestamp, clientId, encryptionKey, path }) {
  const canonical = [clientId, timestamp, "POST", path, rawBody].join("|");
  const expected = "sha256=" + crypto.createHmac("sha256", encryptionKey).update(canonical).digest("hex");

  if (signature.length !== expected.length) return false;
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
```

## Express handler pattern

```javascript theme={null}
app.post("/webhooks/liquidramp", express.raw({ type: "application/json" }), (req, res) => {
  const rawBody = req.body.toString("utf8");
  const signature = req.headers["liquidramp-signature"];
  const timestamp = req.headers["liquidramp-timestamp"];

  if (!verifyWebhook({ rawBody, signature, timestamp, clientId: PARTNER_REF, encryptionKey: ENC_KEY, path: "/webhooks/liquidramp" })) {
    return res.status(401).send("Invalid signature");
  }

  const payload = JSON.parse(rawBody);
  res.status(200).send("OK");
  processWebhookAsync(payload); // after ack
});
```

## Timestamp validation

Reject requests with timestamps outside a reasonable window (e.g. ±5 minutes) to limit replay attacks.

## Common failures

* Parsing JSON before verification (body must be raw bytes)
* Wrong path (must match registered `webhook_url` pathname exactly)
* Using `sk_*` instead of `enc_*` for HMAC

## Related

* [Webhooks quickstart](/quickstarts/webhooks)
* [Security](/compliance/overview)
* [Callbacks](/concepts/callbacks)
