Every delivery is signed so you can confirm it genuinely came from Audienceful and wasn’t tampered with or replayed. Verify the signature on each incoming request before trusting its contents.
Each delivery carries an X-Audienceful-Signature header containing a timestamp and a signature:
X-Audienceful-Signature: t=1751630400,v1=<hex hmac-sha256>
t — the Unix timestamp (seconds) when the delivery was signed.
v1 — the signature: the lowercase hex HMAC-SHA256 of the string {t}.{raw_request_body}, keyed with your endpoint’s signing secret.
How to verify
- Read the raw request body and the
X-Audienceful-Signature header.
- Parse
t and v1 out of the header.
- Reject the request if
t is more than 5 minutes away from your current time (replay protection).
- Compute
HMAC-SHA256(secret, "{t}.{raw_body}") as lowercase hex.
- Compare your computed value against
v1 using a constant-time comparison. If they match, the request is authentic.
Your signing secret (whsec_…) is returned once, when you create the endpoint.
Verify against the raw request body bytes, exactly as received — before any JSON parsing or re-serialization. Re-serializing the body (even reformatting whitespace) changes it, and the signature will no longer match. Most frameworks expose the raw body separately from the parsed JSON.
Examples
import hashlib, hmac, time
def verify_signature(secret: str, raw_body: str, signature_header: str, tolerance: int = 300) -> bool:
parts = dict(kv.split("=", 1) for kv in signature_header.split(","))
timestamp = int(parts["t"])
# Reject stale timestamps (replay protection).
if abs(time.time() - timestamp) > tolerance:
return False
expected = hmac.new(
secret.encode(), f"{timestamp}.{raw_body}".encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, parts["v1"])
import crypto from "crypto";
export function verifySignature(
secret: string,
rawBody: string,
signatureHeader: string,
toleranceSeconds = 300,
): boolean {
const parts: Record<string, string> = {};
for (const kv of signatureHeader.split(",")) {
const [key, value] = kv.split("=", 2);
parts[key] = value;
}
const timestamp = parseInt(parts.t, 10);
// Reject stale timestamps (replay protection).
if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) {
return false;
}
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const expectedBuf = Buffer.from(expected);
const providedBuf = Buffer.from(parts.v1 ?? "");
return (
expectedBuf.length === providedBuf.length &&
crypto.timingSafeEqual(expectedBuf, providedBuf)
);
}
<?php
function verify_signature(string $secret, string $rawBody, string $signatureHeader, int $tolerance = 300): bool {
$parts = [];
foreach (explode(',', $signatureHeader) as $kv) {
[$key, $value] = array_pad(explode('=', $kv, 2), 2, '');
$parts[$key] = $value;
}
$timestamp = (int) ($parts['t'] ?? 0);
// Reject stale timestamps (replay protection).
if (abs(time() - $timestamp) > $tolerance) {
return false;
}
$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
return hash_equals($expected, $parts['v1'] ?? '');
}
The same signature scheme is used by the
automation “Send Webhook” action, signed with that action’s own secret — so the same verification code works there too.