Sécurité webhook

Toutes les livraisons webhook HTTP sont signées avec HMAC-SHA256. Vérifiez la signature de votre côté pour confirmer que la requête provient de Mailverick et n'a pas été altérée.

Format de la signature

La signature est envoyée dans l'en-tête de la requête et suit ce format :

t=1711278330,v1=5a4e3c2b1d...

Fonctionnement de la signature

Le payload signé est construit en concaténant l'horodatage et le corps brut de la requête avec un point comme séparateur :

signed_payload = timestamp + "." + raw_body

L'empreinte HMAC-SHA256 est calculée en utilisant votre secret webhook comme clé.

Étapes de vérification

Exemples de code

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 {
  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
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);
}

Gestion des secrets

Votre secret webhook est généré automatiquement lors de la création d'un abonné. Vous pouvez le régénérer à tout moment depuis le portail via l'action « Régénérer le secret ». Après rotation, l'ancien secret est immédiatement invalidé.

Timeout

Les requêtes webhook ont un timeout configurable entre 5 et 300 secondes. Si votre endpoint ne répond pas dans le délai, la livraison est marquée comme échouée. Les livraisons échouées ne sont pas réessayées. Utilisez une file de messages de votre côté si vous avez besoin d'un traitement garanti.

Intégrations Pub/Sub

Google Cloud Pub/Sub, AWS SNS et Azure Event Grid utilisent leurs propres mécanismes d'authentification (IAM, identifiants de compte de service). Les signatures webhook ne sont pas utilisées pour ces intégrations. L'authentification est gérée par le fournisseur cloud.