ITSkillsCenter
Business Digital

Paiement Wave à la livraison : tutoriel intégration coursier 2026

12 min de lecture

📍 Article principal : Stack logistique PME 2026

Introduction

Une PME de livraison alimentaire à Dakar acceptait initialement uniquement le paiement en espèces. Trois problèmes récurrents : les coursiers transportaient en moyenne 50 000 FCFA d’encaisse en fin de journée, les agressions sont devenues un risque sérieux, et la réconciliation des espèces remises au siège prenait deux heures de comptabilité quotidienne. La mise en place du paiement Wave à la livraison via QR code dynamique a transformé l’opération : 70 % des paiements basculent en Wave en six mois, le coursier ne porte plus que 15 000 FCFA en espèces résiduelles, les agressions deviennent rares, et la comptabilité quotidienne se réduit à 30 minutes. Ce tutoriel décrit l’implémentation : génération du QR code Wave dynamique, affichage côté coursier, vérification serveur via webhook, réconciliation, et patterns de sécurité pour les coursiers.

Prérequis

  • Compte marchand Wave Direct validé par Wave
  • API backend Hono ou Node fonctionnelle
  • App coursier (PWA ou native) qui peut afficher un QR code
  • Endpoint webhook public HTTPS pour recevoir les notifications Wave
  • Niveau : intermédiaire avancé — Temps : 2 heures 30

Étape 1 — Générer un QR code Wave dynamique

Wave Direct expose une API qui crée des sessions de paiement avec montant pré-rempli. Au moment de la livraison, le coursier appuie sur « Encaisser via Wave » dans son app. L’app appelle l’API serveur qui crée une session Wave et retourne l’URL de paiement. Cette URL est encodée en QR code que le coursier affiche à son écran. Le client scanne avec son app Wave, confirme le montant, valide. Wave envoie le paiement au compte marchand de la PME et notifie via webhook.

// API endpoint qui initie le paiement
app.post('/api/courses/:id/encaisser-wave', requireCoursier, async (c) => {
  const courseId = c.req.param('id');
  const course = await chargerCourse(courseId);
  if (course.statut !== 'arrivee') return c.json({ erreur: 'Course non terminée' }, 400);

  const tentativeId = ulid();
  const r = await fetch('https://api.wave.com/v1/checkout/sessions', {
    method: 'POST',
    headers: { 'Authorization': 'Bearer ' + c.env.WAVE_API_KEY, 'Content-Type': 'application/json', 'Idempotency-Key': tentativeId },
    body: JSON.stringify({
      amount: String(course.montantTotal),
      currency: 'XOF',
      success_url: 'https://example.sn/livraison/' + courseId + '/succes',
      error_url: 'https://example.sn/livraison/' + courseId + '/erreur',
      client_reference: tentativeId
    })
  });
  const session = await r.json();

  await db.insert(tentatives).values({
    id: tentativeId, courseId, pspNom: 'wave',
    pspReference: session.id, montantXof: course.montantTotal, statut: 'initiee'
  });

  return c.json({ url: session.wave_launch_url, tentativeId });
});

Côté frontend coursier, on affiche le QR code via une bibliothèque légère comme qrcode. Le coursier voit son écran avec le QR code, le client scanne, et le paiement se déroule sans manipulation supplémentaire côté coursier. Cette simplicité est essentielle — le coursier ne doit pas avoir à comprendre les détails techniques.

Étape 2 — Affichage côté app coursier

