Documentation API
Encaissez par Mobile Money depuis votre SaaS en quatre appels : lister les opérateurs, créer un paiement, suivre son statut, recevoir le webhook. Toutes les routes partagent la base https://zeyopay.com/api/public/v1 et échangent du JSON.
Le mode (test ou production) est porté par la clé API utilisée : une clé zp_test_… encaisse en bac à sable, une clé zp_live_… encaisse réellement. Aucun changement de code n'est nécessaire pour basculer.
# 1. Vérifier que votre clé fonctionne curl https://zeyopay.com/api/public/v1/operators \ -H "Authorization: Bearer zp_test_xxxxxxxx"
Clés API
Vos clés se trouvent dans le tableau de bord, page « API & Développeurs ». Elles s'utilisent uniquement côté serveur : ne les exposez jamais dans un navigateur ou une application mobile. Une clé révoquée renvoie immédiatement 401.
L'API n'est ouverte qu'aux comptes dont l'identité est vérifiée et le contrat signé.
Authorization: Bearer zp_live_9f1c... Content-Type: application/json
Idempotence
Envoyez un en-tête Idempotency-Key unique sur chaque création de paiement ou de lien. Si la même clé revient avec le même corps (retry réseau, double clic), la première réponse est renvoyée telle quelle : aucun second encaissement n'est créé. La même clé avec des paramètres différents renvoie 409 idempotency_key_reuse, et une requête identique encore en cours renvoie 409 request_in_progress. Les clés sont conservées 24 heures.
POST /api/public/v1/payments Authorization: Bearer zp_live_... Idempotency-Key: order-2091-attempt-1
En complément, une même reference marchand déjà en cours ou déjà réussie renvoie 409 duplicate_reference avec l'identifiant du paiement existant.
Limites d'appel
Les limites s'appliquent par clé API, par compte marchand et par adresse IP. Un dépassement renvoie 429 avec l'en-tête Retry-After en secondes.
POST /payments— 30 requêtes par minute.GET /payments— 300 requêtes par minute.POST /payment-links— 60 requêtes par minute.
Lister les opérateurs disponibles
Renvoie les pays et opérateurs réellement activés sur votre compte, avec la devise débitée, les montants minimum et maximum, et les instructions officielles publiées par chaque opérateur. N'écrivez jamais de code USSD en dur : affichez ceux renvoyés ici, ils changent selon l'opérateur et le pays.
GET /api/public/v1/operators
{
"mode": "live",
"countries": [
{
"country": "BFA",
"name": "Burkina Faso",
"prefix": "226",
"operators": [
{
"operator": "ORANGE_BFA",
"name": "Orange",
"currency": "XOF",
"min_amount": 100,
"max_amount": 1000000,
"status": "OPERATIONAL",
"requires_auth_code": true,
"auth_type": "PREAUTH",
"pin_prompt_mode": null,
"instructions": {
"auth_code": [
"Composez *144*4*6#",
"Entrez le montant",
"Entrez le code PIN",
"Entrez le code OTP reçu par SMS"
],
"pin_prompt": [],
"auth_code_channels": [
{
"type": "USSD",
"label": "",
"short_code": "*144*4*6#",
"steps": ["Composez *144*4*6#", "..."],
"steps_en": ["Dial *144*4*6#", "..."]
}
],
"pin_prompt_channels": []
}
}
]
}
]
}Instructions à afficher au client
auth_type: "PREAUTH"— le client doit d'abord suivreinstructions.auth_codepour obtenir un code, puis vous l'envoyez dansauth_code.pin_prompt_mode: "AUTOMATIC"— l'opérateur affiche lui-même la demande de code PIN sur le téléphone du client.pin_prompt_mode: "MANUAL"— le client doit composer le code court indiqué dansinstructions.pin_promptpour valider.short_code— le code court exact de l'opérateur (ex.*555*6#pour Moov Burkina,#144#pour Orange Sénégal).
Créer un paiement
Le paiement est asynchrone : la réponse renvoie PENDING, le client valide sur son téléphone, puis le statut final arrive par webhook ou par interrogation. Indiquez le numéro local (l'indicatif est ajouté selon le pays) ou le numéro international complet.
POST /api/public/v1/payments
{
"amount": 15000,
"currency": "XOF",
"operator": "ORANGE_BFA",
"phone_number": "70000000",
"country": "BFA",
"reference": "INV-2091",
"description": "Abonnement Pro",
"auth_code": "123456" // requis si requires_auth_code = true
}
201 Created
{
"id": "6b0f7a2c-...",
"status": "PENDING",
"amount": 15000,
"currency": "XOF",
"requested_amount": 15000,
"requested_currency": "XOF",
"operator": "ORANGE_BFA",
"phone_number": "22670000000",
"reference": "INV-2091",
"mode": "live",
"instructions": {
"auth_type": "PROVIDER_AUTH",
"requires_auth_code": false,
"pin_prompt_mode": "MANUAL",
"auth_code": [],
"pin_prompt": ["Composez *555*6#", "Entrez le code PIN"],
"pin_prompt_channels": [
{ "type": "USSD", "short_code": "*555*6#", "steps": ["Composez *555*6#", "Entrez le code PIN"] }
]
}
}Affichez instructions.pin_prompt sur votre écran d'attente : ce sont les étapes réelles de l'opérateur choisi, renvoyées à chaque paiement.
// Node.js / TypeScript
const res = await fetch("https://zeyopay.com/api/public/v1/payments", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ZEYO_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
amount: 25,
currency: "EUR", // converti au taux fixe vers la devise de l'opérateur
operator: "MOOV_BFA",
phone_number: "70000000",
country: "BFA",
reference: order.id,
description: "Plan Pro",
}),
});
const payment = await res.json();# PHP
$ch = curl_init("https://zeyopay.com/api/public/v1/payments");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("ZEYO_SECRET_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"amount" => 15000, "currency" => "XOF",
"operator" => "MOOV_BFA", "phone_number" => "70000000",
"reference" => $order->id,
]),
]);
$payment = json_decode(curl_exec($ch), true);Suivre un paiement
Interrogez le paiement toutes les 3 à 5 secondes pendant 5 minutes maximum, ou attendez simplement le webhook. Le statut est rafraîchi en direct auprès de l'opérateur.
GET /api/public/v1/payments/{id}
{
"id": "6b0f7a2c-...",
"status": "COMPLETED", // PENDING | COMPLETED | FAILED | EXPIRED
"amount": 15000,
"currency": "XOF",
"operator": "ORANGE_BFA",
"phone_number": "22670000000",
"reference": "INV-2091",
"failure_reason": null,
"mode": "live",
"created_at": "2026-09-08T14:22:31Z"
}Lister vos paiements
GET /api/public/v1/payments?status=completed&limit=50
{ "data": [ { "id": "...", "status": "COMPLETED", "amount": 15000, ... } ] }Liens de paiement
Pour encaisser sans écrire de tunnel de paiement, créez un lien : la page mobile ZEYO PAY gère le choix du pays, de l'opérateur, du numéro et du code d'autorisation.
POST /api/public/v1/payment-links
{ "name": "Plan Pro mensuel", "amount": 25, "currency": "EUR" }
201 Created
{
"slug": "plan-pro-mensuel-a1b2c",
"amount": 25,
"currency": "EUR",
"url": "https://zeyopay.com/pay/plan-pro-mensuel-a1b2c"
}Un montant à 0 crée un lien à montant libre : le client saisit lui-même la somme.
Devises et taux de change
Vous affichez vos prix en FCFA, en euros ou en dollars. Le client paie toujours dans la devise de son opérateur Mobile Money, convertie au taux fixe de la plateforme. Le champ amount de la réponse correspond au montant réellement débité.
| Devise de vente | Taux fixe |
|---|---|
| 1 XOF | 1 FCFA |
| 1 EUR | 655.957 FCFA |
| 1 USD | 610 FCFA |
Frais ZEYO PAY : 4 % + 100 FCFA par transaction réussie, détaillés dans la page « Frais » de votre tableau de bord.
Webhooks
Chaque paiement encaissé est envoyé en POST à l'URL de votre service. Vérifiez l'en-tête X-Zeyo-Signature (HMAC SHA-256 du corps brut avec votre secret) avant tout traitement, puis répondez 200 rapidement.
POST https://votre-saas.com/webhooks/zeyo
X-Zeyo-Event: payment.completed
X-Zeyo-Signature: 2f5c9e...
{
"event": "payment.completed",
"data": {
"id": "6b0f7a2c-...",
"amount": 15000,
"currency": "XOF",
"operator": "ORANGE_BFA",
"reference": "INV-2091",
"status": "COMPLETED"
},
"created_at": "2026-09-08T14:22:31Z"
}// Vérification de la signature (Node.js)
import { createHmac, timingSafeEqual } from "crypto";
const raw = await request.text();
const expected = createHmac("sha256", process.env.ZEYO_WEBHOOK_SECRET)
.update(raw)
.digest("hex");
const sig = request.headers.get("x-zeyo-signature") ?? "";
if (sig.length !== expected.length ||
!timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return new Response("Invalid signature", { status: 401 });
}
const event = JSON.parse(raw);
// idempotence : ignorez un event.data.id déjà traitéErreurs
Toutes les erreurs suivent le format { "error": { "code": "...", "message": "..." } }.
401 invalid_api_key— clé absente, invalide ou révoquée.403 kyc_required— identité non vérifiée ou contrat non signé.403 account_suspended— compte marchand suspendu.422 operator_unavailable— opérateur non activé sur votre compte.422 amount_out_of_range— montant hors limites de l'opérateur.422 auth_code_required— code d'autorisation Mobile Money manquant.402 deposit_rejected— refusé par l'opérateur ou le client.503 provider_unavailable— service opérateur momentanément indisponible.
Passer en production
- Vérifiez votre identité et signez le contrat marchand depuis le tableau de bord.
- Testez le parcours complet avec une clé
zp_test_…. - Enregistrez votre URL de webhook et conservez le secret côté serveur.
- Remplacez la clé test par la clé
zp_live_…dans vos variables d'environnement. - Contrôlez les premiers encaissements dans « Transactions » et « Frais ».