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...

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

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.