Webhook Security
All HTTP webhook deliveries are signed with HMAC-SHA256. Verify the signature on your end to ensure the request comes from Mailverick and hasn't been tampered with.
Signature Format
The signature is sent in the request header and follows this format:
t=1711278330,v1=5a4e3c2b1d... t: Unix timestamp (seconds) of when the webhook was sentv1: HMAC-SHA256 hex digest of the signed payload
How Signing Works
The signed payload is built by concatenating the timestamp and the raw request body with a dot separator:
signed_payload = timestamp + "." + raw_body The HMAC-SHA256 digest is computed using your webhook secret as the key.
Verification Steps
- Extract the
t(timestamp) andv1(hash) from the signature header. - Optionally check the timestamp is within a tolerance window (default 300 seconds) to prevent replay attacks.
- Build the signed payload:
t + "." + raw_request_body. - Compute the HMAC-SHA256 digest using your webhook secret.
- Compare the computed hash with the
v1value using a timing-safe comparison.
Code Examples
import { createHmac, timingSafeEqual } from "crypto";
// toleranceSec: max age in seconds, 0 to skip replay check
function verifyWebhookSignature(payload, signature, secret, toleranceSec = 300) {
const [, ts, hash] = signature.match(/^t=(\d+),v1=([a-f0-9]+)$/) || [];
if (!ts) return false;
if (toleranceSec > 0 && Math.abs(Date.now() / 1000 - ts) > toleranceSec) return false;
const expected = createHmac("sha256", secret).update(ts + "." + payload).digest("hex");
return timingSafeEqual(Buffer.from(hash), Buffer.from(expected));
} import hmac, hashlib, re, time
# tolerance_sec: max age in seconds, 0 to skip replay check
def verify_webhook_signature(payload, signature, secret, tolerance_sec=300):
m = re.match(r"^t=(\d+),v1=([a-f0-9]+)$", signature)
if not m:
return False
ts, hash_val = m.group(1), m.group(2)
if tolerance_sec > 0 and abs(time.time() - int(ts)) > tolerance_sec:
return False
expected = hmac.new(secret.encode(), f"{ts}.{payload}".encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(hash_val, expected) import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.util.HexFormat;
import java.util.regex.*;
public class WebhookSignature {
// toleranceSec: max age in seconds, 0 to skip replay check
public static boolean verify(String payload, String signature, String secret, long toleranceSec) {
Matcher m = Pattern.compile("^t=(\\d+),v1=([a-f0-9]+)$").matcher(signature);
if (!m.matches()) return false;
long ts = Long.parseLong(m.group(1));
if (toleranceSec > 0 && Math.abs(System.currentTimeMillis() / 1000 - ts) > toleranceSec) return false;
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256"));
String expected = HexFormat.of().formatHex(mac.doFinal((ts + "." + payload).getBytes("UTF-8")));
return MessageDigest.isEqual(m.group(2).getBytes(), expected.getBytes());
} catch (Exception e) { return false; }
}
} <?php
// $toleranceSec: max age in seconds, 0 to skip replay check
function verifyWebhookSignature(string $payload, string $signature, string $secret, int $toleranceSec = 300): bool {
if (!preg_match('/^t=(\d+),v1=([a-f0-9]+)$/', $signature, $m)) return false;
[$ts, $hash] = [$m[1], $m[2]];
if ($toleranceSec > 0 && abs(time() - (int)$ts) > $toleranceSec) return false;
$expected = hash_hmac('sha256', "$ts.$payload", $secret);
return hash_equals($hash, $expected);
} Secret Management
Your webhook secret is generated automatically when you create an event subscriber. You can rotate it at any time from the portal using the "Regenerate secret" action. After rotation, the old secret is invalidated immediately.
Timeout
Webhook requests have a configurable timeout between 5 and 300 seconds. If your endpoint doesn't respond within the timeout, the delivery is marked as failed. Failed deliveries are not retried. Use a message queue on your end if you need guaranteed processing.
Pub/Sub Integrations
Google Cloud Pub/Sub, AWS SNS, and Azure Event Grid use their own authentication mechanisms (IAM, service account credentials). Webhook signatures are not used for these integrations. Authentication is handled by the cloud provider.