ITSkillsCenter
Intelligence Artificielle

Wave Business API et n8n : agent de paiement et webhooks signés

15 دقائق للقراءة

📍 Guide principal : Agents IA pour PME : architecture, déploiement et opérations en 2026

Introduction

Une fois qu’un agent IA produit des devis, l’étape naturelle est d’encaisser sans friction. Wave Business API expose une chaîne de paiement minimaliste et adaptée aux PME francophones d’Afrique de l’Ouest : créer une session de paiement, recevoir un webhook signé quand le client a payé, mettre à jour le statut interne. Bien câblée, cette chaîne tourne sans intervention humaine pour les paiements en dessous d’un seuil de risque.

Ce tutoriel construit l’intégration complète côté n8n : génération de lien de paiement à partir d’un devis validé, réception et vérification du webhook, idempotence, mise à jour du pipeline, relances automatisées en cas de non-paiement. Le contenu se concentre sur Wave ; pour Orange Money et Mixx by Yas, voir n8n workflows Mobile Money : 4 cas pratiques 2026.

Prérequis

  • Un compte Wave Business actif avec un wallet de production. La création se fait via le portail Wave Business à https://business.wave.com/.
  • Une clé API Wave (commençant par wave_sn_prod_ pour le Sénégal ou wave_ci_prod_ pour la Côte d’Ivoire). Génération depuis le portail développeur Wave après ouverture du wallet.
  • Une instance n8n version 2.0 ou ultérieure, accessible publiquement en HTTPS pour recevoir les webhooks. Voir n8n self-hosted 2026 : guide complet.
  • Une base Postgres pour stocker les event_id traités (idempotence) et les statuts de paiement.
  • Niveau attendu : intermédiaire — n8n, HTTP, notions HMAC-SHA256.
  • Temps estimé : 4 à 6 heures pour une intégration de bout en bout testée.

Étape 1 — Comprendre les endpoints Wave et leur sécurité

L’API Wave est documentée à https://docs.wave.com/business. Trois familles d’endpoints comptent pour un agent de paiement.

Checkout Sessions : POST https://api.wave.com/v1/checkout/sessions crée une session de paiement avec montant et URLs de retour. La réponse contient un wave_launch_url à envoyer au client, qui ouvre le tunnel de paiement Wave.

Balance : GET https://api.wave.com/v1/balance retourne le solde du wallet pour la réconciliation comptable. Utile en fin de journée pour comparer la somme des paiements reçus avec le solde réel.

Webhooks : Wave POST sur l’URL configurée dans le portail à chaque événement (checkout.session.completed, checkout.session.payment_failed, etc.). Le payload est signé avec un secret partagé via l’en-tête Wave-Signature au format t=<timestamp>,v1=<signature> où la signature est un HMAC-SHA256 hex du timestamp et du body.

L’authentification de tous les appels sortants se fait par Authorization: Bearer <api_key>. La clé est sensible — elle peut déplacer de l’argent. Stocker dans les credentials n8n, jamais en dur dans un workflow exporté.

Côté devises, XOF (franc CFA UEMOA) et XAF (franc CFA CEMAC) ne supportent pas les décimales — les montants sont des entiers en unités. Un montant de 5 000 FCFA s’envoie sous forme de chaîne "5000", pas "5000.00".

Étape 2 — Créer la credential Wave dans n8n

Dans n8n, ajouter une credential de type HTTP Header Auth :

  • Nom : Wave Business Production
  • Header Name : Authorization
  • Header Value : Bearer wave_sn_prod_VOTRECLEICI

Pour les tests, créer une seconde credential Wave Business Sandbox pointant vers la clé sandbox correspondante. Toujours développer en sandbox d’abord — chaque appel mal câblé en production crée une session orpheline ou un mouvement comptable indésirable.

Tester la credential avec un nœud HTTP Request en GET sur https://api.wave.com/v1/balance ; la réponse doit contenir un objet currency et amount. Si une erreur 401 apparaît, la clé est invalide ou non activée.

Étape 3 — Générer un lien de paiement à la volée

Le déclencheur est typiquement la validation d’un devis (sortie du tutoriel précédent) ou un webhook depuis le panier d’une boutique. Pour ce tutoriel, on déclenche manuellement avec un nœud Manual Trigger qui passe les paramètres devis_id, montant_xof, client_email.

