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 :
https://mon-site.com/webhook/stripe)┌─────────────┐ ┌──────────────┐
│ STRIPE │ │ VOTRE APP │
│ │ │ │
│ Paiement │ │ Serveur │
│ réussi │──── HTTP POST ──────────▶│ /webhook/... │
│ │ { event_data } │ │
│ │◀──── HTTP 200 ────────────│ Traite │
│ │ "OK" │ l'événement │
└─────────────┘ └──────────────┘
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.
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èsBé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.
Pour une boutique Shopify, les webhooks permettent de :
// 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
}
]
}
}
Les webhooks GitHub permettent d'automatiser le déploiement :
push sur la branche main → 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 }
})
Recevoir une notification instantanée quand un prospect remplit un formulaire :
| 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 |
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 :
// 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`)
}
// 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}`
}
})
}
// 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 }
})
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 })
}
Vos webhooks doivent être exposés en HTTPS :
https://mon-site.com/webhook/stripehttp://mon-site.com/webhook/stripeSinon, les données transitent en clair et peuvent être interceptées.
Le secret webhook est aussi important qu'un mot de passe :
// ❌ MAUVAIS
const secret = 'whsec_abc123...'
// ✅ BON
const secret = process.env.STRIPE_WEBHOOK_SECRET
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)
Les services externes (Stripe, Shopify) ne peuvent pas envoyer de webhooks à http://localhost:3000 car votre serveur local n'est pas accessible depuis Internet.
# 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 !
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.).
Alternative gratuite à ngrok :
cloudflared tunnel --url http://localhost:3000
Les services externes attendent une réponse rapide (HTTP 200) :
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 !
})
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.
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'
})
}
Mettez en place des alertes si :
Votre endpoint webhook doit être accessible depuis Internet :
Si votre serveur est down au moment de l'envoi :
Les webhooks peuvent arriver dans le désordre :
payment_intent.created peut arriver après payment_intent.succeededSi le service tiers a un bug ou change son format :
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
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
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
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 :
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.