Free Money, opéré par Free Sénégal (filiale de Saga Africa Holdings), s’impose progressivement comme le troisième acteur du Mobile Money sénégalais en 2026. Avec une part de marché en croissance et une volonté affichée de challenger Wave et Orange Money, Free Money offre une API marchand qui mérite l’intégration pour tout marchand digital qui vise la couverture maximale du marché sénégalais. Voici le tutoriel complet d’intégration en 2026.
Ce tutoriel s’inscrit dans notre série Mobile Money. Pour le panorama global et la stratégie multi-providers, voir notre guide complet API Mobile Money 2026.
Note importante — Free Money n’expose pas de documentation publique aussi standardisée que Wave ou Orange Developer en 2026. Les endpoints (/api/v1/payment/init, etc.) présentés ci-dessous sont un exemple architectural type conforme aux pratiques du marché. Les détails exacts (URL, schéma de payload, header de signature) doivent être confirmés auprès du commercial Free B2B lors de votre activation marchand.
Pourquoi intégrer Free Money
- Couverture : Free Mobile compte plusieurs millions d’abonnés au Sénégal en 2026, dont une part croissante utilise Free Money pour payer en ligne
- Tarification compétitive : frais marchand inférieurs à Orange Money sur la plupart des grilles
- Croissance : utilisateurs souvent jeunes, urbains, qui complètent Wave et Orange Money
- Différenciation : afficher les 3 logos (Wave, OM, Free Money) au checkout est un signal de sérieux
- Support de paiement de factures et services premium en croissance
En pratique, ajouter Free Money à un checkout qui propose déjà Wave + Orange Money apporte 5-15 % de conversion supplémentaire selon la cible démographique.
Prérequis
- Entreprise enregistrée au Sénégal (SARL, SA, ou statut équivalent)
- Contact commercial Free SA (B2B Mobile Money) avec dossier KYC
- Compte marchand Free Money activé
- Node.js 20+ ou Bun
- Domaine HTTPS
- Niveau attendu : intermédiaire
- Temps : 1 semaine y compris activation compte (à lancer en parallèle des autres)
Étape 1 — Activation du compte marchand
L’activation Free Money se fait par contact direct avec le département Mobile Money B2B de Free Sénégal :
- Prise de contact via formulaire
freesn.sn/businessou directement à un commercial Free B2B - Dossier KYC : registre de commerce, NINEA, statuts, identité gérant, IBAN bancaire ou compte Free Money
- Signature du contrat marchand (PDF, parfois avec signature électronique)
- Réception des credentials API :
API_KEY,MERCHANT_ID,WEBHOOK_SECRET - Configuration des URLs de callback dans le portail marchand
Délai typique : 2-3 semaines selon la complétude du dossier. La sandbox est généralement disponible plus rapidement, parfois dès la première semaine.
Étape 2 — Modèle d’API
L’API Free Money en 2026 suit un modèle similaire à Wave et Orange Money : initiation côté serveur, redirection ou push notification utilisateur, callback de confirmation. Endpoints principaux :
POST /api/v1/payment/init— initier une transactionGET /api/v1/payment/{id}— vérifier le statutPOST /api/v1/payment/refund— remboursement- Webhook (callback) configuré côté portail vers votre URL HTTPS
Étape 3 — Setup projet
mkdir freemoney-integration && cd freemoney-integration
bun init -y
bun add hono drizzle-orm postgres zod
cat > .env << 'EOF'
FREEMONEY_API_KEY=fm_test_xxx
FREEMONEY_MERCHANT_ID=MERCH_001
FREEMONEY_WEBHOOK_SECRET=whsec_xxx
FREEMONEY_API_BASE=https://api.freemoney.sn/v1
DATABASE_URL=postgres://localhost/freemoneydemo
EOF
Étape 4 — Initier un paiement
// src/freemoney.ts
const FM_BASE = process.env.FREEMONEY_API_BASE!;
const FM_KEY = process.env.FREEMONEY_API_KEY!;
const FM_MERCHANT = process.env.FREEMONEY_MERCHANT_ID!;
export interface FreeMoneyPaymentInit {
amount: number; // FCFA
customerPhone: string; // +221 76/77/78 (préfixes Free)
orderId: string; // votre référence
returnUrl: string;
callbackUrl: string;
}
export async function initiatePayment(params: FreeMoneyPaymentInit) {
const res = await fetch(`${FM_BASE}/payment/init`, {
method: "POST",
headers: {
"Authorization": `Bearer ${FM_KEY}`,
"Content-Type": "application/json",
"X-Merchant-Id": FM_MERCHANT,
"X-Idempotency-Key": params.orderId,
},
body: JSON.stringify({
amount: params.amount,
currency: "XOF",
phone: params.customerPhone,
order_reference: params.orderId,
return_url: params.returnUrl,
callback_url: params.callbackUrl,
description: `Paiement commande ${params.orderId}`,
}),
});
if (!res.ok) {
const err = await res.text();
throw new Error(`Free Money init failed: ${res.status} ${err}`);
}
return res.json();
// Réponse type : { id, status: "INITIATED", payment_url, expires_at }
}
L’utilisateur reçoit une notification dans son app Free Money, ouvre, valide avec son code PIN. Selon configuration, il peut aussi être redirigé vers une page web Free Money pour saisir son numéro et confirmer.
Étape 5 — Endpoint d’initiation côté backend
// src/server.ts
import { Hono } from "hono";
import { initiatePayment } from "./freemoney";
import { db } from "./db/client";
import { orders } from "./db/schema";
import { nanoid } from "nanoid";
const app = new Hono();
app.post("/api/checkout/freemoney", async (c) => {
const { amount, customerPhone } = await c.req.json();
// Validation côté backend
if (typeof amount !== "number" || amount < 100 || amount > 1_000_000) {
return c.json({ error: "Montant invalide" }, 400);
}
if (!/^(\+221)?(76|77|78)\d{7}$/.test(customerPhone.replace(/\s/g, ""))) {
return c.json({ error: "Numéro Free invalide" }, 400);
}
const orderId = `CMD-${nanoid(10)}`;
await db.insert(orders).values({
id: orderId,
amount,
customerPhone,
provider: "freemoney",
status: "pending",
});
const result = await initiatePayment({
amount,
customerPhone,
orderId,
returnUrl: `https://exemple.sn/checkout/success?order=${orderId}`,
callbackUrl: `https://exemple.sn/webhooks/freemoney`,
});
await db.update(orders)
.set({ providerSessionId: result.id })
.where(eq(orders.id, orderId));
return c.json({
orderId,
paymentUrl: result.payment_url,
expiresAt: result.expires_at,
});
});
Étape 6 — Webhook signé
Le webhook Free Money utilise une signature HMAC SHA-256 dans le header X-Free-Signature. Vérification :
import crypto from "crypto";
export function verifyFreeMoneySignature(rawBody: string, signature: string): boolean {
const expected = crypto
.createHmac("sha256", process.env.FREEMONEY_WEBHOOK_SECRET!)
.update(rawBody)
.digest("hex");
// Comparaison timing-safe
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(signature, "hex"),
);
}
app.post("/webhooks/freemoney", async (c) => {
const rawBody = await c.req.text();
const signature = c.req.header("X-Free-Signature") ?? "";
if (!verifyFreeMoneySignature(rawBody, signature)) {
return c.text("Invalid signature", 401);
}
const event = JSON.parse(rawBody);
// Idempotence
const existing = await db.select().from(orders)
.where(eq(orders.id, event.order_reference))
.limit(1);
if (existing[0]?.status === "paid") return c.text("Already processed", 200);
if (event.status === "SUCCESS") {
await db.update(orders)
.set({
status: "paid",
providerTransactionId: event.transaction_id,
paidAt: new Date(),
})
.where(eq(orders.id, event.order_reference));
await sendReceipt(event.order_reference);
} else if (event.status === "FAILED") {
await db.update(orders).set({ status: "failed" })
.where(eq(orders.id, event.order_reference));
}
return c.text("OK", 200);
});
Étape 7 — Vérification proactive
export async function checkPaymentStatus(sessionId: string) {
const res = await fetch(`${FM_BASE}/payment/${sessionId}`, {
headers: {
"Authorization": `Bearer ${FM_KEY}`,
"X-Merchant-Id": FM_MERCHANT,
},
});
return res.json();
// { id, status: "INITIATED" | "SUCCESS" | "FAILED" | "EXPIRED", ... }
}
Cron toutes les 10 minutes pour les commandes pending de plus de 15 minutes — même logique que pour Wave et Orange Money.
Étape 8 — Numéros Free Sénégal
Les préfixes Free au Sénégal sont historiquement 76 (Free), 77 (Tigo, racheté par Free) et 78. Validez côté frontend ET côté backend. Si l’utilisateur entre un numéro non-Free, retournez une erreur claire (« Ce numéro ne semble pas être Free Mobile ») et proposez de basculer sur un autre provider.
// Validation Zod
import { z } from "zod";
const senegalPhoneSchema = z.string()
.regex(/^(\+221)?(76|77|78)\d{7}$/, "Numéro Sénégal invalide");
// Pour Free spécifiquement
const freeSenegalPhoneSchema = z.string()
.regex(/^(\+221)?(76|77)\d{7}$/, "Numéro Free Sénégal invalide");
Étape 9 — UX checkout multi-provider
Pour un checkout qui propose Wave, Orange Money et Free Money, deux UX possibles :
- Sélection explicite : 3 boutons logos. L’utilisateur clique celui qu’il utilise. Plus clair, recommandé pour les nouveaux clients.
- Détection auto par numéro : un seul champ téléphone, on détecte le provider selon le préfixe et on route. Plus rapide pour les habitués mais cache le choix.
Adaptation Afrique de l’Ouest
Free Money est spécifique au Sénégal en 2026. Pour la Côte d’Ivoire, le Mali ou le Burkina, l’équivalent est généralement MTN MoMo ou Moov Money. Adaptez la liste des providers selon le pays cible. Si votre app vise plusieurs pays, prévoyez une logique de routing par pays utilisateur (détection IP, profil, sélection manuelle).
Comparaison rapide Wave / OM / Free
| Critère | Wave | Orange Money | Free Money |
|---|---|---|---|
| Setup délai | 1-2 semaines | 2-4 semaines | 2-3 semaines |
| Frais marchand | ~1 % | ~2-3 % | ~1.5-2 % |
| Sandbox | Immédiate | Immédiate | ~1 semaine |
| API moderne | Excellente | Bonne | Bonne |
| Webhook signé | HMAC SHA-256 | notif_token | HMAC SHA-256 |
| Documentation | Excellente | Correcte | Correcte mais récente |
| Pénétration utilisateur | Très forte | Universelle | En croissance |
Erreurs fréquentes
| Erreur | Cause | Solution |
|---|---|---|
| « Numéro non Free » | Préfixe différent | Validation regex côté frontend pour rediriger |
| Webhook signature mismatch | Body modifié avant vérif | Conserver le rawBody texte |
| Compte marchand non activé | KYC incomplet | Suivre avec le commercial Free B2B |
| Doublons de transaction | Idempotence absente | X-Idempotency-Key + index unique base |
| Délai expiration trop court | Configuration sandbox restrictive | Demander extension ou afficher countdown UI |