E-commerce

Intégrer Orange Money à votre site e-commerce : 3 méthodes

29 min de lecture

Orange Money à côté de Wave et Mixx by Yas

Wave a connu une croissance fulgurante au Sénégal depuis 2021 grâce à sa tarification disruptive sur les transferts P2P, et le portefeuille Free Money a été renommé Mixx by Yas le 26 novembre 2024 lors du rebranding pan-africain du groupe AXIAN. Mais Orange Money, opérationnel au Sénégal depuis 2010, reste le portefeuille à la base d’utilisateurs la plus large, particulièrement en région et chez les plus de 40 ans. Ne pas le proposer, c’est ignorer une part significative du marché ouest-africain.

Cet article décrit trois méthodes d’intégration d’Orange Money dans une boutique WooCommerce, par ordre de complexité croissante : (1) la voie USSD manuelle, (2) le passage par un agrégateur, (3) l’API Orange Money Web Payment directe. Chacune correspond à un profil de marchand et un volume mensuel.

Comprendre l’écosystème Orange Money

Caractéristique Détail
Frais marchand 2-4% selon le volume et le contrat (1,5-2,5% en B2B négocié direct pour les gros volumes)
Frais client Généralement 0 (payés par le marchand)
Plafond transaction Jusqu’à 2 000 000 FCFA par transaction selon le niveau KYC. Plafonds journaliers et mensuels distincts (compte standard ~500 000 FCFA/jour, compte premium/business jusqu’à 5 000 000 FCFA/mois)
Délai crédit Instantané sur compte OM, J+1 à J+3 sur compte bancaire
USSD #144# (fonctionne sans internet)
Couverture Tout le Sénégal + 17 pays africains (interopérabilité internationale limitée à certains corridors)

Choisir la méthode adaptée à votre volume

Avant d’écrire la moindre ligne de code, on choisit la méthode qui correspond au volume mensuel et à l’effort de développement disponible. La méthode USSD manuelle (méthode 1) convient à un volume inférieur à 100 commandes par mois, pas de développement nécessaire mais validation manuelle des paiements ; elle reste pertinente pour la zone rurale où les clients utilisent un téléphone basique. La méthode agrégateur (méthode 2) s’impose dès qu’on dépasse 100 commandes par mois et qu’on veut industrialiser la confirmation automatique ; le coût est de 2 à 3,5 % de commission par transaction selon l’opérateur ciblé. La méthode API directe (méthode 3) ne devient rentable qu’au-delà de 50 millions FCFA de chiffre d’affaires mensuel ; elle réduit la commission à 1,5-2,5 % négociée avec Orange Business mais exige un effort de développement et de maintenance significatif (entre 5 et 10 jours-homme pour la mise en place, plus une vigilance permanente sur les évolutions API).

La voie pragmatique pour la majorité des PME sénégalaises et ivoiriennes est la méthode 2 via un agrégateur. Elle ouvre simultanément Wave, Orange Money, Mixx by Yas et la carte bancaire avec une seule intégration, tout en restant compatible avec la migration ultérieure vers l’API directe quand le volume le justifie. La pratique de terrain consiste à démarrer en méthode 2, mesurer la part Orange Money pendant six mois, et basculer en méthode 3 si cette part dépasse 60 % et que le volume mensuel franchit le seuil de rentabilité.

Méthode 1 : Paiement USSD manuel

La méthode la plus basique, qui fonctionne même quand le client n’a pas internet :

Configuration WooCommerce

Utilisez la passerelle « Virement bancaire » détournée pour Orange Money :

  • Titre : Paiement par Orange Money
  • Description : Envoyez le montant via Orange Money. Instructions détaillées après validation de commande.
  • Instructions :
Pour payer par Orange Money :
1. Composez #144# sur votre téléphone Orange
2. Choisissez "Transfert d'argent"
3. Entrez le numéro : 77 XXX XX XX
4. Entrez le montant exact : [MONTANT] FCFA
5. Confirmez avec votre code secret
6. Envoyez la confirmation SMS à notre WhatsApp : 77 XXX XX XX

Votre commande sera traitée dès réception du paiement.

Avantage clé : fonctionne pour les clients en zone rurale avec un simple téléphone basique, sans smartphone ni internet.

Méthode 2 : Via CinetPay (recommandé)

CinetPay est une passerelle de paiement panafricaine qui intègre Orange Money dans 10+ pays d’Afrique de l’Ouest. Au Sénégal, des alternatives existent : PayDunya, PayTech, Intouch et HUB2, chacune avec ses propres tarifs et couvertures. C’est la solution la plus simple pour une intégration automatisée.

Étape 1 : Créer un compte CinetPay

  1. Inscrivez-vous sur cinetpay.com
  2. Complétez le KYC : pièce d’identité, NINEA, registre de commerce
  3. Validez votre compte (3-7 jours ouvrables)
  4. Récupérez votre API Key et Site ID dans le tableau de bord

