Eridian

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
PartMeaning
tUnix timestamp of the delivery
v1Hex-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.

See Webhooks and API Keys.

Production API credentials are issued with an institution workspace. Contact sales if you need access.