<script lang="ts">
  import QRCode from 'qrcode';

  let { course } = $props();
  let qrUrl = $state('');
  let statut = $state<'idle' | 'genere' | 'paye' | 'echec'>('idle');
  let canvas: HTMLCanvasElement;

  async function encaisser() {
    statut = 'idle';
    const r = await fetch(`/api/courses/${course.id}/encaisser-wave`, { method: 'POST' });
    const { url, tentativeId } = await r.json();
    await QRCode.toCanvas(canvas, url);
    statut = 'genere';

    // Polling pour détecter le paiement réussi
    const interval = setInterval(async () => {
      const v = await fetch(`/api/tentatives/${tentativeId}/statut`);
      const data = await v.json();
      if (data.statut === 'reussie') {
        clearInterval(interval);
        statut = 'paye';
      } else if (data.statut === 'echouee') {
        clearInterval(interval);
        statut = 'echec';
      }
    }, 2000);
  }
</script>

<canvas bind:this={canvas}></canvas>
{#if statut === 'idle'}<button onclick={encaisser}>Encaisser via Wave</button>{/if}
{#if statut === 'paye'}<p class="success">✅ Paiement reçu</p>{/if}
{#if statut === 'echec'}<p class="erreur">❌ Paiement échoué</p>{/if}

Le polling toutes les 2 secondes vérifie le statut côté backend. Quand le webhook Wave a confirmé le paiement, le backend met à jour le statut et le frontend coursier voit immédiatement la confirmation. Pour optimiser, on pourrait remplacer le polling par SSE comme expliqué dans le tutoriel tracking SSE.

Étape 3 — Webhook serveur

Le webhook reçoit la notification de Wave dès que le paiement est validé côté banque. Il vérifie la signature HMAC, met à jour le statut de la tentative, et marque la course comme payée. Le pattern complet (idempotence, signature, transaction) est détaillé dans notre tutoriel Hono + Wave webhooks. Pour le contexte logistique, on ajoute en plus une notification push à l’app coursier pour qu’il voie immédiatement la confirmation, et un SMS au destinataire avec le reçu.

app.post('/api/webhooks/wave', async (c) => {
  // ... vérification signature et idempotence (voir tutoriel webhooks) ...

  if (paiementValide) {
    await db.transaction(async (tx) => {
      await tx.update(tentatives).set({ statut: 'reussie' }).where(eq(tentatives.id, tentativeId));
      await tx.update(courses).set({ statut: 'livree-payee' }).where(eq(courses.id, courseId));
    });

    // Notifier le coursier
    pushNotification(coursierId, { titre: 'Paiement reçu', message: 'Course #' + course.numero });
    // SMS reçu au destinataire
    envoyerSms(course.destinataire.telephone, 'Reçu Wave : ' + course.montantTotal + ' FCFA');
  }
  return c.json({ ok: true });
});

Étape 4 — Annulation et timeout

Si le client met trop de temps à payer (problème de réseau, hésitation), le coursier doit pouvoir annuler la tentative et basculer sur paiement espèces. L’app expose un bouton « Annuler et payer en espèces » qui appelle l’API d’annulation. Le backend marque la tentative comme expirée, supprime le QR code côté coursier, et accepte ensuite l’enregistrement d’un paiement espèces classique.

app.post('/api/tentatives/:id/annuler', requireCoursier, async (c) => {
  const id = c.req.param('id');
  await db.update(tentatives).set({ statut: 'annulee' }).where(eq(tentatives.id, id));
  return c.json({ ok: true });
});

app.post('/api/courses/:id/encaisser-especes', requireCoursier, async (c) => {
  const courseId = c.req.param('id');
  const { montantRecu } = await c.req.json();
  await db.insert(tentatives).values({
    id: ulid(), courseId, pspNom: 'especes',
    montantXof: montantRecu, statut: 'reussie'
  });
  await db.update(courses).set({ statut: 'livree-payee' }).where(eq(courses.id, courseId));
  return c.json({ ok: true });
});

Pour l’expérience utilisateur, prévoir un timeout automatique de 5 minutes sur la tentative Wave : si aucune confirmation n’arrive, l’app affiche « Délai dépassé, voulez-vous passer en espèces ? ». Cette option évite que le coursier reste bloqué attendant un paiement qui ne viendra jamais.

Étape 5 — Réconciliation comptable

En fin de journée, on génère un rapport qui liste pour chaque coursier ses encaissements ventilés Wave / espèces / autres. Ce rapport accélère la comptabilité et facilite la remise des espèces au siège (montant exact à confirmer). Pour l’audit, on conserve l’historique complet des tentatives sur 7 ans (obligation comptable UEMOA).

// Rapport quotidien par coursier
app.get('/api/admin/rapport-jour/:date', requireAdmin, async (c) => {
  const date = c.req.param('date');
  const tentatives = await db.select().from(tentatives)
    .innerJoin(courses, eq(tentatives.courseId, courses.id))
    .where(and(eq(tentatives.statut, 'reussie'), gte(tentatives.creeLe, debutJour(date)), lte(tentatives.creeLe, finJour(date))));

  const parCoursier = groupBy(tentatives, 'coursierId');
  return c.json(Object.entries(parCoursier).map(([coursierId, items]) => ({
    coursierId,
    nombreLivraisons: items.length,
    montantWave: items.filter(i => i.pspNom === 'wave').reduce((a,i) => a + i.montantXof, 0),
    montantEspeces: items.filter(i => i.pspNom === 'especes').reduce((a,i) => a + i.montantXof, 0)
  })));
});

Étape 6 — Sécurité du coursier

Au-delà du paiement digital qui réduit le risque d’agression, trois protections complémentaires. Premièrement, les coursiers remettent le cash résiduel au siège tous les soirs ou à un point de collecte sécurisé en milieu de journée pour les volumes importants. Pas de coursier qui rentre chez lui avec 100 000 FCFA en poche. Deuxièmement, chaque coursier porte un dispositif d’alerte (bouton SOS dans l’app, ou bracelet connecté) qui peut alerter le siège ou les autorités en cas d’agression. Troisièmement, l’assurance professionnelle des coursiers couvre les vols et agressions — coût négligeable face à la sérénité apportée. Plusieurs assureurs locaux proposent cette couverture pour 2 000 à 5 000 FCFA par coursier et par mois.

Pour la formation des coursiers, sensibiliser explicitement aux scénarios à risque : ne pas afficher trop d’argent, privilégier les paiements digitaux, ne pas accepter les courses dans des zones réputées dangereuses sans validation siège, signaler les comportements suspects de clients. Cette discipline collective réduit considérablement le risque opérationnel.

Erreurs fréquentes

Erreur Cause Solution
QR code Wave non scannable Taille trop petite à l’écran Afficher en grand format, lumière écran maximale
Webhook reçu mais paiement non validé côté app Polling pas relancé SSE ou polling avec timeout généreux
Double encaissement (Wave + espèces) Pas de verrou côté serveur Une seule tentative active par course
Réconciliation incohérente Tentatives orphelines en base Cron quotidien qui marque les tentatives anciennes comme expirées
Coursier oublie d’encaisser Pas de blocage de fin de course Statut « livrée-non-payée » qui déclenche alerte siège

Adaptation au contexte ouest-africain

Trois aspects pratiques. Premièrement, Wave est très répandu au Sénégal et en Côte d’Ivoire mais moins au Mali et au Burkina Faso où Orange Money domine. La PME doit supporter les deux principaux PSP de chaque pays opéré, idéalement avec interface unifiée côté coursier. Pour le Sénégal, accepter aussi Free Money et Orange Money en alternatives. Pour la Côte d’Ivoire, Orange Money et MTN Mobile Money. Cette flexibilité accroît le taux d’acceptation digital. Deuxièmement, les commissions des PSP varient : Wave est généralement gratuit côté client (modèle « Free Send »), Orange Money facture environ 1 % côté marchand. Ces nuances tarifaires influencent la marge nette et doivent être documentées dans le système comptable. Troisièmement, certains clients préfèrent toujours les espèces pour des raisons de confiance ou d’habitude — accepter les deux modes sans pression élimine la friction d’adoption.

Pour le déploiement initial, démarrer avec Wave seul et étendre progressivement aux autres PSP en fonction de la demande client. Cette approche itérative valide l’acceptation marché avant de complexifier l’intégration. Sur la première année, observer typiquement une montée progressive du taux de paiement digital de 30 % à 70 %, avec une stabilisation autour de 80 % au bout de 18-24 mois selon le segment client.

Tutoriels frères

Pour aller plus loin

FAQ

Quelles commissions Wave applique sur les paiements marchand ?
Pour Wave Business au Sénégal, le modèle 2025-2026 est généralement gratuit pour les transactions inférieures à un seuil mensuel, avec frais minimes au-delà. Vérifier les conditions actuelles directement avec Wave Senegal SA.

Comment éviter les paiements Wave frauduleux (faux QR codes) ?
Le QR code Wave officiel pointe vers une URL wave.com validée. Un faux QR code afficherait une autre URL. Sensibiliser les coursiers à vérifier qu’ils utilisent bien l’app officielle et que le QR généré vient bien du backend interne de la PME.

Que faire si Wave a une panne pendant la livraison ?
Basculer immédiatement sur paiement espèces. Le système doit fluidifier ce fallback en un seul tap. Documenter les incidents Wave pour la communication client transparente.

Faut-il proposer le paiement par carte bancaire aussi ?
Pertinent pour les segments B2B (entreprises, institutionnels). Stripe, Adyen, ou les agrégateurs locaux (PayDunya, CinetPay) couvrent ce besoin avec des intégrations similaires.

Architecture multi-PSP côté coursier

Pour les PME logistiques opérant dans plusieurs pays UEMOA, l’app coursier doit supporter Wave, Orange Money, Free Money, et MTN Mobile Money sans complexité accrue pour l’utilisateur. La bonne approche : un seul bouton « Encaisser via mobile money » qui propose un menu rapide avec les options disponibles dans le pays courant. Le coursier sélectionne le PSP préféré du client, et l’app suit le flow correspondant. Le backend gère la diversité via des adaptateurs PSP cohérents derrière une interface commune comme expliqué dans le pilier Hono framework.

Pour la formation des coursiers, ne pas multiplier les écrans ni les concepts. Le coursier ne doit pas avoir à comprendre les différences techniques entre Wave et Orange Money — il choisit en fonction de la préférence client exprimée verbalement, et l’app gère le reste. Cette simplicité opérationnelle est ce qui permet à des coursiers avec un niveau d’éducation modeste d’utiliser efficacement la stack sans erreur.

Évolution du modèle de paiement à terme

À mesure que le segment ouest-africain s’équipe en mobile money (de 25 % en 2020 à plus de 60 % en 2026 dans les zones urbaines couvertes), la part des paiements digitaux croît mécaniquement. Pour les PME logistiques bien équipées, l’objectif réaliste à 3-5 ans est 90 % des paiements en mobile money, avec les espèces résiduelles principalement issues des zones rurales et clientèles plus âgées. Cette transformation libère opérationnellement et financièrement les opérateurs : moins de risque, moins de comptabilité, plus de traçabilité pour la conformité réglementaire.

Pour anticiper cette évolution, investir dès le démarrage dans une architecture multi-PSP flexible évite la coûteuse refonte ultérieure. Le pattern d’adaptateurs PSP avec interface commune décrit dans ce tutoriel est conçu exactement pour cette flexibilité long-terme. Pour les agences techniques qui livrent ces stacks à des PME clientes, capitaliser cette architecture sur plusieurs projets construit un actif technique réutilisable et différenciant sur le marché ouest-africain.

Besoin d'un site web ?

Confiez-nous la Création de Votre Site Web

Site vitrine, e-commerce ou application web — nous transformons votre vision en réalité digitale. Accompagnement personnalisé de A à Z.

À partir de 250.000 FCFA
Parlons de Votre Projet
Publicité