E-commerce13 min de lecture

Stripe Billing multi-devises FCFA + EUR + USD : implémentation 2026

Mohamed Bah·Fondateur, Kolonell
2 juin 2026
Partager :
Stripe Billing multi-devises FCFA + EUR + USD : implémentation 2026

Stripe Billing multi-devises FCFA + EUR + USD : implémentation 2026

E-commerce

Stripe Billing multi-devises FCFA + EUR + USD : la réalité 2026

Le problème : Stripe (Standard et Express) ne supporte pas nativement le XOF (Franc CFA) en mode subscription. La devise "XOF" existe dans l'API Stripe mais Stripe n'a pas d'acquéreur acceptant le XOF pour les paiements par carte. Conséquence : un SaaS Sénégal qui veut facturer des clients locaux en FCFA + des clients européens en EUR + diaspora US en USD doit construire une architecture hybride.

J'ai conçu et implémenté cette architecture pour 4 SaaS Sénégal (1 health-tech, 1 fintech B2B, 1 ed-tech, 1 marketplace). Voici la méthode complète avec code Node.js, gestion taux, et conformité SYSCOHADA.

H2 : Architecture multi-devises éprouvée

Principe : Stripe gère EUR + USD nativement (cartes internationales, diaspora). Pour les clients locaux Sénégal en FCFA, on combine Wave Business API (mandat récurrent FCFA) + facturation conforme SYSCOHADA générée custom. Stripe sert de "source de vérité" pour subscriptions et invoicing logic, Wave est l'exécutant paiement FCFA.

`

Client EUR ──EUR/USD──▶ Stripe Billing ──Carte CB──▶ Stripe acquéreur

▼ source of truth

Client FCFA ──FCFA──▶ Wave Business ──USSD/QR──▶ Wallet Wave

Invoice PDF SYSCOHADA

`

H2 : Setup Stripe Billing pour EUR + USD

`typescript

// stripe-billing-setup.ts

import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {

apiVersion: '2026-04-15.basil',

});

// 1. Créer un Product

const product = await stripe.products.create({

name: 'SaaS Pro',

description: 'Abonnement Pro Kolonell SaaS',

});

// 2. Créer 2 Prices : EUR + USD

const priceEUR = await stripe.prices.create({

product: product.id,

unit_amount: 4900, // 49.00 EUR

currency: 'eur',

recurring: { interval: 'month' },

lookup_key: 'pro_eur_monthly',

});

const priceUSD = await stripe.prices.create({

product: product.id,

unit_amount: 5400, // 54.00 USD

currency: 'usd',

recurring: { interval: 'month' },

lookup_key: 'pro_usd_monthly',

});

`

Tip 1 : utiliser lookup_key pour éviter de hardcoder les Price IDs en front. Récupération via stripe.prices.list({ lookup_keys: ['pro_eur_monthly'] }).

Tip 2 : la conversion EUR → FCFA officielle BCEAO est 655,957 (parité fixe Euro-Franc CFA UEMOA). Toujours afficher l'équivalent FCFA en front même pour paiement EUR (clarté pour cadres locaux qui pensent en FCFA).

H2 : Wave Business pour FCFA récurrent

Wave Business API (v3, 2026) supporte le mandat récurrent avec workflow webhook. Setup :

`typescript

// wave-subscription.ts

import axios from 'axios';

const WAVE_API = 'https://api.wave.com/v1';

const WAVE_TOKEN = process.env.WAVE_BUSINESS_TOKEN!;

interface WaveMandate {

customerId: string;

amountFCFA: number;

frequency: 'monthly' | 'weekly';

startDate: string;

}

async function createWaveMandate(mandate: WaveMandate) {

const res = await axios.post(

${WAVE_API}/mandates,

{

receive_amount: mandate.amountFCFA,

currency: 'XOF',

customer_id: mandate.customerId,

frequency: mandate.frequency,

first_charge_date: mandate.startDate,

idempotency_key: mandate-${mandate.customerId}-${mandate.startDate},

},

{

headers: {

Authorization: Bearer ${WAVE_TOKEN},

'Content-Type': 'application/json',

},

},

);

return res.data;

}

`

Workflow client FCFA :

  • Client choisit "Pro 32 000 FCFA / mois" en front.
  • Backend appelle createWaveMandate.
  • Client reçoit SMS Wave pour confirmer mandat (clic + PIN).
  • Wave webhook mandate.activated → notre backend marque subscription active.
  • Wave webhook charge.success chaque mois → backend met à jour invoice + Stripe Subscription correspondante.

Important : créer une "fake" Stripe Subscription en payment_behavior: 'pending_if_incomplete' pour les clients FCFA, afin d'avoir une source unique de vérité. Le paiement vrai passe par Wave, mais Stripe garde le statut et l'invoice number.