Étape 2 : Installer le plugin WooCommerce CinetPay

  1. Dans WordPress : Extensions > Ajouter
  2. Recherchez « CinetPay » ou téléchargez depuis le site CinetPay
  3. Installez et activez
  4. Allez dans WooCommerce > Réglages > Paiements > CinetPay

Étape 3 : Configuration

Activer : Oui
Titre : Payer par Orange Money / Wave / Carte
Description : Paiement sécurisé par mobile money ou carte bancaire
Mode : Live (après tests en Sandbox)
API Key : votre_api_key_cinetpay
Site ID : votre_site_id
URL de notification : https://votresite.sn/?wc-api=wc_cinetpay

Frais CinetPay : 2% pour Orange Money Sénégal (négociable selon volume), 3.5% pour cartes bancaires.

Méthode 3 : API Orange Money directe

Pour les entreprises avec un volume important et qui veulent réduire les frais d’intermédiaire.

Prérequis

  • Contrat marchand Orange Money (passez par votre conseiller Orange Business en agence, ou créez un compte développeur sur developer.orange.com pour la partie API)
  • Compte Orange Money Business actif
  • Clés d’accès API (Consumer Key, Consumer Secret, Merchant Key)
  • Serveur HTTPS avec certificat SSL valide

Environnement Sandbox vs Production

Avant toute mise en production, testez votre intégration sur l’environnement sandbox d’Orange :

  • Créez un compte développeur sur developer.orange.com et souscrivez à l’API Orange Money Web Payment
  • Utilisez les credentials de test fournis (Consumer Key/Secret de sandbox)
  • Endpoint sandbox : https://api.orange.com/orange-money-webpay/dev/v1/webpayment
  • Endpoint production Sénégal : https://api.orange.com/orange-money-webpay/sn/v1/webpayment (le segment de chemin correspond au code pays : sn, ci, ml, bf, cm…)
  • Effectuez vos tests avec les numéros et codes PIN de test fournis par Orange avant de basculer en production

Flux de paiement Orange Money API

  1. Votre site envoie une requête de paiement à l’API Orange Money
  2. Orange Money retourne une URL de paiement ou un code de paiement
  3. Le client entre son code secret Orange Money pour valider
  4. Orange Money envoie une notification (callback) à votre serveur
  5. Votre site confirme la commande automatiquement
// Initier un paiement Orange Money
function initier_paiement_om($order_id) {
    $order = wc_get_order($order_id);
    
    // 1. Obtenir le token d'accès
    $token_response = wp_remote_post('https://api.orange.com/oauth/v3/token', array(
        'headers' => array(
            'Authorization' => 'Basic ' . base64_encode(CONSUMER_KEY . ':' . CONSUMER_SECRET),
            'Content-Type'  => 'application/x-www-form-urlencoded'
        ),
        'body' => 'grant_type=client_credentials'
    ));
    $token = json_decode(wp_remote_retrieve_body($token_response))->access_token;
    
    // 2. Créer la demande de paiement
    $payment_response = wp_remote_post('https://api.orange.com/orange-money-webpay/sn/v1/webpayment', array(
        'headers' => array(
            'Authorization' => 'Bearer ' . $token,
            'Content-Type'  => 'application/json'
        ),
        'body' => json_encode(array(
            'merchant_key' => MERCHANT_KEY,
            'currency'     => 'OUV',
            'order_id'     => 'CMD-' . $order_id,
            'amount'       => intval($order->get_total()),
            'return_url'   => $order->get_checkout_order_received_url(),
            'cancel_url'   => $order->get_cancel_order_url(),
            'notif_url'    => home_url('/wc-api/om_callback/'),
            'lang'         => 'fr'
        ))
    ));
    
    $body = json_decode(wp_remote_retrieve_body($payment_response));
    
    if (isset($body->payment_url)) {
        return array('result' => 'success', 'redirect' => $body->payment_url);
    }
    
    return new WP_Error('om_error', 'Erreur Orange Money. Veuillez réessayer.');
}

Gérer le callback (notification) Orange Money

L’étape la plus critique et souvent mal implémentée. Voici un exemple de fonction de callback robuste avec vérification du montant, idempotence et logging :

// Endpoint de callback : https://votresite.sn/wc-api/om_callback/
add_action('woocommerce_api_om_callback', 'om_handle_callback');

function om_handle_callback() {
    $payload = json_decode(file_get_contents('php://input'), true);
    error_log('[OM Callback] ' . print_r($payload, true));

    if (empty($payload['order_id']) || empty($payload['status'])) {
        status_header(400); exit;
    }

    // Extraire l'ID WooCommerce (format CMD-{id}-{timestamp})
    preg_match('/CMD-(\d+)/', $payload['order_id'], $m);
    $order = wc_get_order($m[1] ?? 0);
    if (!$order) { status_header(404); exit; }

    // Idempotence : ne pas retraiter une commande déjà payée
    if ($order->is_paid()) { status_header(200); exit; }

    // Vérification du montant (sécurité critique)
    if ((int) $payload['amount'] !== (int) $order->get_total()) {
        $order->add_order_note('Montant OM incohérent : ' . $payload['amount']);
        status_header(400); exit;
    }

    if ($payload['status'] === 'SUCCESS') {
        $order->payment_complete($payload['txnid'] ?? '');
        $order->add_order_note('Paiement OM confirmé. Réf : ' . ($payload['txnid'] ?? ''));
    } else {
        $order->update_status('failed', 'Paiement OM échoué : ' . $payload['status']);
    }
    status_header(200);
}

