Webhooks

Configuration, signature HMAC et événements marchands.

Configuration

Créez un endpoint dans le menu Développeurs, section Webhooks, ou via POST /merchant/merchants/:id/webhook-endpoints.

Le secret whsec_… n'est affiché qu'une fois. Pour faire tourner le secret sans recréer l'URL : POST …/webhook-endpoints/:endpointId/rotate-secret.

Signature

Chaque livraison inclut X-Izzi-Signature (HMAC-SHA256, hex sans préfixe) et X-Izzi-Timestamp (secondes Unix).

Corps signé : concaténation {timestamp}.{rawBody} (point littéral entre les deux).

Fenêtre de validité recommandée : 300 secondes. Vérifiez la signature avant de parser le JSON.

En-tête X-Izzi-Event-Id : identifiant unique de la livraison (idempotence côté récepteur).

Il n'y a pas d'endpoint IzziPay pour vérifier une livraison. Recalculez le HMAC chez vous avec le secret whsec_… affiché à la création de l'endpoint.

Vérification avec le SDK

Le SDK Node expose verifyWebhookSignature. Il applique la fenêtre de 300 secondes et une comparaison en temps constant.

typescript
import { verifyWebhookSignature } from "@izzipay/sdk";

const result = verifyWebhookSignature({
  secret: process.env.IZZI_WEBHOOK_SECRET!,
  rawBody: requestBody,
  signature: request.headers["x-izzi-signature"],
  timestamp: request.headers["x-izzi-timestamp"],
});

if (!result.verified) {
  throw new Error(result.reason ?? "invalid signature");
}

Vérification manuelle (Node.js)

Même calcul sans SDK, avec node:crypto.

Passez le corps brut, octet pour octet. Ne reconstruisez pas le JSON avec JSON.stringify(req.body) après un middleware qui parse le body.

Sur Express, utilisez express.raw() sur cette route. Sur Fastify, conservez le raw body dans preParsing.

En Python, comparez le même message signé avec hmac.compare_digest.

typescript
import { createHmac, timingSafeEqual } from "node:crypto";

const TOLERANCE_SECONDS = 300;

function verifyIzziWebhook(input: {
  secret: string;
  rawBody: string;
  signature: string | undefined;
  timestampHeader: string | undefined;
}): boolean {
  if (!input.signature || !input.timestampHeader) return false;

  const timestamp = Number(input.timestampHeader);
  if (!Number.isFinite(timestamp)) return false;

  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - timestamp) > TOLERANCE_SECONDS) return false;

  const expected = createHmac("sha256", input.secret)
    .update(`${timestamp}.${input.rawBody}`)
    .digest("hex");

  if (expected.length !== input.signature.length) return false;
  return timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(input.signature, "hex"),
  );
}

Relivraison

Jusqu'à 20 tentatives par événement, backoff exponentiel (1 s → 1 h), timeout HTTP 8 s par tentative.

Circuit breaker après échecs répétés sur un endpoint ; consultez l'historique via GET …/webhook-deliveries.

Adresses IP sortantes

IZZIPAY ne publie pas de liste fixe d'IP egress pour les webhooks. Authentifiez chaque requête via la signature HMAC plutôt que par filtrage IP.

Événements

Voir la référence API Webhooks pour les payloads exemples de chaque type.