Ajouter un nœud HTTP Request configuré ainsi :

  • Méthode : POST
  • URL : https://api.wave.com/v1/checkout/sessions
  • Authentication : Generic Credential Type → HTTP Header Auth → Wave Business Production
  • Body Content Type : JSON
  • Body :
{
  "amount": "{{ $json.montant_xof }}",
  "currency": "XOF",
  "error_url": "https://votre-site.example/paiement/erreur?devis={{ $json.devis_id }}",
  "success_url": "https://votre-site.example/paiement/ok?devis={{ $json.devis_id }}",
  "client_reference": "DEVIS-{{ $json.devis_id }}"
}

Le champ client_reference est une chaîne libre que Wave renvoie dans le webhook. C’est lui qu’on utilisera pour rattacher le paiement au devis interne. Sans ce champ, on est obligé de matcher par montant, ce qui casse dès qu’il y a deux devis du même montant en attente.

La réponse contient un id (identifiant de session Wave), un wave_launch_url (URL à donner au client) et un status initial à open. Stocker ces trois champs dans la table interne paiements avec la référence au devis.

Tester en sandbox d’abord. La sandbox Wave permet de simuler un paiement réussi en cliquant sur un bouton dans l’interface de la session — pas besoin de vrai compte client. Si la création échoue avec une erreur 422, vérifier que le montant est une chaîne sans décimales et que les URLs sont en HTTPS.

Étape 4 — Envoyer le lien au client

Le wave_launch_url est la pièce maîtresse à transmettre. Trois canaux selon le contexte.

Par e-mail via un nœud Send Email : message court avec le lien et le montant, expéditeur sur l’adresse pro de l’entreprise. Garder le mail simple — beaucoup de filtres anti-spam taggent négativement les mails avec liens vers des domaines de paiement quand l’expéditeur n’est pas authentifié SPF/DKIM/DMARC.

Par WhatsApp via Meta Cloud API : message texte avec le lien et un visuel optionnel. Taux d’ouverture supérieur à 90 %, mais nécessite un template approuvé pour l’envoi initiateur (catégorie utility chez Meta).

Par SMS via un fournisseur africain (Twilio, MessageBird, Africa’s Talking, Orange) : reach maximal mais coût plus élevé et lien parfois cassé sur les vieux téléphones. À réserver aux clients sans WhatsApp.

Côté traçabilité, inscrire dans la table paiements le canal d’envoi et l’horodatage. Cette donnée alimentera plus tard l’analyse de conversion : quels canaux convertissent le mieux à quelle heure de la journée.

Étape 5 — Recevoir le webhook et vérifier la signature

Wave POST sur l’URL configurée dans le portail à chaque changement d’état d’une session. Cette étape est critique : un webhook accepté sans vérification permet à un attaquant qui devine l’URL d’injecter de faux paiements.

Configurer le webhook côté Wave dans le portail développeur, en pointant vers une URL n8n du type https://votre-n8n.example.com/webhook/wave. Wave fournit un secret partagé (Webhook Secret) qui sera utilisé pour signer chaque appel.

Côté n8n, créer un workflow wave-webhook avec un trigger Webhook configuré ainsi :

  • HTTP Method : POST
  • Path : wave
  • Response Mode : Response Node (réponse différée)
  • Authentication : None (la vérification se fait dans le workflow)
  • Raw Body : ON — option indispensable pour récupérer le corps brut nécessaire au calcul HMAC

Avec l’option Raw Body activée, n8n expose le body brut comme chaîne directement dans $json.body (selon la version, $json.bodyRaw ou via la propriété binaire — le code ci-dessous gère les deux cas). La signature Wave est calculée sur la concaténation directe timestamp + raw_body, sans aucun séparateur. Toute reformatation du JSON (espaces, ordre des clés) casse la vérification. C’est le piège numéro un des intégrations webhook signées.

Ajouter immédiatement après le Webhook un nœud Code qui vérifie la signature :

const crypto = require('crypto');
const SECRET = $env.WAVE_WEBHOOK_SECRET;

const item = $input.first();
const headers = item.json.headers || {};
const sigHeader = headers['wave-signature'] || '';