Tester en environnement sandbox avant la production

L’erreur classique consiste à passer directement en production avec un compte marchand réel et à découvrir un défaut de paiement le premier jour. Avant toute mise en production, on couvre trois scénarios sur le sandbox Orange Money : un paiement nominal qui aboutit (le webhook arrive, le statut est SUCCESS, la commande passe en payée), un paiement annulé par le client (le client refuse le code PIN, le webhook arrive avec un statut d’échec, la commande passe en échouée), un timeout (le client ne saisit jamais son code, l’agrégateur retourne un statut EXPIRED au bout de 90 secondes — votre interface doit afficher un message clair et proposer une nouvelle tentative).

L’environnement sandbox Orange utilise des identifiants spécifiques (Consumer Key et Consumer Secret de test, distincts des identifiants production), des numéros de simulation fournis par Orange, et un endpoint dédié : https://api.orange.com/orange-money-webpay/dev/v1/webpayment. Ne tentez jamais de faire un paiement de test avec votre numéro réel sur l’environnement de production avant d’avoir validé le sandbox — chaque tentative consomme du crédit et reste dans l’historique de transaction.

Via PayDunya : Wave + Orange Money en une seule intégration

Si la boutique a déjà intégré PayDunya pour Wave, Orange Money est automatiquement inclus dans la liste des moyens de paiement proposés. Le client choisit son moyen de paiement préféré sur la page PayDunya hébergée, ce qui évite à votre serveur de manipuler les codes PIN ou d’OTP : la responsabilité de la conformité PCI-DSS reste côté agrégateur.

L’avantage opérationnel est de mutualiser une seule intégration pour plusieurs portefeuilles : Wave, Orange Money, Mixx by Yas, et la carte bancaire Visa/Mastercard. Le coût se situe typiquement autour de 3 à 3,5 % par transaction selon l’opérateur (négociable au-delà d’un certain volume mensuel), mais la maintenance d’une intégration unique compense largement le temps économisé sur les évolutions techniques (changements d’endpoints, mise à jour des libellés d’opérateurs comme Free Money → Mixx by Yas, ajout d’opérateurs comme MTN MoMo en Côte d’Ivoire).

Côté code, la création d’une facture PayDunya retourne un token et une invoice_url. Vous redirigez le client sur cette URL, et le webhook IPN https://votresite.sn/api/paydunya/ipn reçoit la confirmation à l’issue du paiement. La double vérification (rappel API depuis votre serveur avec le token reçu) est obligatoire pour éviter qu’un attaquant ne forge une fausse notification de paiement vers votre URL publique.

Gérer les cas particuliers

Paiement échoué / timeout

Orange Money peut mettre 30 secondes à 1 minute pour traiter un paiement. Prévoyez :

  • Un message d’attente clair : « Validation en cours, ne fermez pas cette page… »
  • Un timer de 90 secondes avant d’afficher « Réessayer »
  • Un recours : « Si le problème persiste, payez par Wave ou contactez-nous sur WhatsApp »

Remboursement Orange Money

Les remboursements Orange Money ne sont pas automatisés. Processus :

  1. Identifiez le paiement dans votre tableau de bord Orange Money / CinetPay
  2. Effectuez un transfert retour manuellement depuis votre compte OM
  3. Documentez la transaction avec numéro de référence
  4. Mettez à jour le statut WooCommerce en « Remboursé »

Client sans smartphone

