???? Kadryza est en version b??ta. Cr??ez votre compte gratuitement ???

Webhooks

Les webhooks vous permettent de recevoir des notifications en temps réel lorsque le statut d’une transaction change. C’est le mécanisme recommandé pour savoir quand un paiement a été confirmé, refusé ou expiré.


Pourquoi les webhooks (et pas le polling)

Le statut d’une transaction Mobile Money change de façon asynchrone. L’utilisateur confirme (ou refuse) le paiement depuis son téléphone, ce qui peut prendre de quelques secondes à 5 minutes.

ApprocheMéthodeProblème
PollingGET /v1/transactions/:id toutes les 2sGaspille vos requêtes API, surcharge votre serveur, latence de détection
WebhookKadryza vous notifie dès que le statut changeTemps réel, zéro requête inutile, fiable
❌ Polling (à ne pas faire) :
┌──────────┐  GET /v1/transactions/:id  ┌──────────┐
│  Votre   │ ─────────────────────────▶ │ Kadryza  │
│  serveur │ ◀───────── PENDING ─────── │   API    │
│          │ ─────────────────────────▶ │          │
│          │ ◀───────── PENDING ─────── │          │
│          │ ─────────────────────────▶ │          │
│          │ ◀───────── SUCCESS ─────── │          │
└──────────┘    (3 requêtes gaspillées) └──────────┘

✅ Webhook (recommandé) :
┌──────────┐                            ┌──────────┐
│  Votre   │                            │ Kadryza  │
│  serveur │ ◀── POST transaction.success│   API    │
│          │ ──── 200 OK ──────────────▶ │          │
└──────────┘     (1 seule requête)       └──────────┘

Configuration

Ajouter un endpoint webhook

  1. Connectez-vous au dashboard Kadryza
  2. Accédez à WebhooksAjouter un endpoint
  3. Entrez l’URL de votre endpoint (ex: https://votre-site.com/webhooks/kadryza)
  4. Copiez le secret de signature généré
🔒

Votre endpoint webhook doit utiliser HTTPS en production. Kadryza ne livre pas de webhooks vers des URL HTTP non sécurisées.


Événements disponibles

Kadryza émet 3 types d’événements webhook de production, plus un événement de test :

ÉvénementStatut associéQuand il est émis
transaction.successSUCCESSLe payeur a confirmé le paiement sur son téléphone
transaction.failedFAILEDLe paiement a été refusé (solde insuffisant, annulation, erreur opérateur)
transaction.timeoutTIMEOUTLe payeur n’a pas répondu dans les 5 minutes
transaction.testSUCCESSEnvoyé manuellement depuis le dashboard pour vérifier la connectivité
ℹ️

Le statut EXPIRED (expiration côté passerelle) n’émet pas de webhook. Pour détecter ce cas, interrogez périodiquement l’API (GET /v1/transactions/:id) pour les transactions restées en PENDING au-delà de leur expires_at.


Format du payload

Chaque webhook est une requête POST envoyée à votre endpoint avec le payload JSON suivant.

transaction.success

Payload — transaction.success
{
  "event": "transaction.success",
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "reference": "order_2025_001",
    "internal_ref": "KADRYZA-A1B2C3D4",
    "amount": 15000,
    "currency": "XAF",
    "operator": "AIRTEL",
    "phone_number": "+23566000000",
    "description": "Abonnement mensuel Premium",
    "status": "SUCCESS",
    "created_at": "2025-06-15T14:30:00Z",
    "updated_at": "2025-06-15T14:31:12Z"
  },
  "timestamp": "2025-06-15T14:31:12Z"
}

transaction.failed

Payload — transaction.failed
{
  "event": "transaction.failed",
  "data": {
    "id": "c3d4e5f6-a7b8-9012-cdef-234567890123",
    "reference": "order_2025_002",
    "internal_ref": "KADRYZA-C3D4E5F6",
    "amount": 50000,
    "currency": "XAF",
    "operator": "MOOV",
    "phone_number": "+23599000000",
    "description": "Achat équipement",
    "status": "FAILED",
    "created_at": "2025-06-15T15:00:00Z",
    "updated_at": "2025-06-15T15:00:45Z"
  },
  "timestamp": "2025-06-15T15:00:45Z"
}