// Le nœud Webhook DOIT avoir l'option "Raw Body" activée.
// Le body brut arrive comme chaîne dans $json.body (ou en binaire selon version n8n).
let rawBody = item.json.body;
if (typeof rawBody !== 'string') {
  const bin = item.binary?.data;
  if (!bin) {
    throw new Error('Raw Body indisponible — activer l\'option sur le nœud Webhook');
  }
  rawBody = Buffer.from(bin.data, 'base64').toString('utf8');
}

const parts = Object.fromEntries(sigHeader.split(',').map(p => p.split('=')));
const ts = parts.t;
const sig = parts.v1;

if (!ts || !sig) {
  throw new Error('Wave-Signature header malformé');
}

// Wave concatène timestamp + body brut SANS séparateur
const payload = ts + rawBody;
const expected = crypto.createHmac('sha256', SECRET).update(payload).digest('hex');

const expectedBuf = Buffer.from(expected, 'hex');
const sigBuf = Buffer.from(sig, 'hex');
if (expectedBuf.length !== sigBuf.length || !crypto.timingSafeEqual(expectedBuf, sigBuf)) {
  throw new Error('Signature Wave invalide');
}

const ageSec = Math.floor(Date.now() / 1000) - Number(ts);
if (ageSec > 300) {
  throw new Error('Webhook trop ancien (rejet anti-replay)');
}

// Re-parser le body pour la suite du workflow
return [{ json: JSON.parse(rawBody) }];

La vérification HMAC-SHA256 confirme que le webhook vient bien de Wave. Trois précautions critiques : on signe sur le body BRUT en concaténation directe (jamais le JSON re-sérialisé et jamais avec un séparateur ajouté, sinon le hash diverge), on rejette les webhooks de plus de 300 secondes pour fermer la fenêtre de replay, et on compare les signatures avec timingSafeEqual pour bloquer les attaques par mesure de temps.

Si la vérification échoue, retourner 400 immédiatement — pas 401 (qui inviterait à retenter avec d’autres clés) et pas 500 (qui déclencherait des retries Wave). Côté Response Node, configurer le code 400.

Étape 6 — Idempotence et persistance

Wave retransmet un webhook si la réponse n’arrive pas en moins de 30 secondes ou si elle contient un code d’erreur 5xx. Sans précaution, le même paiement peut être traité deux fois.

Après la vérification de signature, ajouter un nœud Postgres en INSERT sur la table evenements_wave :

