Développement·

Webhooks : notification automatique en temps réel entre applications

Découvrez les webhooks, le mécanisme qui permet aux applications web de communiquer en temps réel. Exemples concrets avec Stripe, Shopify, GitHub et bonnes pratiques d'implémentation.

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

  1. Vous configurez une URL sur votre serveur (ex: https://mon-site.com/webhook/stripe)
  2. Vous enregistrez cette URL dans l'application source (Stripe, Shopify, GitHub, etc.)
  3. Quand un événement se produit, l'application source envoie une requête HTTP POST à votre URL
  4. Votre serveur reçoit la notification et traite l'événement
  5. 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ée
  • payment_intent.failed : paiement échoué → envoyer une notification
  • customer.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 branche main → déployer en production
  • pull_request ouvert → lancer les tests automatiques
  • release 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èreWebhooksAPI classique (polling)
DirectionL'app source vous envoie les donnéesVous 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ès payment_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 :

Découvrir mes projetsMe contacter