transaction.timeout

Payload — transaction.timeout
{
  "event": "transaction.timeout",
  "data": {
    "id": "d4e5f6a7-b8c9-0123-defa-345678901234",
    "reference": "order_2025_003",
    "internal_ref": "KADRYZA-D4E5F6A7",
    "amount": 3000,
    "currency": "XAF",
    "operator": "AIRTEL",
    "phone_number": "+23566111111",
    "description": "Crédit téléphonique",
    "status": "TIMEOUT",
    "created_at": "2025-06-15T16:00:00Z",
    "updated_at": "2025-06-15T16:05:00Z"
  },
  "timestamp": "2025-06-15T16:05:00Z"
}

Headers HTTP du webhook

Chaque requête webhook inclut les headers suivants :

HeaderValeurDescription
Content-Typeapplication/jsonFormat du payload
X-Kadryza-Signaturesha256=<hmac_hex>Signature HMAC-SHA256 du payload
X-Kadryza-Eventtransaction.successType d’événement
X-Kadryza-Delivery-IdUUIDID unique de cette livraison (utile pour l’idempotence)
User-AgentKadryza-Webhook/1.0Identifiant de l’expéditeur

Vérification de signature

🔐

Obligatoire — Vérifiez TOUJOURS la signature avant de traiter un webhook. Sans cette vérification, un attaquant pourrait envoyer de fausses notifications à votre endpoint et simuler des paiements réussis.

Algorithme

  1. Kadryza calcule un HMAC-SHA256 du body brut du webhook avec votre secret de signature
  2. Le résultat est préfixé par sha256= et placé dans le header X-Kadryza-Signature
  3. Votre serveur recalcule le HMAC avec le même secret et compare les deux valeurs
HMAC-SHA256(webhook_secret, raw_body) → hex_digest
"sha256=" + hex_digest → signature attendue
Comparer avec X-Kadryza-Signature (timing-safe)

Implémentation

verify-sdk.js
import Kadryza from '@kadryza/sdk'
import express from 'express'
 
const app = express()
const kadryza = new Kadryza({
  apiKey: process.env.KADRYZA_API_KEY
})
 
app.post('/webhooks/kadryza', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-kadryza-signature']
  const payload = req.body.toString()
 
  const isValid = kadryza.webhooks.verifySignature({
    payload,
    signature,
    secret: process.env.KADRYZA_WEBHOOK_SECRET
  })
 
  if (!isValid) {
    console.error('❌ Signature invalide — webhook rejeté')
    return res.status(401).json({ error: 'Signature invalide' })
  }
 
  // Signature valide — traiter l'événement
  res.status(200).json({ received: true })
 
  const event = JSON.parse(payload)
  console.log('✅ Webhook vérifié:', event.event, '—', event.data.reference)
})
⚠️

Piège courant avec Express.js — Si vous utilisez express.json() comme middleware global, il parse le body avant votre handler webhook. Le body brut est alors perdu et la vérification de signature échoue. Utilisez express.raw({ type: 'application/json' }) spécifiquement sur votre route webhook.


Retry policy (politique de réessai)

Si votre endpoint ne répond pas avec un code HTTP 2xx, Kadryza réessaie automatiquement la livraison du webhook.

Calendrier des tentatives

TentativeDélai après l’échec précédentDélai total depuis le premier envoi
1ère (initial)0
2ème1 minute1 minute
3ème5 minutes6 minutes
4ème30 minutes36 minutes
5ème (dernière)2 heures2h 36min

Qu’est-ce qu’un échec ?

SituationConsidéré comme
Réponse HTTP 200299✅ Succès — plus de retry
Réponse HTTP 3xx (redirection)❌ Échec — retry
Réponse HTTP 4xx (erreur client)❌ Échec — retry
Réponse HTTP 5xx (erreur serveur)❌ Échec — retry
Timeout (pas de réponse en 10s)❌ Échec — retry
Erreur DNS / connexion refusée❌ Échec — retry
💡

