Webhook Signing
HMAC verification of X-Eridian-Signature.
Every webhook request includes X-Eridian-Signature. Verify the signature against the raw body before you parse JSON. Reject unsigned or stale payloads. Rotation of whsec_ secrets is described at the end of this page.
Header Format
X-Eridian-Signature: t=1718400000,v1=4f1c0a9e8b7d6c5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1
| Part | Meaning |
|---|---|
t | Unix timestamp of the delivery |
v1 | Hex-encoded HMAC-SHA256 |
The signed payload is {t}.{raw_body} with no extra whitespace.
Verify
Reject if t is older than 5 minutes (replay window). Compute HMAC-SHA256 with the webhook secret. Compare using a constant-time function.
Python
import hmac
import hashlib
import time
def verify_eridian_signature(secret: str, header: str, raw_body: bytes) -> None:
parts = dict(item.split("=", 1) for item in header.split(","))
timestamp = int(parts["t"])
if abs(time.time() - timestamp) > 300:
raise ValueError("stale webhook timestamp")
signed = f"{timestamp}.".encode("utf-8") + raw_body
expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, parts["v1"]):
raise ValueError("invalid X-Eridian-Signature")
TypeScript
import crypto from "node:crypto";
export function verifyEridianSignature(secret: string, header: string, rawBody: Buffer): void {
const parts = Object.fromEntries(header.split(",").map((item) => item.split("=")));
const timestamp = Number(parts.t);
if (Math.abs(Date.now() / 1000 - timestamp) > 300) {
throw new Error("stale webhook timestamp");
}
const signed = Buffer.concat([Buffer.from(`${timestamp}.`), rawBody]);
const expected = crypto.createHmac("sha256", secret).update(signed).digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(parts.v1 ?? "", "hex"))) {
throw new Error("invalid X-Eridian-Signature");
}
}
Parse JSON only after verification. Reading req.body as an already-parsed object will fail verification because byte order and whitespace will not match.
Multiple Secrets During Rotation
When you rotate, Eridian may send v1 (current) and v1 plus v0 (previous) for 24 hours:
X-Eridian-Signature: t=1718400000,v1=...,v0=...
Accept the payload if any listed v1/v0 digest matches a secret you still hold.
POST /v1/webhooks/wh_02c9/rotate-secret
Authorization: Bearer eridian_sk_...
The new whsec_ is returned once. Store it in the same secret manager you use for eridian_sk_ keys.
Failures
Failed verification should return HTTP 400. Do not return 500; Eridian will retry and you will amplify load. Log X-Eridian-Delivery-Id and X-Eridian-Request-Id without logging the raw body if it may contain residual business data.
Production API credentials are issued with an institution workspace. Contact sales if you need access.