H2 : Synchronisation Stripe ↔ Wave (le vrai défi)

`typescript

// sync-wave-to-stripe.ts

import { stripe } from './stripe-client';

interface WaveChargeWebhook {

event: 'charge.success' | 'charge.failed';

mandate_id: string;

amount_fcfa: number;

timestamp: string;

}

async function handleWaveCharge(webhook: WaveChargeWebhook) {

// 1. Retrouver le Stripe customer correspondant

const customer = await findCustomerByWaveMandate(webhook.mandate_id);

if (!customer) return;

// 2. Récupérer la Stripe Subscription FCFA équivalente

const subscriptions = await stripe.subscriptions.list({

customer: customer.stripeCustomerId,

status: 'active',

});

const fcfaSub = subscriptions.data.find((s) => s.metadata.currency_real === 'XOF');

if (!fcfaSub) return;

if (webhook.event === 'charge.success') {

// 3. Créer une Invoice Stripe en EUR équivalent (pour reporting)

const amountEUR = Math.round((webhook.amount_fcfa / 655.957) * 100); // cents

Besoin d'un site web professionnel ?

Kolonell crée des sites web qui attirent des clients, optimisés pour le marché sénégalais. Devis gratuit en 2 minutes.

const invoice = await stripe.invoices.create({

customer: customer.stripeCustomerId,

subscription: fcfaSub.id,

auto_advance: false,

collection_method: 'send_invoice',

metadata: {

currency_real: 'XOF',

amount_real_fcfa: String(webhook.amount_fcfa),

wave_charge_id: webhook.mandate_id,

},

});

await stripe.invoices.pay(invoice.id, { paid_out_of_band: true });

} else {

// 4. Mark subscription past_due

await stripe.subscriptions.update(fcfaSub.id, {

metadata: { wave_status: 'past_due', last_attempt: webhook.timestamp },

});

}

}

`

Clé : paid_out_of_band: true indique à Stripe que le paiement a été reçu hors Stripe (via Wave). L'invoice est marquée payée, le revenu apparaît dans Stripe Reports avec metadata pour reconstituer le mapping FCFA réel.

H2 : Conformité SYSCOHADA — facturation Sénégal

Le SYSCOHADA (Système Comptable OHADA, v2 révisée 2018, appliqué Sénégal) impose :

  • Numérotation continue des factures (pas de trou).
  • Mention TVA 18 % distincte (sauf exonérations art. 354 CGI Sénégal).
  • Mention NINEA + RCCM émetteur.
  • Devise : libellable en FCFA (XOF), avec possibilité libellé secondaire EUR/USD.
  • Archivage 10 ans (papier ou électronique conforme).

Implémentation : générer le PDF custom avec pdfkit (déjà dans le stack Kolonell).

`typescript

// invoice-syscohada.ts

import PDFDocument from 'pdfkit';

interface InvoiceData {

invoiceNumber: string; // ex 'KOL-2026-00427'

emitterNINEA: string;

emitterRCCM: string;

customerName: string;

customerNINEA?: string; // optionnel pour B2C

items: { description: string; quantity: number; unitPriceHT: number }[];

currency: 'XOF' | 'EUR' | 'USD';

tvaRate: number; // 0.18 par défaut

}

function generateSyscohadaInvoice(data: InvoiceData): Buffer {

const doc = new PDFDocument({ size: 'A4', margin: 50 });

const buffers: Buffer[] = [];

doc.on('data', (b) => buffers.push(b));

// En-tête conforme

doc.fontSize(20).text('FACTURE', { align: 'right' });

doc.fontSize(10).text(N° ${data.invoiceNumber}, { align: 'right' });

doc.text(NINEA: ${data.emitterNINEA});

doc.text(RCCM: ${data.emitterRCCM});

// Calcul HT, TVA, TTC

const totalHT = data.items.reduce((s, it) => s + it.quantity * it.unitPriceHT, 0);

const tva = totalHT * data.tvaRate;

const totalTTC = totalHT + tva;

doc.moveDown().text(Total HT: ${formatCurrency(totalHT, data.currency)});

doc.text(TVA ${(data.tvaRate * 100).toFixed(0)}%: ${formatCurrency(tva, data.currency)});

doc.fontSize(14).text(Total TTC: ${formatCurrency(totalTTC, data.currency)}, {

underline: true,

});

if (data.currency !== 'XOF') {

const equivXOF = data.currency === 'EUR' ? totalTTC * 655.957 : totalTTC * 605; // taux indicatif USD

doc.fontSize(10).text((Équivalent: ${formatCurrency(equivXOF, 'XOF')}), { align: 'right' });

}

doc.end();

return Buffer.concat(buffers);

}

function formatCurrency(amount: number, currency: 'XOF' | 'EUR' | 'USD'): string {

return new Intl.NumberFormat('fr-SN', { style: 'currency', currency }).format(amount);

}

`

