Introduction

L'API MaliaPay vous permet d'accepter les paiements mobiles et bancaires en Côte d'Ivoire. Une seule intégration pour accéder à Wave, Orange Money, MTN MoMo, Djamo, PeyaPay et les paiements par carte bancaire (Moov CI est momentanément indisponible).

Base URL

https://business.malia.ci/api/v1

Format des requêtes

  • Toutes les requêtes sont en JSON
  • Header obligatoire : Content-Type: application/json
  • Authentification marchand : X-API-Key: VOTRE_CLE_API
  • Le champ montant accepte int ou string et est normalisé côté serveur
  • Le champ customer_email est facultatif

1. Authentification

L'API de paiement n'accepte qu'un seul mode d'authentification : la clé API (header X-API-Key).

Clé API — obligatoire

Envoyez votre clé API via le header X-API-Key. La clé est liée à un marchand actif et permet d'appeler les endpoints de paiement sans login préalable.

X-API-Key: sk_live_xxxxxxxxxxxxxxxx Content-Type: application/json
Important

Le merchant_id présent dans le body est forcé côté serveur pour correspondre à celui de la clé API. Cela évite qu'un client paie au nom d'un autre marchand.

2. Clé API (X-API-Key)

Chaque marchand dispose d'une clé API unique, générée à la création du compte. Elle est visible dans votre espace marchand ; pour en obtenir une nouvelle, contactez le support.

Où trouver ma clé API ?

Connectez-vous à votre espace marchand : votre clé figure dans Intégration API → Vos identifiants et dans votre Profil (menu de votre compte, en haut à droite). Elle est masquée par défaut ; un bouton permet de l'afficher et de la copier.

Utiliser la clé API

cURL
curl -X POST https://business.malia.ci/api/v1/payments \ -H "X-API-Key: sk_live_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{"channel":"WAVECI","montant":5000,...}'

Protection du merchant_id

Le middleware identifie le marchand grâce à la clé API et remplace le merchant_id du body par celui associé. Même si le client modifie merchant_id dans la requête, c'est la clé API qui fait autorité.

Sécurité

Ne partagez jamais votre clé API dans du code côté client. Stockez-la dans vos variables d'environnement et appelez l'API depuis votre backend.

3. Canaux de paiement

Utilisez ces codes pour spécifier le canal de paiement lors de la création d'une transaction.

CanalCode APITypeDescription
Wave CIWAVECIMobile MoneyPaiement via l'application Wave
Orange MoneyOMCIMobile MoneyPaiement via Orange Money CI
MTN MoMoMTNCIIMobile MoneyPaiement via MTN Mobile Money
Moov CIMOOVCIMobile MoneyPaiement via Moov Money — momentanément indisponible
DjamoDJAMOMobile MoneyPaiement via l'application Djamo
PeyaPayPEYA_PAYMobile MoneyPaiement via PeyaPay (avec OTP)
Carte BancaireCARDCartePaiement par carte Visa/Mastercard
Moov CI momentanément indisponible

Le canal MOOVCI n'est pas disponible pour le moment. N'envoyez pas de paiement Moov et ne l'incluez pas dans vos tests ; nous annoncerons sa remise en service.

4. Créer un paiement

Cet endpoint crée une transaction de paiement et retourne une URL de redirection pour le client final.

POST /api/v1/payments

Headers requis

X-API-Key: sk_live_xxxxxxxxxxxxxxxx Content-Type: application/json

Corps de la requête