Une part non négligeable des utilisateurs Orange Money au Sénégal, en particulier en zone rurale, utilise encore un téléphone basique (selon les rapports GSMA et ARTP, la pénétration smartphone dépasse 70% en zone urbaine mais reste plus faible en zone rurale). Pour eux :

  • Proposez le USSD (#144#) comme option principale
  • Envoyez les instructions par SMS (pas WhatsApp) après la commande
  • Acceptez les commandes par appel téléphonique avec paiement USSD

Plugin WooCommerce complet pour l’API Orange Money Web Payment

Pour passer du fragment de code à un plugin WooCommerce déployable, on consolide les étapes en quatre fichiers : l’amorce du plugin, la classe de passerelle, le gestionnaire OAuth2 avec cache des tokens, le gestionnaire de callback. Le résultat tient dans un sous-dossier wp-content/plugins/om-webpay-direct/ et s’active comme n’importe quel plugin. La référence officielle de l’API est documentée sur developer.orange.com/apis/om-webpay (l’accès aux specs détaillées exige un compte développeur Orange).

# Arborescence du plugin
om-webpay-direct/
├── om-webpay-direct.php       # entrée plugin
├── includes/
│   ├── class-om-gateway.php   # WC_Payment_Gateway
│   ├── class-om-api.php       # OAuth2 + webpayment
│   └── class-om-callback.php  # endpoint notif_url
└── readme.txt

Cette séparation isole la logique métier (gateway WooCommerce) du transport HTTP (API client) et du webhook (callback). Test mental : on doit pouvoir exécuter un test unitaire sur OM_API::create_webpayment() sans charger WooCommerce, simplement en mockant wp_remote_post. Si l’arborescence est propre, le découplage tombe naturellement.

<?php
// om-webpay-direct.php — entrée du plugin
/**
 * Plugin Name: Orange Money Web Payment Direct
 * Description: Passerelle WooCommerce pour l'API Orange Money Web Payment.
 * Version:     1.0.0
 * Requires PHP: 8.0
 */
defined('ABSPATH') || exit;

require_once __DIR__ . '/includes/class-om-api.php';
require_once __DIR__ . '/includes/class-om-callback.php';

add_action('plugins_loaded', function () {
    if (!class_exists('WC_Payment_Gateway')) return;
    require_once __DIR__ . '/includes/class-om-gateway.php';
    add_filter('woocommerce_payment_gateways', fn($g) => array_merge($g, ['WC_Gateway_Orange_Money']));
});

// Enregistrement de l'endpoint de callback côté REST API WordPress
add_action('woocommerce_api_om_callback', ['OM_Callback', 'handle']);

Le hook woocommerce_api_om_callback rend l’URL https://votresite.sn/wc-api/om_callback/ publiquement accessible — c’est exactement le format attendu par le champ notif_url envoyé à Orange Money. Le hook plugins_loaded garantit que WC_Payment_Gateway existe avant l’extension de la classe — sinon une fatale au chargement bloque tout le site. Test mental : désactiver WooCommerce ne casse plus le site, le filtre redevient inerte.

<?php
// includes/class-om-api.php — OAuth2 + create webpayment
class OM_API {

    /** Récupère un token OAuth2, mis en cache pour sa durée de vie */
    public static function get_access_token(): string {
        $cached = get_transient('om_access_token');
        if ($cached) return $cached;

        $basic = base64_encode(OM_CONSUMER_KEY . ':' . OM_CONSUMER_SECRET);
        $resp = wp_remote_post('https://api.orange.com/oauth/v3/token', [
            'headers' => [
                'Authorization' => "Basic $basic",
                'Content-Type'  => 'application/x-www-form-urlencoded'
            ],
            'body'    => 'grant_type=client_credentials',
            'timeout' => 30
        ]);

        if (is_wp_error($resp)) throw new Exception('OM token error: ' . $resp->get_error_message());
        $body = json_decode(wp_remote_retrieve_body($resp), true);
        if (empty($body['access_token'])) throw new Exception('OM token missing in response');

        // expires_in en secondes — on garde une marge de 60s
        $ttl = max(60, (int) ($body['expires_in'] ?? 3600) - 60);
        set_transient('om_access_token', $body['access_token'], $ttl);
        return $body['access_token'];
    }

    /** Crée une demande de paiement, retourne l'URL de redirection */
    public static function create_webpayment(WC_Order $order): string {
        $token = self::get_access_token();
        $endpoint = sprintf('https://api.orange.com/orange-money-webpay/%s/v1/webpayment', OM_COUNTRY_CODE);

        $resp = wp_remote_post($endpoint, [
            'headers' => [
                'Authorization' => "Bearer $token",
                'Content-Type'  => 'application/json'
            ],
            'body'    => wp_json_encode([
                'merchant_key' => OM_MERCHANT_KEY,
                'currency'     => 'OUV',
                'order_id'     => 'CMD-' . $order->get_id() . '-' . time(),
                'amount'       => (int) $order->get_total(),
                'return_url'   => $order->get_checkout_order_received_url(),
                'cancel_url'   => $order->get_cancel_order_url(),
                'notif_url'    => home_url('/wc-api/om_callback/'),
                'lang'         => 'fr',
                'reference'    => $order->get_order_key()
            ]),
            'timeout' => 30
        ]);

        if (is_wp_error($resp)) throw new Exception('OM webpay error: ' . $resp->get_error_message());
        $body = json_decode(wp_remote_retrieve_body($resp), true);
        if (empty($body['payment_url'])) throw new Exception('OM payment_url manquant');

        // Persister pay_token et notif_token pour la double vérification au callback
        $order->update_meta_data('_om_pay_token',   $body['pay_token'] ?? '');
        $order->update_meta_data('_om_notif_token', $body['notif_token'] ?? '');
        $order->save();

        return $body['payment_url'];
    }
}

Trois points méritent attention dans cette classe. Le cache du token via set_transient évite de redemander un OAuth2 à chaque commande — sans cache, on accumule des centaines d’appels inutiles à l’endpoint /oauth/v3/token et on s’expose à un rate-limit côté Orange. Le code pays OM_COUNTRY_CODE est externalisé dans wp-config.php pour permettre un même plugin de servir un site Sénégal et un site Côte d’Ivoire avec des constantes différentes. Les pay_token et notif_token retournés par Orange sont persistés en métadonnées de commande — ils servent à la double vérification côté callback. Test mental : si le token OAuth est rejeté (401), le code lève une exception explicite plutôt qu’un warning silencieux, ce qui force le marchand à vérifier ses Consumer Key/Secret avant de continuer.

<?php
// includes/class-om-gateway.php — passerelle WooCommerce
class WC_Gateway_Orange_Money extends WC_Payment_Gateway {
    public function __construct() {
        $this->id          = 'orange_money';
        $this->method_title = 'Orange Money (API directe)';
        $this->has_fields  = false;

        $this->init_form_fields();
        $this->init_settings();
        $this->title       = $this->get_option('title', 'Orange Money');
        $this->description = $this->get_option('description', 'Payez avec Orange Money via #144#.');
        $this->enabled     = $this->get_option('enabled');

        add_action('woocommerce_update_options_payment_gateways_' . $this->id, [$this, 'process_admin_options']);
    }

    public function init_form_fields() {
        $this->form_fields = [
            'enabled'     => ['title' => 'Activer', 'type' => 'checkbox', 'default' => 'no'],
            'title'       => ['title' => 'Titre', 'type' => 'text',     'default' => 'Orange Money'],
            'description' => ['title' => 'Description', 'type' => 'textarea',
                              'default' => 'Paiement Orange Money — vous recevrez un code USSD à composer.']
        ];
    }

    public function process_payment($order_id) {
        $order = wc_get_order($order_id);
        try {
            $payment_url = OM_API::create_webpayment($order);
            $order->update_status('on-hold', 'Paiement Orange Money initié, en attente de confirmation.');
            wc_reduce_stock_levels($order_id);
            WC()->cart->empty_cart();
            return ['result' => 'success', 'redirect' => $payment_url];
        } catch (Exception $e) {
            wc_add_notice('Erreur de paiement Orange Money : ' . $e->getMessage(), 'error');
            error_log('[OM] process_payment: ' . $e->getMessage());
            return ['result' => 'failure'];
        }
    }
}

La passerelle suit fidèlement la Payment Gateway API de WooCommerce et hérite des comportements standards (réduction de stock, vidage du panier, journalisation). Le statut intermédiaire on-hold est volontaire : il marque la commande comme « en attente du paiement » sans la considérer comme payée — c’est le callback qui fera basculer en processing ou completed. Test mental : si l’API Orange retourne une erreur, l’utilisateur voit une notice WooCommerce explicite plutôt qu’une page blanche, et l’erreur est tracée dans wp-content/debug.log via error_log.

<?php
// includes/class-om-callback.php — webhook notif_url
class OM_Callback {
    public static function handle(): void {
        $raw = file_get_contents('php://input');
        $payload = json_decode($raw, true);
        error_log('[OM Callback] ' . wp_json_encode($payload));

        if (empty($payload['order_id']) || empty($payload['status'])) {
            status_header(400); exit;
        }

        // Extraire l'ID WooCommerce du format CMD-{id}-{timestamp}
        if (!preg_match('/CMD-(\d+)-/', $payload['order_id'], $m)) {
            status_header(400); exit;
        }
        $order = wc_get_order((int) $m[1]);
        if (!$order) { status_header(404); exit; }

        // Idempotence — on ne retraite pas une commande déjà payée
        if ($order->is_paid()) { status_header(200); echo 'already paid'; exit; }

        // Double vérification : on attend le pay_token connu côté plugin
        $expected_pay_token = $order->get_meta('_om_pay_token');
        if (!empty($payload['pay_token']) && !hash_equals($expected_pay_token, $payload['pay_token'])) {
            $order->add_order_note('Callback OM rejeté : pay_token incorrect.');
            status_header(400); exit;
        }

        // Vérification du montant
        if ((int) $payload['amount'] !== (int) $order->get_total()) {
            $order->add_order_note('Callback OM rejeté : montant incohérent ' . $payload['amount']);
            status_header(400); exit;
        }

        if ($payload['status'] === 'SUCCESS') {
            $order->payment_complete($payload['txnid'] ?? '');
            $order->add_order_note('Paiement OM confirmé. Réf : ' . ($payload['txnid'] ?? ''));
        } else {
            $order->update_status('failed', 'Paiement OM échoué : ' . $payload['status']);
        }

        status_header(200); echo 'OK';
    }
}

Cinq garde-fous empilés dans ce callback : validation du payload (order_id et status obligatoires), parsing strict du format CMD-{id}-{ts} via regex (pas de intval permissif), idempotence par is_paid(), comparaison constant-time du pay_token avec hash_equals (résistant aux attaques temporelles), vérification du montant attendu vs reçu. La méthode payment_complete() de WooCommerce déclenche en cascade les actions hookées (envoi de mail au client, mail à l’admin, déclenchement de l’expédition). Test mental : deux callbacks Orange en parallèle pour la même commande — le premier passe à completed, le second voit is_paid() === true et répond 200 sans rien faire.

Workflow CinetPay équivalent (PHP/WooCommerce)

Pour la méthode 2 (passerelle agrégateur), le code se simplifie car CinetPay propose un SDK PHP officiel et un endpoint unique pour tous les opérateurs. La référence officielle est docs.cinetpay.com/api/1.0-en/checkout/initialisation et checkout/verification.

<?php
// includes/class-cinetpay-api.php — CinetPay v2
class CinetPay_API {
    const BASE = 'https://api-checkout.cinetpay.com/v2';

    /** Démarre un paiement, retourne l'URL de redirection */
    public static function init_payment(WC_Order $order): string {
        $transaction_id = 'WC-' . $order->get_id() . '-' . time();
        $body = [
            'apikey'         => CP_API_KEY,
            'site_id'        => CP_SITE_ID,
            'transaction_id' => $transaction_id,
            'amount'         => (int) $order->get_total(),  // doit être multiple de 5 sauf USD
            'currency'       => 'XOF',
            'description'    => 'Commande #' . $order->get_id(),
            'notify_url'     => home_url('/wc-api/cinetpay_callback/'),
            'return_url'     => $order->get_checkout_order_received_url(),
            'channels'       => 'ALL',
            'customer_name'    => $order->get_billing_first_name(),
            'customer_surname' => $order->get_billing_last_name(),
            'customer_email'   => $order->get_billing_email(),
            'customer_phone_number' => $order->get_billing_phone(),
            'customer_address' => $order->get_billing_address_1(),
            'customer_city'    => $order->get_billing_city(),
            'customer_country' => $order->get_billing_country(),  // ISO 2 lettres
            'customer_state'   => $order->get_billing_state() ?: 'NA',
            'customer_zip_code'=> $order->get_billing_postcode() ?: '00000',
        ];

        // Vérifier que le montant est multiple de 5 (contrainte CinetPay XOF/XAF)
        if ($body['amount'] % 5 !== 0) {
            throw new Exception('CinetPay exige un montant multiple de 5 FCFA — total actuel : ' . $body['amount']);
        }

        $resp = wp_remote_post(self::BASE . '/payment', [
            'headers' => ['Content-Type' => 'application/json'],
            'body'    => wp_json_encode($body),
            'timeout' => 30
        ]);
        if (is_wp_error($resp)) throw new Exception('CinetPay init failed: ' . $resp->get_error_message());

        $out = json_decode(wp_remote_retrieve_body($resp), true);
        if (($out['code'] ?? '') !== '201') {
            throw new Exception('CinetPay rejet : ' . ($out['message'] ?? 'inconnu') . ' — ' . ($out['description'] ?? ''));
        }

        $order->update_meta_data('_cp_transaction_id', $transaction_id);
        $order->save();
        return $out['data']['payment_url'];
    }

    /** Vérifie une transaction (appel de confirmation depuis le webhook) */
    public static function check_transaction(string $transaction_id): array {
        $resp = wp_remote_post(self::BASE . '/payment/check', [
            'headers' => ['Content-Type' => 'application/json'],
            'body'    => wp_json_encode([
                'apikey' => CP_API_KEY, 'site_id' => CP_SITE_ID, 'transaction_id' => $transaction_id
            ]),
            'timeout' => 30
        ]);
        if (is_wp_error($resp)) throw new Exception('CinetPay check failed: ' . $resp->get_error_message());
        return json_decode(wp_remote_retrieve_body($resp), true) ?: [];
    }
}

// Webhook handler — branché via add_action('woocommerce_api_cinetpay_callback', ...)
class CinetPay_Callback {
    public static function handle(): void {
        $transaction_id = sanitize_text_field($_POST['cpm_trans_id'] ?? $_GET['cpm_trans_id'] ?? '');
        if (!$transaction_id) { status_header(400); exit; }

        $check = CinetPay_API::check_transaction($transaction_id);
        $status = $check['data']['status'] ?? 'UNKNOWN';

        if (!preg_match('/WC-(\d+)-/', $transaction_id, $m)) { status_header(400); exit; }
        $order = wc_get_order((int) $m[1]);
        if (!$order) { status_header(404); exit; }
        if ($order->is_paid()) { status_header(200); echo 'already paid'; exit; }

        if ((int) ($check['data']['amount'] ?? 0) !== (int) $order->get_total()) {
            $order->add_order_note('CinetPay rejet : montant incohérent.');
            status_header(400); exit;
        }

        if ($status === 'ACCEPTED') {
            $order->payment_complete($transaction_id);
            $order->add_order_note('CinetPay confirmé. Méthode : ' . ($check['data']['payment_method'] ?? '?'));
        } else {
            $order->update_status('failed', 'CinetPay : ' . $status);
        }
        status_header(200); echo 'OK';
    }
}

Trois différences notables avec le workflow Orange Money direct : pas d’OAuth2 (CinetPay accepte la clé API directement dans le payload), validation explicite du « montant multiple de 5 » imposée par la documentation CinetPay côté XOF/XAF, vérification systématique via /v2/payment/check dans le webhook (la doc CinetPay le rappelle : « always make a call to the Verification API »). Test mental : une commande à 1 243 FCFA (non multiple de 5) lève une exception locale avant le moindre appel HTTP, ce qui évite une erreur 400 côté CinetPay et un message technique incompréhensible pour le client.

Mise en service du plugin

L’installation suit la procédure WordPress standard : copie du dossier dans wp-content/plugins/, activation via le menu Extensions, configuration des constantes dans wp-config.php.

// wp-config.php — à ajouter avant /* That's all, stop editing! */
define('OM_CONSUMER_KEY',    'votre_consumer_key');
define('OM_CONSUMER_SECRET', 'votre_consumer_secret');
define('OM_MERCHANT_KEY',    'votre_merchant_key');
define('OM_COUNTRY_CODE',    'sn');   // sn, ci, ml, bf, cm
define('CP_API_KEY',         'votre_api_key_cinetpay');
define('CP_SITE_ID',         'votre_site_id');

Aucune des clés sensibles ne vit dans le code source ni dans la base de données WordPress (wp_options). Le placement dans wp-config.php sort les secrets du périmètre normal des sauvegardes WordPress et facilite la rotation : on remplace une constante, on relance PHP-FPM, le plugin utilise immédiatement la nouvelle valeur grâce au cache transient qui sera revalidé. Test mental : on supprime une constante par erreur → la prochaine commande échoue avec une exception explicite « OM_CONSUMER_KEY non défini », pas une erreur opaque côté Orange.

Distinguer Orange Money, Wave et Mixx by Yas dans l’interface

Trois portefeuilles dominent le marché sénégalais en 2026, avec des codes USSD distincts qu’il faut afficher correctement : Orange Money via #144#, Mixx by Yas (ex-Free Money) via #150#, Wave via l’application mobile uniquement (pas d’USSD). MTN Mobile Money n’est pas présent au Sénégal — l’inclure dans la liste des moyens de paiement crée de la confusion chez les clients qui pensent que la boutique accepte les portefeuilles MTN qu’ils utilisent dans d’autres pays. Côté Côte d’Ivoire, le tableau diffère : Orange Money, MTN MoMo et Wave coexistent, et il faut les afficher tous les trois pour ne pas exclure une part du marché.

Sur la page checkout, on affiche les logos des opérateurs supportés à côté du bouton de paiement, avec le libellé exact (« Mixx by Yas » plutôt que « Free Money », libellé désuet depuis novembre 2024). On évite les libellés génériques comme « Mobile Money » qui obligent le client à deviner si son portefeuille est supporté ; les pertes de panier sont mesurables sur ce point.

Sécurité des paiements Orange Money

  • Ne stockez jamais les clés API dans le code source : utilisez wp-config.php ou des variables d’environnement
  • Vérifiez toujours les montants dans le callback : comparez le montant reçu avec le montant de la commande WooCommerce
  • Utilisez HTTPS : obligatoire pour les API de paiement
  • Loguez toutes les transactions : en cas de litige, vous avez besoin de preuves
  • Vérifiez la provenance des callbacks : vérifiez l’IP source ou utilisez un token de sécurité
// Stocker les clés API de manière sécurisée dans wp-config.php
define('OM_CONSUMER_KEY', 'votre_consumer_key');
define('OM_CONSUMER_SECRET', 'votre_consumer_secret');
define('OM_MERCHANT_KEY', 'votre_merchant_key');

// Puis dans votre plugin :
$consumer_key = defined('OM_CONSUMER_KEY') ? OM_CONSUMER_KEY : '';

Checklist intégration Orange Money

  • Compte Orange Money Business ou passerelle (CinetPay/PayDunya) actif
  • Plugin configuré et testé en mode sandbox
  • Callback/webhook fonctionnel (testez avec un vrai paiement de 100 FCFA)
  • Logo Orange Money visible sur la page checkout et le footer
  • Instructions USSD claires pour les clients sans smartphone
  • Fallback en place (Wave, COD) si Orange Money est indisponible
  • Clés API stockées dans wp-config.php, pas dans le code
  • Processus de remboursement documenté
  • Rapprochement comptable quotidien entre OM et WooCommerce

Réconciliation comptable

Pour fiabiliser votre comptabilité, mettez en place une procédure de rapprochement :

  • Exportez quotidiennement les transactions depuis votre tableau de bord Orange Money / CinetPay (export CSV ou via l’API de reporting)
  • Comparez avec les commandes WooCommerce de la même période (statut « Terminée »)
  • Identifiez les écarts : paiements reçus sans commande (callbacks perdus) ou commandes payées sans crédit OM (frauduleux)
  • Conservez les références de transaction (txnid) dans les notes de commande WooCommerce pour faciliter le rapprochement

Conformité et protection des données

Au Sénégal, la Loi 2008-12 sur la protection des données personnelles, supervisée par la CDP (Commission de Protection des Données Personnelles), encadre la collecte et le traitement des données clients. En tant que marchand en ligne :

  • Déclarez votre traitement de données auprès de la CDP (cdp.sn)
  • Affichez une politique de confidentialité claire mentionnant les données collectées par Orange Money et les passerelles
  • Limitez la conservation des données de paiement au strict nécessaire (références de transaction uniquement, jamais de codes secrets)

Hardening production : check-list avant go-live

Le tutoriel ci-dessus décrit le flow nominal et la sécurité de base. Avant la première transaction réelle sur ce code, huit points doivent être verrouillés — chaque omission est documentée comme cause d’incident sur des intégrations en production. La même liste est appliquée par les équipes paiement matures sur les sites en zone CEDEAO.

  1. Secrets jamais en base de données ni en clair en code. Clé API et secret webhook stockés dans un secret manager (HashiCorp Vault, AWS Secrets Manager, Doppler) ou a minima dans le fichier .env hors du repo (avec .gitignore strict) et chmod 600. Vérifier qu’aucune clé prod n’apparaît dans l’historique git via git log -p | grep -i "prod_\|sk_live\|api_key".
  2. Vérification HMAC sur raw body uniquement. Ne jamais re-stringifier le body parsé : les whitespaces, l’ordre des clés JSON et l’encodage UTF-8 doivent rester intacts. Utiliser express.raw() en Node, request.get_data() en Flask avant tout get_json(), file_get_contents("php://input") en PHP (jamais $_POST).
  3. Comparaison signature en temps constant. crypto.timingSafeEqual (Node, vérifier la longueur des buffers avant), hmac.compare_digest (Python), hash_equals (PHP), hmac.Equal (Go). Une comparaison == classique laisse fuir des bits par timing attack.
  4. Idempotence atomique. Contrainte unique en base sur l’ID d’événement provider (event_id Wave, notif_token Orange, X-Reference-Id MTN, id CinetPay/PayDunya/Flutterwave). Pattern INSERT … ON CONFLICT DO NOTHING qui revoie 200 immédiatement sur doublon, sans réappliquer l’effet métier (provisionnement, livraison, email).
  5. Fenêtre anti-replay sur le timestamp. Rejeter tout webhook dont le t= diffère de l’heure serveur de plus de 5 minutes. Évite la replay attack avec une signature historique interceptée. Synchroniser l’heure serveur via NTP (chrony ou systemd-timesyncd) pour éviter les rejets dûs à une dérive d’horloge.
  6. Timeout HTTP explicites séparés. Connect timeout 5 secondes, read timeout 15-30 secondes selon le provider. Jamais d’appel sans timeout — un connect bloqué peut faire monter votre PHP-FPM ou Node worker pool à saturation en quelques secondes.
  7. Retry exponentiel uniquement pour 5xx et 429. Base 2 (1s, 2s, 4s, 8s), plafond 60 secondes, maximum 4 tentatives. Les 4xx (sauf 429) sont des erreurs de configuration qui ne se corrigent pas en rejouant — propager immédiatement à l’opérateur. Utiliser un identifiant de retry stable côté provider (Idempotency-Key Stripe, client_reference Wave, externalId MTN) pour ne pas créer de doublons.
  8. Monitoring + alerting + réconciliation J+1. Métriques Prometheus ou équivalent : taux 401/403/429 sur appels sortants, taux de signatures invalides, latence p95 par provider, échec de réconciliation J-1. Page-out sur seuils stricts. Job cron quotidien 02h00 qui confronte la table interne aux exports providers — trois sorties scénarisées (100 % match, écart minoritaire = rapport finance, écart majoritaire = page-out + suspension nouvelles transactions).

La version exhaustive de cette check-list, avec un exemple de chaque fix en code, est dans le guide Wave Business API en production : KYC, clés live, IP whitelisting et HMAC. Les principes y sont génériques et s’appliquent identiquement à Orange Money, MTN MoMo, Flutterwave, CinetPay, PayDunya et Paystack.

Ressources officielles

Pour approfondir

Site web pour PME, freelance ou association

Pack tout-inclus : conception, domaine, hébergement, formation, support 6 mois. Tarif transparent, sans frais cachés.

À partir de 350 000 FCFA

📧 E-mail
💬 WhatsApp

Pour aller plus loin avec le paiement mobile

Service ITSkillsCenter

Application mobile Android et iOS

Création d'application mobile Android et iOS. À partir de 350 000 FCFA.

Démarrer mon projet
Publicité