H2 : Gestion des taux EUR/USD/FCFA

Parité fixe : 1 EUR = 655,957 XOF (BCEAO, parité Trésor français, ne bouge pas).

Taux flottant USD : doit être rafraîchi quotidiennement. Sources gratuites :

  • exchangerate-api.com (gratuit jusqu'à 1 500 req/mois).
  • fixer.io (gratuit 100 req/mois).
  • API BCEAO (officiel, lent mais souverain).

Implémentation cache Redis 24h :

`typescript

// fx-cache.ts

import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL!);

async function getUsdToXof(): Promise {

const cached = await redis.get('fx:usd:xof');

if (cached) return parseFloat(cached);

const res = await fetch('https://api.exchangerate-api.com/v4/latest/USD');

const data = await res.json();

const rate = data.rates.XOF; // ex 605.32

await redis.set('fx:usd:xof', String(rate), 'EX', 86400); // 24h

return rate;

}

`

Règle business : appliquer une marge taux de +1,5 % pour couvrir volatilité intra-mois (sinon perte sur mois USD baissier).

H2 : Edge cases observés en production

  • Client Wave avec solde insuffisant : 3 tentatives à J, J+1, J+2 (configurer dans Wave Business). Si échec total : email + suspension subscription.
  • Stripe SCA Europe pour cartes UE : implémenter 3D Secure 2 obligatoire (payment_intent.confirmation_method = 'automatic' + use_stripe_sdk: true).
  • Client demande facture en EUR alors qu'il paie en FCFA : générer 2 PDFs (FCFA pour comptable Sénégal, EUR pour reporting maison-mère).
  • Remboursement Wave : nécessite intervention manuelle (pas d'API refund instantané). Prévoir 48h délai pour client.
  • Webhook Wave manqué : implémenter idempotency + retry queue (BullMQ + Redis). Wave envoie 3 retries automatiques sur 24h.

H2 : Coût implémentation

PhaseEffortCoût TJM 250 KFCFA
Setup Stripe Billing EUR/USD3 jours750 000 FCFA
Intégration Wave Business + webhooks5 jours1 250 000 FCFA
Sync Stripe ↔ Wave + invoicing SYSCOHADA6 jours1 500 000 FCFA
Tests + edge cases + monitoring4 jours1 000 000 FCFA
Total18 jours4 500 000 FCFA

FAQ

Pourquoi Stripe ne supporte pas FCFA en card present ?

Stripe n'a pas d'acquéreur bancaire dans la zone UEMOA. Possible théoriquement via Stripe Atlas + arrangement bancaire local, mais non opérationnel en 2026. Workaround Wave seul moyen scalable.

Peut-on utiliser PayDunya ou CinetPay à la place de Wave ?

Oui techniquement. PayDunya supporte récurrent depuis 2024. Inconvénients : 4 % frais (vs 1 % Wave), taux échec mandat 12 % (vs 2 % Wave), UX moins fluide. Pour SaaS B2C grand public Sénégal, Wave reste #1.

Quel taux EUR/USD utiliser pour reporting comptable ?

Pour SYSCOHADA, utiliser le taux du jour de l'émission de facture (BCEAO publication). Pour Stripe Reports interne, taux Stripe (proche marché). Différence < 0,5 % en pratique.

Comment gérer TVA exonérée (export hors UEMOA) ?

Pour clients diaspora hors UEMOA (France, UE, US) : services digitaux exportés, exonération TVA Sénégal (art. 354 CGI) si facturation en devise étrangère. Mention "Exportation de services - TVA exonérée art. 354 CGI" sur facture.

SCA Europe : tous les paiements EUR doivent-ils être 3DS ?

Oui depuis sept. 2019 (PSD2). Stripe gère 3DS 2 automatiquement si payment_intent.confirmation_method = 'automatic'. Taux échec 3DS Sénégal-émetteur Wave/Orange : ~8 % (vs 2 % cartes UE), prévoir fallback Wave.

Discutons de votre cas

Si vous construisez un SaaS Sénégal multi-devises et voulez implémenter une stack Stripe Billing + Wave + SYSCOHADA conforme, nous concevons l'architecture. WhatsApp +221 77 596 93 33.

Tags :#Stripe Billing#Wave#FCFA#SaaS#SYSCOHADA#multi-devises
Partager :

Mohamed Bah

Fondateur, Kolonell

Passionné par le digital et l'entrepreneuriat en Afrique, Mohamed accompagne les entreprises sénégalaises dans leur transformation digitale depuis 2020. Fondateur de Kolonell, il croit que chaque PME mérite une présence en ligne professionnelle et accessible.