JSON
{ "merchant_id": "MI_12345678", // Optionnel - déterminé par votre clé API "channel": "WAVECI", // Obligatoire - Code du canal "montant": 5000, // Obligatoire - Montant en FCFA (minimum 100) "reference": "CMD-001", // Obligatoire - Votre référence commande "customer_name": "Jean", // Obligatoire - Nom du client "customer_surname": "Dupont", // Optionnel - Prénom du client "customer_phone_number": "0700000000", // Obligatoire - Téléphone client "customer_email": "client@email.com", // Optionnel "description": "Achat produit XYZ", // Optionnel "aggregated_merchant_id": "am-1erynpsng20s6", // Optionnel - ID agrégateur "return_url": "https://monsite.ci/merci", // Optionnel - URL après paiement "error_url": "https://monsite.ci/erreur", // Optionnel - URL paiement échoué "success_url": "https://monsite.ci/succes", // Optionnel - URL paiement validé "notification_url": "https://monsite.ci/webhook" // Optionnel - URL webhook }
Où trouver votre Merchant ID ?

Connectez-vous à votre espace marchand : votre ID Marchand (commençant par MI_) figure dans Intégration API → Vos identifiants et dans votre Profil.

Réponse succès (200/201)

JSON
{ "status": "pending", "message": "Transaction créée. Redirigez le client vers le lien.", "transaction_id": "Mxxxxxxxxxxxxxxxxxxxxxxx", "link": "https://business.malia.ci/checkout/Mxxxxxxxxxxxxxxxxxxxxxxx", "pay_token": "", "fees": { "montant_net": 4900, "frais_malia": 50, "frais_operateur": 50, "total_fees": 100 } }
Prochaine étape

Redirigez votre client vers l'URL retournée dans le champ link. Le client complètera le paiement directement avec l'opérateur.

Numéro de téléphone

Formats acceptés : 0700000000, +2250700000000 ou 225700000000. Le serveur normalise automatiquement les numéros avant d'appeler les opérateurs.

MTN MoMo

Avec MTNCII aucune redirection n'est retournée. Le statut est pending et le client reçoit une notification push/ USSD pour valider la demande. Surveillez le statut via GET /payments/{transaction_id} ou le webhook.

Flux de paiement par canal

1

Wave, Orange, Djamo

Redirection vers une page de confirmation. Le client valide sur son téléphone et retourne automatiquement.

2

PeyaPay

Nécessite une validation par OTP. Le client entre le code reçu par SMS sur la page de paiement.

3

Carte Bancaire

Redirection vers l'interface sécurisée de SycaPay pour saisie des informations de carte. À son retour, le client est renvoyé vers votre success_url ou error_url. La confirmation du paiement est vérifiée par MaliaPay auprès de l'opérateur : attendez la notification ou interrogez le statut, sans vous fier à la seule redirection.

5. PeyaPay (OTP)

Le canal PEYA_PAY fonctionne en deux étapes : une requête d'initiation envoie un OTP au client, puis une requête de vérification finalise le paiement.

Étape 1 — Initier

POST /api/v1/peyapay/init

Le body est identique à la création de paiement standard, avec "channel": "PEYA_PAY" et un numéro de téléphone valide. La réponse contient peyapay_transaction_id à mémoriser.

Exemple de réponse (201)

JSON
{ "status": "otp_required", "requiresOtp": true, "transaction_id": "Mxxxxxxxxxxxxxxxxxxxxxxx", "peyapay_transaction_id": 12345, "fees": { "montant_net": 4900, "frais_malia": 50, "frais_operateur": 50, "total_fees": 100 } }

Étape 2 — Vérifier le OTP

POST /api/v1/peyapay/verify

Le client reçoit un code par SMS et le renvoie via votre backend.

JSON
{ "peyapay_transaction_id": 12345, "otp_code": "123456" }

Étape 3 — Consulter le statut (optionnel)

GET /api/v1/peyapay/status/{peyapay_transaction_id}
Conseil

Le endpoint verify est synchrone. En cas de traitement asynchrone, utilisez le statut PeyaPay pour suivre l'opération.

6. Vérifier le statut d'un paiement

Consultez le statut d'une transaction pour savoir si elle a été complétée, est en attente, ou a échoué.

GET /api/v1/payments/{transaction_id}

Exemple de requête

cURL
curl -X GET https://business.malia.ci/api/v1/payments/Mxxxxxxxxxxxxxxxxxxxxxxx \
-H "X-API-Key: sk_live_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json"

Réponse

JSON
{ "transaction_id": "Mxxxxxxxxxxxxxxxxxxxxxxx", "reference": "CMD-001", "montant": "5000.00", "channel": "WAVECI", "status": "success", // pending, processing, success, failed, cancelled, pending_otp "customer_name": "Jean", "frais_malia": 50, "frais_operateur": 50, "created_at": "2024-01-15T10:30:00Z" }
Un statut peut évoluer

Un paiement non confirmé par l'opérateur au bout de 10 minutes passe en failed. Si l'opérateur confirme ensuite le paiement (cas fréquent de la carte bancaire), il repasse en success et vous êtes notifié. Tenez toujours compte du dernier statut reçu.

Statuts possibles

StatutDescription
pendingPaiement initié, en attente de confirmation client
processingPaiement en cours de traitement chez l'opérateur
pending_otpEn attente de validation OTP (PeyaPay)
successPaiement confirmé et réussi
failedPaiement échoué (fonds insuffisants, erreur opérateur...)
cancelledPaiement annulé par le client ou expiré

7. Notifications (webhook)

Indiquez l'adresse de votre serveur dans le champ notification_url de chaque paiement. MaliaPay y envoie un POST JSON dès qu'un statut est confirmé : succès, échec, annulation ou traitement en cours.

Livraison

La notification est envoyée avec un délai maximal de 10 secondes : votre endpoint doit répondre rapidement par un HTTP 2xx (200 recommandé). Si votre serveur répond autre chose ou ne répond pas à temps, MaliaPay renvoie la notification après 1 minute, puis 5 minutes, 15 minutes, 1 heure et 6 heures (5 relances, environ 7 h 20 en tout), puis abandonne. Si votre serveur a été indisponible plus longtemps, interrogez GET /api/v1/payments/{transaction_id}.

Contenu de la notification

JSON
{ "transaction_id": "Mxxxxxxxxxxxxxxxxxxxxxxx", "reference": "CMD-001", "status": "success", // success, failed, cancelled, processing "montant": "5000.00", "channel": "WAVECI", "customer_name": "Jean", "gateway_data": { /* données brutes de l'opérateur, format variable, à titre informatif */ } }

Valeurs du champ status

StatutSignification
successPaiement confirmé : vous pouvez valider la commande
failedPaiement échoué, refusé ou non confirmé dans le délai
cancelledPaiement annulé par le client
processingPaiement en cours de traitement chez l'opérateur

Vérifier la signature

Chaque notification porte deux en-têtes. Vous pouvez vérifier qu'elle vient bien de MaliaPay et qu'elle n'a pas été modifiée. Cette vérification est facultative : vos intégrations actuelles continuent de fonctionner sans changement.

En-têteContenu
X-MaliaPay-TimestampHorodatage Unix (secondes) de l'envoi
X-MaliaPay-Signaturesha256= suivi du HMAC-SHA256 de {timestamp}.{corps brut}, calculé avec votre clé API

Calculez la signature sur le corps brut reçu, sans le décoder ni le ré-encoder, comparez-la en temps constant, et rejetez les notifications dont l'horodatage a plus de 5 minutes pour éviter qu'une notification interceptée soit rejouée.

PHP
<?php $brut = file_get_contents('php://input'); // corps brut, avant tout json_decode $timestamp = $_SERVER['HTTP_X_MALIAPAY_TIMESTAMP'] ?? ''; $recue = $_SERVER['HTTP_X_MALIAPAY_SIGNATURE'] ?? ''; $attendue = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $brut, getenv('MALIAPAY_API_KEY')); if ($timestamp === '' || abs(time() - (int) $timestamp) > 300 || !hash_equals($attendue, $recue)) { http_response_code(401); // signature absente, invalide ou trop ancienne exit; } // signature valide : traiter la notification
Node.js
const crypto = require('crypto'); // Avec Express : app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { ... }) function signatureValide(corpsBrut, timestamp, recue, cleApi) { const attendue = 'sha256=' + crypto.createHmac('sha256', cleApi) .update(timestamp + '.' + corpsBrut).digest('hex'); const a = Buffer.from(attendue), b = Buffer.from(recue || ''); const recent = Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300; return recent && a.length === b.length && crypto.timingSafeEqual(a, b); }
Python
import hmac, hashlib, time def signature_valide(corps_brut: bytes, timestamp: str, recue: str, cle_api: str) -> bool: attendue = 'sha256=' + hmac.new(cle_api.encode(), timestamp.encode() + b'.' + corps_brut, hashlib.sha256).hexdigest() recent = abs(time.time() - int(timestamp or 0)) <= 300 return recent and hmac.compare_digest(attendue, recue or '')

Confirmer avant de valider une commande

La signature prouve l'origine de la notification. Avant de livrer ou de valider une commande, relisez tout de même le statut auprès de MaliaPay avec votre clé API, et vérifiez la référence : une notification peut arriver dans le désordre ou être remplacée par une plus récente.

PHP
<?php // webhook.php — adresse fournie dans notification_url $payload = json_decode(file_get_contents('php://input'), true); $transactionId = $payload['transaction_id'] ?? null; // 1. Relire le statut auprès de MaliaPay (clé API conservée côté serveur) $ch = curl_init('https://business.malia.ci/api/v1/payments/' . urlencode($transactionId)); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['X-API-Key: ' . getenv('MALIAPAY_API_KEY')], ]); $payment = json_decode(curl_exec($ch), true); // 2. Valider seulement si le paiement est confirmé et correspond à votre commande if (($payment['status'] ?? null) === 'success' && $payment['reference'] === $referenceAttendue) { // marquer la commande comme payée (de façon idempotente) } http_response_code(200);
Bonnes pratiques

Répondez toujours HTTP 200. Traitez les notifications de façon idempotente : une même transaction peut être notifiée plusieurs fois (relances, ou statuts successifs comme d'abord failed puis success). Ne considérez pas la page de retour du client (success_url) comme une preuve de paiement.

8. Codes d'erreur

Code HTTPErreurDescription
401UnauthorizedClé API absente, invalide, ou marchand inactif
404Not FoundTransaction introuvable
422Validation ErrorChamp manquant ou invalide (canal inconnu, montant inférieur à 100, URL incorrecte…)
502Gateway ErrorL'opérateur a refusé la demande ou le canal est en maintenance : le champ reason donne la cause
500Server ErrorErreur serveur, réessayez plus tard

Erreur de validation (422)

JSON
{ "message": "Le montant minimum est de 100 XOF", "errors": { "montant": ["Le montant minimum est de 100 XOF"] } }

Authentification ou opérateur (401, 502)

JSON
{ "status": "error", "message": "Clé API invalide ou marchand inactif." } { "status": "error", "message": "Gateway error. Please try again.", "reason": "…cause…" }

9. Exemples complets

Exemple PHP complet

PHP
class MaliaPayClient { private $baseUrl = 'https://business.malia.ci/api/v1'; private $apiKey; public function __construct($apiKey) { $this->apiKey = $apiKey; } // 1. Créer un paiement public function createPayment($data) { return $this->request('POST', '/payments', $data); } // 2. Vérifier le statut public function getPayment($transactionId) { return $this->request('GET', "/payments/{$transactionId}"); } private function request($method, $path, $data = []) { $ch = curl_init($this->baseUrl . $path); $headers = [ 'Content-Type: application/json', "X-API-Key: {$this->apiKey}" ]; curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers, CURLOPT_POSTFIELDS => json_encode($data) ]); $result = json_decode(curl_exec($ch), true); curl_close($ch); return $result; } } // Utilisation $client = new MaliaPayClient('sk_live_xxxxxxxxxxxxxxxx'); $payment = $client->createPayment([ 'merchant_id' => 'MI_12345678', // Votre ID Marchand 'channel' => 'WAVECI', 'montant' => 5000, 'reference' => 'CMD-001', 'customer_name' => 'Jean', 'customer_surname' => 'Dupont', 'customer_phone_number' => '0700000000' ]); // Rediriger le client (hors MTN) if (!empty($payment['link'])) { header("Location: {$payment['link']}"); }

Exemple JavaScript (Node.js)

JavaScript
const API_KEY = 'sk_live_xxxxxxxxxxxxxxxx'; const createPayment = async () => { // 1. Créer le paiement const paymentRes = await fetch('https://business.malia.ci/api/v1/payments', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': API_KEY }, body: JSON.stringify({ merchant_id: 'MI_12345678', // Votre ID Marchand channel: 'WAVECI', montant: 5000, reference: 'CMD-001', customer_name: 'Jean', customer_surname: 'Dupont', customer_phone_number: '0700000000' }) }); const payment = await paymentRes.json(); // 2. Rediriger vers la page de paiement if (payment.link) { window.location.href = payment.link; } };

Exemple Python

Python
import requests BASE_URL = "https://business.malia.ci/api/v1" API_KEY = "sk_live_xxxxxxxxxxxxxxxx" headers = { "Content-Type": "application/json", "X-API-Key": API_KEY } # 1. Créer un paiement payment_res = requests.post( f"{BASE_URL}/payments", headers=headers, json={ "merchant_id": "MI_12345678", # Votre ID Marchand "channel": "WAVECI", "montant": 5000, "reference": "CMD-001", "customer_name": "Jean", "customer_surname": "Dupont", "customer_phone_number": "0700000000" } ) payment = payment_res.json() print(f"Statut: {payment['status']}") if payment.get("link"): print(f"Rediriger vers: {payment['link']}")

10. Bac à sable (sandbox)

Utilisez le bac à sable pour tester votre intégration sans effectuer de vrai paiement. L'endpoint accepte le même body que /payments mais ne déclenche aucun appel opérateur.

POST /api/v1/test

Comportement

  • Mêmes règles de validation que /payments
  • Mêmes frais calculés
  • Crée une transaction enregistrée avec le statut success et gateway_transaction_id = TEST
  • Aucun appel à Wave, Orange, MTN, Moov, Djamo ou SycaPay
  • Ne simule que le succès : les scénarios d'échec, d'annulation, d'attente ou de confirmation tardive ne sont pas disponibles
  • N'envoie aucune notification à votre notification_url : testez la réception de vos webhooks avec un vrai paiement de faible montant
  • La transaction de test est enregistrée à part : elle n'apparaît pas dans votre historique et GET /api/v1/payments/{transaction_id} la renvoie en 404

Exemple de requête

cURL
curl -X POST https://business.malia.ci/api/v1/test \ -H "X-API-Key: sk_live_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{"merchant_id":"MI_12345678","channel":"WAVECI","montant":5000,"reference":"TEST-001","customer_name":"Jean","customer_phone_number":"0700000000"}'

Réponse (201)

JSON
{ "status": "success", "message": "[SANDBOX] Paiement simulé avec succès. Aucun appel opérateur effectué.", "transaction_id": "Mxxxxxxxxxxxxxxxxxxxxxxx", "link": "", "fees": { "montant_net": 4900, "frais_malia": 50, "frais_operateur": 50, "total_fees": 100 } }
Conseil

Utilisez ce endpoint pour valider votre clé API, votre format JSON et la structure des réponses avant de passer en production.

Support & contact

Besoin d'aide pour l'intégration ? Notre équipe technique vous accompagne par email : indiquez votre identifiant marchand et la référence de la transaction concernée.

Une solution proposée par Lkmdigital