Webhook qui renvoie 401 ou 403 : un petit nombre de causes. La plupart du temps ce n'est même pas un problème de signature — c'est un middleware ou un default framework qui rejette avant votre code. Voici la checklist de diagnostic qu'on utilise, dans l'ordre.
D'abord, distinguer 401 de 403
401 Unauthorized : le serveur a reçu la requête, regardé les credentials ou la signature, et refusé. Votre handler a probablement tourné.
403 Forbidden : le serveur a reçu et refusé pour une autre raison. Souvent la requête n'a jamais atteint votre handler.
Ouvrez la capture du tunnel et regardez la réponse. Si vous voyez le body de votre handler ("invalid signature"), c'est un 401 de votre code. Sinon, le framework a rejeté avant.
Les cinq causes les plus probables
1. Middleware CSRF (Django, Rails, Laravel)
La protection CSRF par défaut rejette les POSTs sans token de session. Les providers webhook n'en envoient pas. Symptômes : 403, body générique, pas de logs handler.
Solution : exclure la route webhook du CSRF. Django : @csrf_exempt. Rails : skip_before_action :verify_authenticity_token. Laravel : ajoutez à VerifyCsrfToken::$except. Détaillé dans le guide Django.
2. Middleware d'auth trop large
Middleware d'auth global (JWT, session, API key) appliqué à la route webhook. Le provider n'envoie pas votre header d'auth, le middleware renvoie 401 avant votre handler.
Solution : exclure le chemin webhook. L'auth webhook est la signature.
3. Vérification de signature qui échoue
Votre handler a tourné mais la signature ne correspond pas. Sous-causes par fréquence décroissante :
- Corps parsé par un middleware avant la vérification.
- Mauvais encodage (hex vs base64). Voir guide de signature.
- Mauvais secret (test vs live, dashboard vs CLI, env vs runtime).
- Timestamp trop vieux.
- Whitespace en fin de secret chargé depuis un fichier env.
4. Mauvaise URL tunnel enregistrée
Tunnel redémarré, URL changée, mais le dashboard provider a encore l'ancienne. Ressemble à un 401 mais la requête atteint un autre serveur.
Solution : confirmez que l'URL du dashboard correspond à votre session tunnel actuelle. Pour URL stable, sous-domaine réservé ou named tunnels.
5. Restrictions CORS, content-type, méthode
Rare pour les webhooks. Route acceptant uniquement application/json + provider en form-encoded (Twilio) : 415 souvent, mais 403 possible si mal configuré.
Flow de diagnostic 90 secondes
- Lisez la réponse. Si elle contient les mots de votre handler, la requête est arrivée. Sinon non.
- Logs handler. Vos statements logguent-ils ?
- URL enregistrée. Dashboard provider = session tunnel actuelle ?
- En-têtes entrants. Capture tunnel = ce que votre code lit.
- Longueur du corps au moment de la signature. Si zéro, un middleware l'a mangé.
- Secret double-check. Length dans le runtime = length dashboard ?
Étapes 1 et 5 attrapent 80% des cas en deux minutes.
Capturez la requête en échec
L'outil le plus utile ici : tunnel avec capture et rejeu. Pas besoin d'attendre le retry — rejouez contre le handler local pendant que vous déboguez. Instant.
Le 401 vu de chaque provider
- Stripe : 401 = échec de signature côté code. Dashboard affiche delivery failed avec le body de réponse.
- GitHub : 401 → marqué failed et retenté. Page Recent deliveries affiche la réponse.
- Shopify : 401 OK pour la sécurité mais Shopify retente. 19 échecs sur 48h → désactivation.
Voir guide debug webhook local. Rejoignez la waitlist PortPreview.