Après 5 tentatives échouées, le webhook est marqué comme non délivré dans votre dashboard. Vous pouvez le relancer manuellement depuis WebhooksHistorique.


Bonnes pratiques

1. Répondre 200 immédiatement

Votre endpoint doit répondre HTTP 200 le plus vite possible (idéalement en moins de 500ms). Traitez l’événement après avoir répondu.

Bon exemple
app.post('/webhooks/kadryza', handler, (req, res) => {
  // ✅ Répondre immédiatement
  res.status(200).json({ received: true })
 
  // Puis traiter en async
  processEvent(req.event)
})
Mauvais exemple
app.post('/webhooks/kadryza', handler, async (req, res) => {
  // ❌ NE PAS faire ça — le traitement prend trop de temps
  await updateDatabase(req.event)
  await sendEmail(req.event)
  await notifySlack(req.event)
 
  // Kadryza a peut-être déjà timeout et va retry
  res.status(200).json({ received: true })
})

2. Gérer l’idempotence

Le même webhook peut arriver plusieurs fois (retry, doublon réseau). Utilisez le header X-Kadryza-Delivery-Id pour dédupliquer.

Idempotence webhook
app.post('/webhooks/kadryza', handler, async (req, res) => {
  res.status(200).json({ received: true })
 
  const deliveryId = req.headers['x-kadryza-delivery-id']
 
  // Vérifier si déjà traité
  const existe = await db.webhookDeliveries.findOne({ id: deliveryId })
  if (existe) {
    console.log('Webhook déjà traité, ignoré')
    return
  }
 
  // Marquer comme traité AVANT de traiter (éviter les races)
  await db.webhookDeliveries.insert({ id: deliveryId, processed_at: new Date() })
 
  // Traiter
  await processEvent(req.event)
})

3. Vérifier la signature (toujours)

Ne faites jamais confiance au contenu d’un webhook sans vérifier la signature. Voir la section vérification de signature ci-dessus.

4. Logger tous les webhooks

Conservez un log de tous les webhooks reçus (payload, signature, statut de traitement). C’est indispensable pour le débogage.

Logging
async function processWebhook(event, deliveryId) {
  // Logger l'événement brut
  await db.webhookLogs.insert({
    delivery_id: deliveryId,
    event_type: event.event,
    payload: event,
    received_at: new Date(),
    processed: false
  })
 
  try {
    await handleEvent(event)
    await db.webhookLogs.updateOne(
      { delivery_id: deliveryId },
      { processed: true, processed_at: new Date() }
    )
  } catch (error) {
    await db.webhookLogs.updateOne(
      { delivery_id: deliveryId },
      { processed: false, error: error.message }
    )
    // Ne pas throw — on a déjà répondu 200
    console.error('Erreur traitement webhook:', error)
  }
}

Tester les webhooks localement

En développement, votre localhost n’est pas accessible depuis Internet. Utilisez ngrok pour exposer votre serveur local.

Installation et utilisation de ngrok

Terminal — Installation
# macOS
brew install ngrok
 
# Windows (chocolatey)
choco install ngrok
 
# Linux (snap)
snap install ngrok
Terminal — Lancer le tunnel
# Démarrer votre serveur sur le port 3000
node webhook-server.js
 
# Dans un autre terminal, exposer le port 3000
ngrok http 3000

ngrok affiche une URL publique temporaire :

Session Status   online
Forwarding       https://a1b2c3d4.ngrok-free.app → http://localhost:3000

Configurer l’URL dans le dashboard

  1. Copiez l’URL ngrok : https://a1b2c3d4.ngrok-free.app/webhooks/kadryza
  2. Dans le dashboard, accédez à WebhooksAjouter un endpoint
  3. Collez l’URL ngrok comme endpoint
  4. Initiez une transaction de test
⚠️

L’URL ngrok change à chaque redémarrage (version gratuite). Pensez à mettre à jour l’URL dans le dashboard après chaque redémarrage de ngrok.

Vérifier la réception

Terminal — Logs de votre serveur
🚀 Webhook handler actif sur le port 3000
 Webhook vérifié: transaction.success order_2025_001

