Vérifier la signature d’un webhook avant tout traitement est non-négociable. Sans vérification, n’importe qui peut envoyer un faux payment_intent.completed à votre endpoint et marquer une commande comme payée sans qu’aucun fonds n’ait été reçu.

Le contrat

  • Signature : HMAC-SHA256(secret, raw_body) en hex
  • Timestamp : aussi à l’intérieur du body signé

Pourquoi le timestamp est important

Sans vérification de timestamp, un attaquant qui capture un webhook valide peut le rejouer indéfiniment. Le timestamp inclus dans le body signé :
  • Bloque les replays > 5 minutes
  • Permet à votre serveur de détecter si le timestamp est trop loin dans le futur (clock skew exploitable)

Avec le SDK Node (recommandé)

Le SDK :
  • Compare la signature en temps constant (crypto.timingSafeEqual)
  • Rejette les headers multi-value (header Signature dupliqué → ambigu → reject)
  • Tolérance par défaut 5 min (configurable via toleranceSeconds)
  • Rejette les timestamps > 60s dans le futur

Sans SDK (autre langage)

Erreurs courantes

Ne JAMAIS désactiver la vérification “temporairement pour debug”. Si vous ne pouvez pas faire passer un webhook, comparez en local avec un test depuis le dashboard (Webhooks → Envoyer test). Le payload est identique à un événement réel.

Changer de secret

Le secret d’un endpoint est généré une seule fois, à sa création, et n’est jamais régénéré : il n’existe pas de rotation in-place ni de bouton « Rotate secret ». Pour utiliser un nouveau secret, créez un nouvel endpoint (vous recevez un nouveau whsec_…), puis supprimez l’ancien. Pour basculer sans perdre d’événement, faites se chevaucher les deux endpoints le temps de la transition. Tant que les deux sont actifs, votre serveur reçoit chaque événement deux fois (une fois par secret) : dédupliquez via l’id de l’événement.
  1. Créez un nouvel endpoint (vous pouvez réutiliser la même URL) et notez son secret NEW_SECRET.
  2. Mettez votre serveur à jour pour accepter les deux secrets (ci-dessus).
  3. Supprimez l’ancien endpoint, puis retirez OLD_SECRET.