Webhooks : notification automatique en temps réel entre applications
Qu'est-ce qu'un Webhook ?
Un webhook (aussi appelé callback HTTP ou reverse API) est un mécanisme qui permet à une application web d'envoyer automatiquement des notifications en temps réel à une autre application lorsqu'un événement se produit.
Contrairement à une API classique où vous devez demander régulièrement les informations (polling), avec un webhook, l'application vous prévient directement quand quelque chose se passe.
Analogie simple :
- API classique = Vous appelez votre banque toutes les heures pour savoir si vous avez reçu un virement
- Webhook = Votre banque vous envoie un SMS dès qu'un virement arrive
Comment fonctionne un Webhook ?
Le principe
- Vous configurez une URL sur votre serveur (ex:
https://mon-site.com/webhook/stripe
) - Vous enregistrez cette URL dans l'application source (Stripe, Shopify, GitHub, etc.)
- Quand un événement se produit, l'application source envoie une requête HTTP POST à votre URL
- Votre serveur reçoit la notification et traite l'événement
- Vous répondez avec un code HTTP 200 pour confirmer la réception
Schéma technique
┌─────────────┐ ┌──────────────┐
│ STRIPE │ │ VOTRE APP │
│ │ │ │
│ Paiement │ │ Serveur │
│ réussi │──── HTTP POST ──────────▶│ /webhook/... │
│ │ { event_data } │ │
│ │◀──── HTTP 200 ────────────│ Traite │
│ │ "OK" │ l'événement │
└─────────────┘ └──────────────┘
Exemple concret : paiement Stripe
Imaginons un client qui paie une facture sur votre site :
// 1. Stripe envoie une requête POST à votre endpoint
POST https://votre-site.com/api/webhooks/stripe
Content-Type: application/json
{
"type": "payment_intent.succeeded",
"data": {
"object": {
"id": "pi_123456",
"amount": 5000, // 50€
"customer": "cus_ABC123",
"metadata": {
"invoice_id": "INV-2025-001"
}
}
}
}
// 2. Votre serveur traite l'événement
export default defineEventHandler(async (event) => {
const body = await readBody(event)
if (body.type === 'payment_intent.succeeded') {
const invoiceId = body.data.object.metadata.invoice_id
// Marquer la facture comme payée
await markInvoiceAsPaid(invoiceId)
// Envoyer un email de confirmation
await sendConfirmationEmail(invoiceId)
// Débloquer l'accès au produit
await grantAccess(invoiceId)
}
// 3. Répondre à Stripe
return { received: true }
})
Résultat : Votre application est notifiée instantanément du paiement et peut débloquer l'accès au produit en temps réel.
Cas d'usage réels
1. Stripe : encaissement de paiements
J'utilise les webhooks Stripe avec Axonaut pour gérer mes factures :
Événements écoutés :
payment_intent.succeeded
: paiement réussi → marquer la facture comme payéepayment_intent.failed
: paiement échoué → envoyer une notificationcustomer.subscription.deleted
: abonnement annulé → révoquer l'accès
Bénéfice : Les factures sont automatiquement marquées comme payées dans Axonaut dès que le client paie par carte bancaire, sans intervention manuelle.
2. Shopify : synchronisation de boutique
Pour une boutique Shopify, les webhooks permettent de :
- Synchroniser les commandes avec votre système interne
- Mettre à jour le stock en temps réel
- Envoyer des notifications personnalisées
- Déclencher des workflows d'expédition
// Webhook Shopify : nouvelle commande
{
"topic": "orders/create",
"data": {
"id": 123456,
"email": "client@example.com",
"total_price": "99.00",
"line_items": [
{
"title": "T-shirt noir",
"quantity": 2
}
]
}
}
3. GitHub : CI/CD automatique
Les webhooks GitHub permettent d'automatiser le déploiement :
push
sur la branchemain
→ déployer en productionpull_request
ouvert → lancer les tests automatiquesrelease
publié → créer une notification
// Webhook GitHub : nouveau push
export default defineEventHandler(async (event) => {
const payload = await readBody(event)
if (payload.ref === 'refs/heads/main') {
// Déclencher le déploiement
await deployToProduction()
}
return { success: true }
})
4. Formulaires de contact
Recevoir une notification instantanée quand un prospect remplit un formulaire :
- Créer le contact dans votre CRM
- Envoyer un email de confirmation
- Notifier l'équipe commerciale
- Déclencher un workflow de qualification
Webhooks vs API classique
Critère | Webhooks | API classique (polling) |
---|---|---|
Direction | L'app source vous envoie les données | Vous demandez les données à l'app |
Temps réel | ✅ Instantané | ❌ Délai (intervalle de polling) |
Consommation | ✅ Faible (1 requête par événement) | ❌ Élevée (1 requête toutes les X secondes) |
Complexité | ⚠️ Nécessite un serveur exposé | ✅ Simple à implémenter |
Fiabilité | ⚠️ Peut échouer (serveur down) | ✅ Vous contrôlez le timing |
Exemple concret : suivi de commandes
Avec API classique (polling) :
// Vous interrogez l'API toutes les 30 secondes
setInterval(async () => {
const orders = await fetch('https://api.shop.com/orders?status=new')
// 99% du temps : aucune nouvelle commande
// = beaucoup de requêtes inutiles
}, 30000)
Avec Webhook :
// L'API vous prévient uniquement quand il y a une nouvelle commande
export default defineEventHandler(async (event) => {
const newOrder = await readBody(event)
// Traitement uniquement quand nécessaire
await processOrder(newOrder)
})
Résultat :
- Polling : 2 880 requêtes/jour (1 toutes les 30s) dont 99% inutiles
- Webhook : 10 requêtes/jour (uniquement les nouvelles commandes)
Implémenter un Webhook (exemples)
Exemple 1 : Webhook Stripe (Nuxt/Node.js)
// server/api/webhooks/stripe.post.js
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
export default defineEventHandler(async (event) => {
// 1. Récupérer la signature pour vérifier l'authenticité
const sig = getHeader(event, 'stripe-signature')
const body = await readRawBody(event)
let stripeEvent
try {
// 2. Vérifier que la requête vient bien de Stripe
stripeEvent = stripe.webhooks.constructEvent(
body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
)
} catch (err) {
// Signature invalide = requête frauduleuse
throw createError({
statusCode: 400,
message: `Webhook Error: ${err.message}`
})
}
// 3. Traiter l'événement
switch (stripeEvent.type) {
case 'payment_intent.succeeded':
const paymentIntent = stripeEvent.data.object
await handlePaymentSuccess(paymentIntent)
break
case 'payment_intent.failed':
const failedPayment = stripeEvent.data.object
await handlePaymentFailed(failedPayment)
break
case 'customer.subscription.deleted':
const subscription = stripeEvent.data.object
await handleSubscriptionCanceled(subscription)
break
default:
console.log(`Unhandled event type: ${stripeEvent.type}`)
}
// 4. Répondre à Stripe
return { received: true }
})
async function handlePaymentSuccess(paymentIntent) {
const invoiceId = paymentIntent.metadata.invoice_id
// Marquer la facture comme payée
await db.invoices.update({
where: { id: invoiceId },
data: { status: 'paid', paid_at: new Date() }
})
// Envoyer un email de confirmation
await sendEmail({
to: paymentIntent.receipt_email,
template: 'payment-success',
data: { invoiceId }
})
console.log(`✅ Paiement ${paymentIntent.id} traité avec succès`)
}
Exemple 2 : Webhook GitHub (déploiement automatique)
// server/api/webhooks/github.post.js
import crypto from 'crypto'
export default defineEventHandler(async (event) => {
// 1. Vérifier la signature GitHub
const signature = getHeader(event, 'x-hub-signature-256')
const body = await readRawBody(event)
const hmac = crypto.createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET)
const digest = 'sha256=' + hmac.update(body).digest('hex')
if (signature !== digest) {
throw createError({ statusCode: 401, message: 'Invalid signature' })
}
const payload = JSON.parse(body)
// 2. Traiter l'événement
if (payload.ref === 'refs/heads/main') {
console.log('🚀 Nouveau push sur main, déploiement en cours...')
// Déclencher le déploiement
await deployToProduction()
return { status: 'deployed' }
}
return { status: 'ignored' }
})
async function deployToProduction() {
// Lancer un script de déploiement
await $fetch('https://api.vercel.com/v1/deployments', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.VERCEL_TOKEN}`
}
})
}
Exemple 3 : Webhook personnalisé (notification de formulaire)
// server/api/webhooks/contact-form.post.js
export default defineEventHandler(async (event) => {
const formData = await readBody(event)
// 1. Créer le contact dans le CRM
await createContact({
name: formData.name,
email: formData.email,
message: formData.message,
source: 'contact-form'
})
// 2. Envoyer un email de confirmation au prospect
await sendEmail({
to: formData.email,
template: 'contact-confirmation',
subject: 'Nous avons bien reçu votre message'
})
// 3. Notifier l'équipe commerciale
await sendSlackNotification({
channel: '#commercial',
message: `📧 Nouveau contact : ${formData.name} (${formData.email})`
})
return { success: true }
})
Sécurité des Webhooks
1. Vérifier la signature
IMPORTANT : N'importe qui peut envoyer une requête POST à votre endpoint. Vous devez vérifier que la requête vient bien de la source attendue.
Chaque service utilise un mécanisme de signature :
Stripe :
const sig = getHeader(event, 'stripe-signature')
const stripeEvent = stripe.webhooks.constructEvent(
body,
sig,
process.env.STRIPE_WEBHOOK_SECRET // ← Secret à protéger
)
GitHub :
const signature = getHeader(event, 'x-hub-signature-256')
const hmac = crypto.createHmac('sha256', process.env.GITHUB_SECRET)
const digest = 'sha256=' + hmac.update(body).digest('hex')
if (signature !== digest) {
throw createError({ statusCode: 401 })
}
Shopify :
const hmac = getHeader(event, 'x-shopify-hmac-sha256')
const hash = crypto
.createHmac('sha256', process.env.SHOPIFY_SECRET)
.update(body)
.digest('base64')
if (hmac !== hash) {
throw createError({ statusCode: 401 })
}
2. Utiliser HTTPS
Vos webhooks doivent être exposés en HTTPS :
- ✅
https://mon-site.com/webhook/stripe
- ❌
http://mon-site.com/webhook/stripe
Sinon, les données transitent en clair et peuvent être interceptées.
3. Protéger le secret
Le secret webhook est aussi important qu'un mot de passe :
- ✅ Stocké dans les variables d'environnement
- ✅ Jamais commité dans Git
- ❌ Jamais en dur dans le code
// ❌ MAUVAIS
const secret = 'whsec_abc123...'
// ✅ BON
const secret = process.env.STRIPE_WEBHOOK_SECRET
4. Gérer l'idempotence
Un webhook peut être envoyé plusieurs fois pour le même événement (retry en cas d'échec). Vous devez éviter de traiter 2 fois le même événement :
// Vérifier si l'événement a déjà été traité
const existingEvent = await db.webhookEvents.findUnique({
where: { stripe_event_id: stripeEvent.id }
})
if (existingEvent) {
// Déjà traité, on ignore
return { received: true }
}
// Marquer l'événement comme traité
await db.webhookEvents.create({
data: { stripe_event_id: stripeEvent.id, processed_at: new Date() }
})
// Traiter l'événement
await handleEvent(stripeEvent)
Tester les Webhooks en local
Problème
Les services externes (Stripe, Shopify) ne peuvent pas envoyer de webhooks à http://localhost:3000
car votre serveur local n'est pas accessible depuis Internet.
Solutions
1. Stripe CLI (recommandé pour Stripe)
# Installer Stripe CLI
brew install stripe/stripe-cli/stripe
# Se connecter
stripe login
# Rediriger les webhooks vers localhost
stripe listen --forward-to localhost:3000/api/webhooks/stripe
Stripe CLI redirige tous les webhooks Stripe vers votre serveur local. Parfait pour développer !
2. ngrok (universel)
ngrok crée un tunnel HTTPS vers votre localhost :
# Installer ngrok
brew install ngrok
# Créer un tunnel
ngrok http 3000
# → https://abc123.ngrok.io → localhost:3000
Utilisez l'URL ngrok dans la configuration du webhook (Shopify, GitHub, etc.).
3. Cloudflare Tunnel
Alternative gratuite à ngrok :
cloudflared tunnel --url http://localhost:3000
Bonnes pratiques
1. Répondre rapidement (< 5s)
Les services externes attendent une réponse rapide (HTTP 200) :
- ✅ Répondre immédiatement
- ✅ Traiter l'événement en asynchrone si nécessaire
export default defineEventHandler(async (event) => {
const payload = await readBody(event)
// ✅ Ajouter à une queue de traitement
await queue.add('process-webhook', payload)
// ✅ Répondre immédiatement
return { received: true }
// ❌ Ne PAS attendre le traitement complet
// await longProcessingTask(payload) // Trop long !
})
2. Logger tous les événements
Conservez une trace de tous les webhooks reçus :
// Logger l'événement
await db.webhookLogs.create({
data: {
source: 'stripe',
event_type: stripeEvent.type,
payload: stripeEvent,
received_at: new Date()
}
})
Utile pour débugger et auditer.
3. Gérer les erreurs gracieusement
Si le traitement échoue, le service va réessayer :
try {
await handleEvent(stripeEvent)
return { received: true }
} catch (error) {
// Logger l'erreur
console.error('Webhook error:', error)
// Répondre avec un code 500
// → Stripe va réessayer plus tard
throw createError({
statusCode: 500,
message: 'Processing failed'
})
}
4. Surveiller les webhooks
Mettez en place des alertes si :
- Trop d'échecs consécutifs
- Délai de traitement trop long
- Aucun webhook reçu depuis X heures (problème de configuration)
Limites des Webhooks
1. Nécessite un serveur public
Votre endpoint webhook doit être accessible depuis Internet :
- ❌ Impossible avec une app frontend uniquement (React, Vue)
- ✅ Nécessite un backend (Node.js, PHP, Python, etc.)
2. Pas de garantie de livraison
Si votre serveur est down au moment de l'envoi :
- ⚠️ Le webhook peut être perdu
- ✅ La plupart des services réessayent plusieurs fois
- ✅ Vous devez implémenter une logique de fallback
3. Ordre non garanti
Les webhooks peuvent arriver dans le désordre :
payment_intent.created
peut arriver aprèspayment_intent.succeeded
- Gérez les dépendances avec précaution
4. Dépendance au service tiers
Si le service tiers a un bug ou change son format :
- Vos webhooks peuvent casser
- Testez toujours en environnement de test
Alternatives aux Webhooks
1. Polling (API classique)
Interroger régulièrement l'API :
setInterval(async () => {
const newOrders = await fetchNewOrders()
await processOrders(newOrders)
}, 60000) // Toutes les minutes
Avantages : Simple, vous contrôlez le timing Inconvénients : Consomme des ressources, pas temps réel
2. WebSockets
Connexion bidirectionnelle persistante :
const ws = new WebSocket('wss://api.example.com')
ws.onmessage = (event) => {
const data = JSON.parse(event.data)
handleEvent(data)
}
Avantages : Temps réel, bidirectionnel Inconvénients : Plus complexe, connexion maintenue
3. Server-Sent Events (SSE)
Flux unidirectionnel du serveur vers le client :
const eventSource = new EventSource('/api/events')
eventSource.onmessage = (event) => {
console.log('Nouvel événement:', event.data)
}
Avantages : Simple, temps réel Inconvénients : Unidirectionnel uniquement
Conclusion
Les webhooks sont devenus un standard incontournable du web moderne. Ils permettent aux applications de communiquer en temps réel de manière efficace et économe en ressources.
Que vous utilisiez Stripe pour encaisser des paiements, Shopify pour gérer une boutique, GitHub pour automatiser le déploiement, ou Axonaut pour la gestion commerciale, les webhooks sont le mécanisme qui rend toutes ces intégrations possibles.
Points clés à retenir :
- ✅ Les webhooks sont des notifications automatiques envoyées par une app quand un événement se produit
- ✅ Ils sont plus efficaces que le polling pour le temps réel
- ✅ La sécurité (vérification de signature) est essentielle
- ✅ Toujours répondre rapidement (< 5s) et traiter en asynchrone si nécessaire
- ✅ Logger tous les événements pour débugger facilement
Pour aller plus loin :
Cet article vous a-t-il été utile ?
Vos retours sont complètement anonymes et m'aident à améliorer mon contenu
Qu'est-ce que Debian ?
Découvrez Debian, une distribution Linux stable et sécurisée idéale pour les débutants comme pour les utilisateurs avancés. Apprenez à l'installer et à l'utiliser avec ce guide détaillé.
Qu'est-ce qu'un développeur web ?
Découvrez en quoi consiste le métier, les compétences requises et pourquoi faire appel à un expert pour votre projet en ligne.