CREATE TABLE evenements_wave (
    event_id TEXT PRIMARY KEY,
    type_evenement TEXT NOT NULL,
    payload JSONB NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

Configurer le nœud Postgres en mode Insert avec On Conflict à do nothing sur la clé event_id. Si le webhook arrive deux fois, le second insert ne fait rien et un nœud IF en aval bifurque vers une réponse 200 immédiate.

INSERT INTO evenements_wave (event_id, type_evenement, payload)
VALUES ($1, $2, $3)
ON CONFLICT (event_id) DO NOTHING
RETURNING event_id;

Si RETURNING rend une ligne, c’est un nouvel événement à traiter. Si rien n’est retourné, c’est un doublon — répondre 200 sans rien faire d’autre.

Étape 7 — Mettre à jour le statut métier

Pour les événements neufs, brancher selon le type reçu.

Pour checkout.session.completed, l’événement contient le client_reference (notre référence devis) et le montant payé. Mettre à jour la table paiements à payé, mettre à jour le devis à encaissé, déclencher l’envoi de la facture par e-mail, et — si le paiement déclenche un workflow logistique (livraison, accès produit) — appeler le workflow concerné.

Pour checkout.session.payment_failed, marquer le paiement à échec et notifier le commercial. Ne pas annuler le devis automatiquement — le client peut retenter avec un autre canal.

Pour checkout.session.expired, marquer à expiré. Si le devis est encore actif, optionnellement régénérer un nouveau lien et relancer le client.

Toujours répondre 200 à Wave une fois le traitement terminé. Si le traitement métier échoue (par exemple Postgres indisponible momentanément), répondre 500 — Wave réessaiera. Mais traiter avec parcimonie : une erreur 500 systématique génère des retries en boucle.

Étape 8 — Réconciliation quotidienne

Les webhooks peuvent rater (DNS, panne réseau, n8n redémarré au mauvais moment). La sécurité de fin est la réconciliation : chaque nuit, lister via API les sessions des dernières 24 heures et vérifier que chaque paiement encaissé a bien sa contrepartie en base.

Wave expose GET https://api.wave.com/v1/checkout/sessions?from=...&to=... pour lister les sessions sur une période. Créer un workflow planifié quotidien à 3 h du matin qui pagine ce listing, croise avec la table interne, et alerte sur les écarts.

L’écart typique : un paiement marqué succeeded côté Wave mais absent côté base interne. Récupération automatique : insérer la ligne manquante dans paiements et evenements_wave, déclencher le post-traitement (envoi facture, etc.). Logger l’incident dans une table incidents_paiement pour analyse.

Étape 9 — Relances en cas de non-paiement

Un lien envoyé n’est pas un paiement. Pour les devis qui n’ont pas été payés sous 48 heures, envoyer une relance polie ; sous 7 jours, marquer le devis comme abandonné et proposer un nouveau lien.

Workflow planifié relance-paiement avec Schedule Trigger sur cron 0 10 * * *. Logique :

  1. SELECT des paiements pending créés il y a 48 à 72 heures, sans relance envoyée.
  2. Pour chacun : envoi WhatsApp ou e-mail de relance avec le même wave_launch_url (toujours valide tant que la session n’est pas expirée).
  3. UPDATE de la table avec derniere_relance_le = NOW().

Wave ferme automatiquement les sessions Checkout au bout de 30 jours. Avant cette échéance, créer une nouvelle session si le client a manifesté un intérêt récent ; sinon laisser expirer.

Erreurs fréquentes

Erreur Cause Solution
Création de session retourne 422 Montant avec décimales pour XOF, ou URLs non HTTPS Forcer le montant en chaîne entière, vérifier les URLs
Webhook reçu avec signature invalide WAVE_WEBHOOK_SECRET faux ou body modifié par un middleware Vérifier le secret côté n8n et désactiver les middlewares qui réécrivent le body
Doubles paiements en base Idempotence absente Stocker event_id avec contrainte UNIQUE et bifurquer en début de workflow
Webhooks Wave en retry permanent Réponse 500 systématique côté n8n Capturer les erreurs métier et répondre 200 quand l’événement a été reçu et stocké
Le client clique mais n’arrive pas à payer URL d’erreur ou de succès en HTTP au lieu de HTTPS Wave exige HTTPS pour les URLs de retour
La réconciliation détecte des écarts récurrents Webhooks bloqués en pare-feu Mettre en allowlist les plages IP Wave documentées dans le portail développeur

FAQ

Combien Wave Business prélève-t-il sur les paiements reçus ?
Les frais Wave Business sont publiés sur le portail Wave et varient selon le pays et le volume mensuel — typiquement autour de 1 % par paiement reçu en 2026, avec des paliers dégressifs au-delà de certains volumes. Vérifier la grille tarifaire courante directement chez Wave avant de mettre en production.

Wave fonctionne-t-il pour les paiements internationaux ?
Wave Business est centré sur les paiements en XOF (UEMOA) et XAF (CEMAC), reçus de comptes Wave dans la même zone. Pour des clients hors zone, prévoir un fallback Stripe ou Lygos selon la cible.

Comment tester sans vrai compte ?
La sandbox Wave fournit des clés wave_xx_test_... et un environnement complet où on peut simuler des paiements. Le webhook arrive sur la même URL que la production, avec le secret sandbox approprié.

Que faire si on perd la clé API ?
La régénérer immédiatement depuis le portail Wave. L’ancienne clé est révoquée, et il faut mettre à jour la credential côté n8n. Aucun mouvement comptable n’est compromis tant que la clé n’a pas été utilisée par un tiers.

Peut-on programmer des paiements récurrents (abonnements) avec Wave ?
Wave Business est centré sur les paiements one-shot via Checkout. Pour de l’abonnement, deux approches : programmer un workflow n8n qui génère une nouvelle session chaque mois et envoie le lien au client, ou utiliser une plateforme de tokenisation tierce. La première approche est légère mais demande l’action explicite du client à chaque échéance.

Tutoriels associés

Ressources

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité