Webhooks
HMAC signing for outbound results and inbound submissions
Aletheia's outbound result webhook is generic, any tenant that registers a callback URL
and an outbound HMAC secret receives HMAC-signed deliveries with X-Forensic-Signature.
Aletheia's inbound URL-fetch webhook is the push-style intake surface (see
Submitting a case §5): a tenant enrolled for
inbound push signs its POSTs with X-Inbound-Signature.
Both directions use HMAC-SHA256, emitted and verified as lowercase hexadecimal with
no prefix (no sha256=), computed over the exact raw bytes of the request body, not a
re-serialized or canonicalised form.
| Direction | Header | Who signs |
|---|---|---|
| Outbound, Aletheia to tenant | X-Forensic-Signature | Aletheia |
| Inbound, tenant to Aletheia | X-Inbound-Signature | The tenant |
Verification is constant-time on both sides. The inbound check lowercases the received header
before comparison, so an uppercase-hex inbound signature is also accepted, but emit lowercase
hex for forward-compatibility. Base64 and sha256=-prefixed forms are not accepted.
Sign and verify the exact bytes on the wire. Do not pretty-print, re-key-order, or re-encode the JSON before hashing, capture the raw body and HMAC that, distinct from the parsed JSON.
Reference verifier (Python)
import hashlib, hmac
def verify(secret: str, raw_body: bytes, received_sig: str) -> bool:
expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, received_sig.strip().lower())Reference verifier (Node.js)
const crypto = require('crypto');
function verify(secret, rawBody, receivedSig) {
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(receivedSig.trim().toLowerCase());
return a.length === b.length && crypto.timingSafeEqual(a, b);
}The outbound signed body is replayed unchanged on each retry attempt, so the signature is stable across retries. See also Guides / Webhooks for the cross-service overview.