Aller au contenu

Webhooks

Aucune livraison, signature toujours invalide ou même événement reçu trois fois.

Les problèmes de webhook sont particulièrement simples à diagnostiquer, car Wajub enregistre chaque tentative de livraison et la réponse de votre endpoint. Commencez par ouvrir Konsole, Webhook Tester. Cet écran indique si Wajub a essayé et ce que votre serveur a répondu. Ces deux informations vous orientent vers la bonne partie de cette page.

Aucun webhook n'arrive

Si Konsole n'affiche aucune tentative, l'événement n'a jamais ciblé votre endpoint. Si des tentatives ont échoué, le problème se trouve de votre côté.

Affichage dans KonsoleCauseSolution
Aucune tentativeAucun endpoint enregistré dans cet environnementEnregistrez-le, la sandbox et le mode live sont distincts
Tentative, connexion refuséeL'URL n'est pas accessible depuis Internetlocalhost ne l'est pas, utilisez wajub listen
Tentative, 404 ou 405La route existe pour GET, pas pour POSTLes webhooks utilisent toujours POST
Tentative, 5xxVotre handler a levé une erreurConsultez la dernière section de cette page

Le développement local nécessite un tunnel, fourni par la CLI.

Transférer les livraisons live vers une route locale
wajub listen --forward-to localhost:3000/webhooks/wajub

Recevoir des webhooks en local présente les autres options. Déclencher des événements permet de produire un événement précis sans effectuer de paiement réel.

La signature reste toujours invalide

Il existe quatre causes. La première explique la plupart des cas.

Votre framework a analysé le corps avant le calcul du hash. La signature couvre les octets exacts envoyés par Wajub. Après l'analyse et la resérialisation par un middleware JSON, un ordre de clés ou une espace différente suffit à invalider le hash, même si l'objet reste identique. Récupérez le corps brut sur la route du webhook avant l'exécution de tout analyseur.

Octets bruts uniquement sur la route du webhook
app.post('/webhooks/wajub', express.raw({ type: '*/*' }), (req, res) => {
  const event = wajub.webhooks.constructEvent(
    req.body,
    req.headers['x-wajub-signature'],
    req.headers['x-wajub-timestamp'],
  );
  res.sendStatus(200);
});

app.use(express.json());

L'ordre de ces deux lignes est important. Si express.json() est monté en premier, il consomme le corps de chaque route et votre handler brut reçoit un buffer vide.

Vous avez transmis le secret comme quatrième argument. constructEvent reçoit le payload, la signature, l'horodatage et éventuellement une tolérance en secondes. Le secret de signature provient du client construit avec webhookSecret. Le transmettre en quatrième position ne lève aucune erreur. Il remplace la tolérance par une chaîne, ignore la vérification du décalage et désactive la protection contre les répétitions.

Vous avez calculé le hash de la mauvaise chaîne. Le payload signé est {timestamp}.{raw_body}, avec un point littéral entre les deux valeurs. L'en-tête contient un préfixe v1= à retirer avant la comparaison.

Votre comparaison a levé une erreur au lieu de renvoyer false. crypto.timingSafeEqual lève une RangeError lorsque les buffers ont des longueurs différentes, précisément le résultat d'une signature tronquée ou mal formée. Comparez d'abord les longueurs, puis utilisez une comparaison en temps constant. Vérification de signature présente l'implémentation manuelle.

event.type vaut undefined

Le nom de l'événement se trouve dans event, pas dans type.

Contenu réellement reçu
{
"id": "evt_9xh0XQ5wu2lxCSUGajfv",
"event": "payment.succeeded",
"data": {
"id": "trx_CSUGajfv9xh0XQ5wu2lx",
"status": "succeeded"
},
"livemode": true,
"pending_webhooks": 0,
"api_version": "2026-08-01",
"request": null,
"created": 1748083260
}

Adaptez votre traitement à event.event. Si une autre plateforme utilisait type, cette habitude provoque l'erreur. Les SDKs Node et PHP typent actuellement l'événement analysé sous la forme { type, data }, ce qui ne correspond pas aux données reçues et sera corrigé. Fiez-vous au JSON ci-dessus.

Le même événement arrive plusieurs fois

Ce comportement est prévu. Une livraison qui ne répond pas avec 2xx en 10 secondes est réessayée cinq fois après 30 secondes, 1 minute, 5 minutes, 10 minutes et 1 heure. Un déploiement pendant cette période ou un handler momentanément lent produit des doublons.

Dédupliquez avec id, qui reste identique lors de chaque nouvelle tentative de la même livraison. Enregistrez-le dans un emplacement qui survit aux redémarrages. Un Set interne au processus se vide pendant le déploiement qui a justement provoqué le retard à traiter.

La décision vient de l'insertion, pas d'une recherche
CREATE TABLE processed_events (
  event_id     TEXT PRIMARY KEY,
  processed_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

INSERT INTO processed_events (event_id)
VALUES ($1)
ON CONFLICT (event_id) DO NOTHING
RETURNING event_id;

Une ligne est renvoyée la première fois et aucune lors d'une répétition, sans concurrence entre la vérification et l'écriture.

Mon handler est lent ou lève une erreur

Le délai entre la connexion et la réponse est de 10 secondes. Toutes les opérations de livraison de la commande, comme une écriture en base de données, un PDF ou un e-mail, doivent avoir lieu après l'accusé de réception.

Vérifiez la signature, enregistrez l'identifiant de l'événement, répondez 200, puis placez l'événement dans une file. Si le traitement échoue, réessayez depuis la file. Répondre 500 après une erreur de livraison demande à Wajub de renvoyer l'événement. Le handler échoue encore et un bug se reproduit cinq fois.

Le seul cas qui doit produire une réponse autre que 2xx est une signature invalide. Renvoyez 400 : la requête ne vient pas de Wajub et aucune nouvelle livraison n'est nécessaire.

Un événement attendu ne s'est jamais déclenché

Comparez son nom au Catalogue des événements avant de conclure à un bug. Deux erreurs sont fréquentes : invoice.paid n'existe pas et le résultat d'un remboursement arrive sous la forme refund.succeeded, pas sur le paiement.

Si l'événement existe et que votre endpoint y est abonné, Konsole indique s'il a été généré. Un événement jamais généré signifie que le paiement n'a jamais atteint cet état. Consultez alors Paiements.

Que pensez-vous de ce contenu ?