Vous pouvez aussi consulter l’inspecteur ngrok sur http://localhost:4040 pour voir les requêtes entrantes, les headers et les payloads en temps réel.


Tester vos webhooks

Avant de passer en production, vous pouvez vérifier que votre endpoint reçoit et traite correctement les webhooks grâce à l’événement transaction.test.

L’événement transaction.test

Contrairement aux événements de production (transaction.success, transaction.failed, transaction.timeout), l’événement transaction.test est envoyé à la demande depuis le dashboard. Il contient un payload réaliste avec des données fictives, signé avec votre vrai secret de webhook.

🧪

L’événement transaction.test est identique en structure aux événements de production. La seule différence est le champ event ("transaction.test") et le header additionnel X-Kadryza-Test: true.

Payload complet

Payload — transaction.test
{
  "event": "transaction.test",
  "data": {
    "id": "f8a1b2c3-d4e5-6789-abcd-ef0123456789",
    "reference": "test_reference_001",
    "amount": 1000,
    "currency": "XAF",
    "operator": "AIRTEL",
    "phone_number": "+23566000000",
    "status": "SUCCESS",
    "confirmed_at": null
  },
  "timestamp": "2025-06-15T12:00:00Z"
}

Headers HTTP

HeaderValeurDescription
Content-Typeapplication/jsonFormat du payload
X-Kadryza-Signaturesha256=<hmac_hex>Signature HMAC-SHA256 (même algorithme qu’en production)
X-Kadryza-TesttrueSpécifique aux tests — indique que c’est un webhook de test
User-AgentKadryza-Webhook/1.0Identifiant de l’expéditeur

Déclencher un test depuis le dashboard

  1. Connectez-vous au dashboard Kadryza
  2. Accédez à Webhooks
  3. Repérez l’endpoint que vous voulez tester
  4. Cliquez sur le bouton Tester (icône de flèche)
  5. Le dashboard envoie immédiatement un transaction.test à votre URL
  6. Le résultat s’affiche : ✅ succès (HTTP 2xx reçu) ou ❌ échec (timeout, erreur HTTP)
⚠️

Le test webhook n’est pas réessayé automatiquement. Si votre endpoint ne répond pas, corrigez le problème et relancez le test manuellement.

Gérer transaction.test dans votre code

Votre handler webhook doit reconnaître l’événement transaction.test et le traiter sans effet de bord sur vos données de production :

Gestion de transaction.test
app.post('/webhooks/kadryza', handler, (req, res) => {
  res.status(200).json({ received: true })
 
  const event = JSON.parse(req.body.toString())
 
  // Détecter les webhooks de test
  if (event.event === 'transaction.test') {
    console.log('🧪 Webhook de test reçu — connectivité OK')
    // NE PAS mettre à jour votre base de données
    // NE PAS envoyer d'emails ou de notifications
    return
  }
 
  // Traiter les événements de production normalement
  switch (event.event) {
    case 'transaction.success':
      // Marquer la commande comme payée
      break
    case 'transaction.failed':
      // Notifier l'utilisateur
      break
    case 'transaction.timeout':
      // Proposer de réessayer
      break
  }
})

Avec le SDK :

Gestion avec @kadryza/sdk
import type { WebhookEventType } from '@kadryza/sdk'
 
function handleWebhookEvent(eventType: WebhookEventType, data: any) {
  switch (eventType) {
    case 'transaction.test':
      console.log('🧪 Test webhook reçu')
      break
    case 'transaction.success':
      // Logique de production
      break
    case 'transaction.failed':
    case 'transaction.timeout':
      // Logique de production
      break
  }
}

Récapitulatif

AspectDétail
Événements productiontransaction.success, transaction.failed, transaction.timeout
Événement testtransaction.test (déclenché manuellement depuis le dashboard)
MéthodePOST vers votre URL
FormatJSON avec event, data, timestamp
SignatureHMAC-SHA256 dans X-Kadryza-Signature
Retry5 tentatives : immédiat → 1min → 5min → 30min → 2h
Timeout10 secondes pour répondre
ProtocoleHTTPS obligatoire en production