L'API FiveOne Pay
Encaissez du Mobile Money en quelques appels : création de paiement, suivi et webhooks signés.
Démarrage rapide
FiveOne Pay est une API REST pour encaisser des paiements Mobile Money à Madagascar : MVola, Orange Money et Airtel Money. Base URL :
https://api.fiveonepay.com/v1Les montants sont en Ariary (MGA), entiers — 50 000 MGA s'écrit 50000. Premier appel :
curl -X POST https://api.fiveonepay.com/v1/payments \
-H "Authorization: Bearer sk_test_…" \
-H "Content-Type: application/json" \
-d '{
"amount": 50000,
"reference": "CMD-1042",
"operator": "MVOLA",
"payerNumber": "0341234567",
"callbackUrl": "https://votre-site.com/webhooks/fiveonepay",
"successUrl": "https://votre-site.com/merci"
}'Authentification
Authentifiez chaque requête avec votre clé secrète dans l'en-tête Authorization :
Authorization: Bearer sk_live_…Deux environnements, deux clés (menu « Clés API ») :
sk_test_…— environnement Sandbox : montant libre, paiements simulés (aucun argent réel).sk_live_…— environnement Production (KYC validé requis).
SUCCESS après quelques secondes. Rien à composer sur un téléphone : votre webhook payment.success, votre solde et vos notifications se déclenchent comme en production.Créer un paiement
POST/v1/payments
FiveOne Pay renvoie une payment_url : la page de paiement hébergée vers laquelle rediriger votre client. Si vous précisez operator, la page est prête pour cet opérateur ; sinon le client le choisit lui-même.
| Champ | Type | Requis | Description |
|---|---|---|---|
amount | entier | Requis | Montant à encaisser en Ariary (MGA), sans décimales. Minimum 100. |
reference | chaîne | Requis | Votre référence de commande (≤ 120 caractères), renvoyée dans les webhooks. Unique par marchand + environnement. |
operator | chaîne | Optionnel | MVOLA, ORANGE_MONEY ou AIRTEL_MONEY. Si omis, le client choisit son opérateur sur la page de paiement. |
description | chaîne | Optionnel | Libellé affiché au client (≤ 255 caractères). |
payerNumber | chaîne | Optionnel | Téléphone du client payeur (0XXXXXXXXX, sans le 0, ou +261…). Le préfixe doit correspondre à operator : MVola 034/038, Orange 032/037, Airtel 033/035. Le client doit payer depuis ce numéro — un paiement envoyé depuis une autre ligne ne peut pas être validé. |
callbackUrl | URL | Pour les webhooks | URL notifiée (POST signé) à chaque événement de paiement. |
successUrl | URL | Optionnel | Redirection du client après un paiement réussi. |
cancelUrl | URL | Optionnel | Redirection du client en cas d'annulation. |
Réponse 201 Created — le paiement créé (commission = 2,75 % ; min 100, max 16 500 MGA) :
{
"id": "clzx8p2k9000108l0hf2v9m3d",
"status": "PENDING",
"environment": "SANDBOX",
"amount": 50000,
"payable_amount": 50000,
"currency": "MGA",
"commission": 1375,
"net_amount": 48625,
"reference": "CMD-1042",
"fiveonepay_reference": "48205173",
"operator": "MVOLA",
"payment_url": "https://pay.fiveonepay.com/pay/48205173",
"expires_at": "2026-06-22T09:45:00Z",
"created_at": "2026-06-22T09:30:00Z"
}fiveonepay_reference (8 chiffres) est la référence unique FiveOne Pay de la transaction, à conserver pour votre suivi. Redirigez votre client vers payment_url ; le statut final vous parvient par webhook.
amount et payable_amount — amount est votre prix : votre facture, la commission, le net_amountet le crédit de votre solde s'y rapportent toujours. payable_amount est le montant présenté au client, prérempli dans son code de paiement ; il peut dépasser amount de quelques ariary. Facturez et rapprochez votre comptabilité sur amount — vous êtes crédité de net_amount dans tous les cas.Idempotence
Après une coupure réseau, rejouez la requête sans risque de double paiement : ajoutez un en-tête Idempotency-Key unique par commande. Un second appel avec la même clé renvoie le paiement déjà crééau lieu d'en créer un nouveau.
curl -X POST https://api.fiveonepay.com/v1/payments \
-H "Authorization: Bearer sk_live_…" \
-H "Idempotency-Key: CMD-1042" \
-H "Content-Type: application/json" \
-d '{ "amount": 50000, "reference": "CMD-1042" }'Récupérer & lister
GET/v1/payments/:id
curl https://api.fiveonepay.com/v1/payments/clzx8p2k9000108l0hf2v9m3d \
-H "Authorization: Bearer sk_test_…"La réponse a la même forme que la création (statut à jour).
GET/v1/payments
Liste paginée. Filtres : status, page (défaut 1), limit (défaut 20, max 100).
curl "https://api.fiveonepay.com/v1/payments?status=SUCCESS&page=1&limit=20" \
-H "Authorization: Bearer sk_test_…"{
"data": [ { "id": "clzx…", "status": "SUCCESS", "amount": 50000, … } ],
"pagination": { "page": 1, "limit": 20, "total": 134, "total_pages": 7 }
}Liens de paiement
Un lien de paiement est un modèle réutilisable à montant fixe : créez-le une fois, partagez son url (ou son QR), et chaque clientqui l'ouvre règle le même montant. Chaque paiement crée une transaction distincte — idéal pour vendre un produit à prix fixe. Un lien n'expire pas.
POST/v1/payment-links
curl -X POST https://api.fiveonepay.com/v1/payment-links \
-H "Authorization: Bearer sk_test_…" \
-H "Content-Type: application/json" \
-d '{
"amount": 30000,
"description": "Gourde inox 1L",
"operator": "MVOLA"
}'| Champ | Type | Requis | Description |
|---|---|---|---|
amount | entier | Requis | Montant fixe payé par chaque client, en MGA (≥ 100). |
description | chaîne | Optionnel | Ce que voit le client (ex. « Gourde inox 1L »). |
operator | chaîne | Optionnel | MVOLA, ORANGE_MONEY ou AIRTEL_MONEY. Si omis, le client choisit. |
Réponse 201 Created — partagez le champ url avec vos clients :
{
"id": "clzr7k1a900010al0abc1def",
"reference": "plink_ab12cd34ef56ab78",
"environment": "SANDBOX",
"amount": 30000,
"currency": "MGA",
"description": "Gourde inox 1L",
"operator": "MVOLA",
"active": true,
"url": "https://pay.fiveonepay.com/link/plink_ab12cd34ef56ab78",
"payments_count": 0,
"total_collected": 0,
"created_at": "2026-07-03T09:30:00Z",
"updated_at": "2026-07-03T09:30:00Z"
}data.payment_link_id vous permet de les rattacher au lien.Gérer les liens
GET/v1/payment-links
Liste paginée (page, limit) avec, par lien, le nombre de paiements et le total collecté.
GET/v1/payment-links/:id
Détail d'un lien + ses paiements.
POST/v1/payment-links/:id/deactivate
Désactive le lien (refuse les nouveaux paiements). …/activate le réactive.
Paiements récurrents
Facturez un client de façon répétée sur un intervalle libre. Contrairement à une carte bancaire, le Mobile Money exige que le client valide chaque paiement par PIN : FiveOne Pay planifie l'échéance, relance le client par e-mail et héberge la page de paiement ; le client règle en deux clics. Vous, vous écoutez les webhooks.
1. Créer un plan
Un plan est une offre récurrente réutilisable : montant + fréquence (interval × intervalCount). Ex. day × 7 = tous les 7 jours ; month × 1 = mensuel.
POST/v1/plans
curl -X POST https://api.fiveonepay.com/v1/plans \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"name": "Abonnement Pro",
"amount": 50000,
"interval": "month",
"intervalCount": 1
}'| Champ | Type | Requis | Description |
|---|---|---|---|
name | string | Requis | Nom du plan (ex. « Abonnement Pro »). |
amount | integer | Requis | Montant en Ariary (≥ 100), facturé à chaque échéance. |
interval | string | Requis | Unité : day | week | month | year. |
intervalCount | integer | Optionnel | Multiplicateur (1–365, défaut 1). Ex. day × 7 = tous les 7 jours. |
operator | string | Optionnel | MVOLA | ORANGE_MONEY | AIRTEL_MONEY, ou omis (le client choisit). |
maxRetries | integer | Optionnel | Nb de relances avant impayé (défaut 3). |
retryEveryHrs | integer | Optionnel | Délai entre relances, en heures (défaut 24). |
graceHrs | integer | Optionnel | Grâce après la dernière relance avant PAST_DUE (défaut 0). |
- La fenêtre de relance (
maxRetries × retryEveryHrs + graceHrs) doit être inférieure à l'intervalle du plan, sinon deux échéances se chevaucheraient →400. Ex. un plan « tous les 3 jours » (72 h) refuse3 × 24 h. - En Sandbox,
amountn’est pas plafonné : les paiements y sont simulés, sans argent réel.
Gérer les plans
GET/v1/plans
Liste paginée (page, limit, filtre active).
GET/v1/plans/:id
Détail d'un plan.
PATCH/v1/plans/:id
Modifie name, active et la config de relance. Le montant et la fréquence ne sont pas modifiables — créez un nouveau plan.
2. Créer un client
Un clientréutilisable porte le numéro payeur et l'e-mail de rappel. externalRef le rend idempotent — re-POSTer la même référence renvoie le même client.
POST/v1/customers
curl -X POST https://api.fiveonepay.com/v1/customers \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"phone": "0341234567",
"email": "client@exemple.mg",
"externalRef": "client-42"
}'| Champ | Type | Requis | Description |
|---|---|---|---|
phone | string | Requis | Numéro Mobile Money du payeur (0XXXXXXXXX). |
name | string | Optionnel | Nom du client. |
email | string | Optionnel | Reçoit les rappels de paiement par e-mail. |
externalRef | string | Optionnel | Votre identifiant client. Idempotent : ré-utilisé si déjà vu. |
Gérer les clients
GET/v1/customers
Liste paginée (filtre externalRef).
GET/v1/customers/:id
Détail du client + ses abonnements.
PATCH/v1/customers/:id
Modifie phone, name, email.
3. Abonner le client
POST/v1/subscriptions
curl -X POST https://api.fiveonepay.com/v1/subscriptions \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"customerId": "cus_id…",
"planId": "plan_id…"
}'| Champ | Type | Requis | Description |
|---|---|---|---|
customerId | string | Requis | Client existant. Ou fournissez un objet customer inline. |
planId | string | Requis | Plan auquel abonner le client. |
callbackUrl | string | Optionnel | URL notifiée (POST signé) pour les événements de cet abonnement. |
startAt | string | Optionnel | Date ISO de la 1re échéance (utile si billNow=false). |
billNow | boolean | Optionnel | Facturer la 1re période immédiatement (défaut true). |
Réponse 201 Created — si billNow est actif, la première facture est émise :
{
"id": "sub_id…",
"status": "ACTIVE",
"next_billing_at": "2026-08-09T00:00:00Z",
"first_invoice": {
"id": "inv_id…",
"status": "OPEN",
"amount": 50000,
"invoice_url": "https://pay.fiveonepay.com/invoice/inv_id…",
"payment_url": "https://pay.fiveonepay.com/pay/48205173"
}
}invoice_url — c'est un lien stable qui régénère un paiement à chaque ouverture. payment_url pointe le paiement courant et expire au bout de quelques minutes (contrainte Mobile Money).POST /v1/subscriptions accepte aussi un objet customer inline ({ "customer": { "phone": "…", "externalRef": "…" }, "planId": "…" }) : le client est créé/retrouvé puis abonné en un seul appel.Ce que vous branchez : les webhooks
À chaque échéance, votre logique tient en une règle : quand payment.success arrive avec un subscription_id, prolongez l'accès du client ; sur subscription.past_due, suspendez-le. Les paiements d'abonnement enrichissent payment.success / payment.expired de subscription_id et invoice_id. Voir la section Webhooks.
Gérer les abonnements
GET/v1/subscriptions
Liste paginée (filtres status, planId, customerId).
GET/v1/subscriptions/:id
Détail + historique des factures, chacune avec ses reminders (rappels envoyés).
DELETE/v1/subscriptions/:id
Annule l'abonnement — plus aucune échéance ne sera générée.
Factures
Chaque échéance produit une facture, ressource de premier niveau. Elle porte son invoice_url, son statut (OPEN, PAID, UNPAID, VOID) et le journal des rappels envoyés.
GET/v1/invoices
curl "https://api.fiveonepay.com/v1/invoices?status=OPEN&page=1" \
-H "Authorization: Bearer sk_live_…"Filtres : status, page, limit. Chaque facture inclut plan_name, customer_label et reminders[]. GET /v1/invoices/:id renvoie le détail.
{
"id": "inv_id…",
"status": "OPEN",
"amount": 50000,
"attempts": 2,
"invoice_url": "https://pay.fiveonepay.com/invoice/inv_id…",
"reminders": [
{
"type": "MANUAL",
"channel": "EMAIL",
"recipient": "client@exemple.mg",
"sent_at": "2026-07-09T14:32:00Z"
},
{
"type": "AUTO",
"channel": "EMAIL",
"recipient": "client@exemple.mg",
"sent_at": "2026-07-08T09:00:00Z"
}
]
}Relancer une facture
Les rappels partent par e-mail — renseignez emailsur le client, sinon rien n'est envoyé. Le scheduler relance automatiquement selon la config du plan (reminders[].type = AUTO). Pour relancer hors calendrier :
POST/v1/invoices/:id/remind
curl -X POST https://api.fiveonepay.com/v1/invoices/inv_id…/remind \
-H "Authorization: Bearer sk_live_…"
# 200 OK
# { "invoice_url": "https://pay.fiveonepay.com/invoice/inv_id…" }429 sinon). Une facture déjà réglée renvoie 400. Ces rappels apparaissent avec type: "MANUAL" dans reminders[].UNPAID) reste payable via son invoice_url. Quand le client règle en retard, l'abonnement PAST_DUE repasse automatiquement ACTIVE (webhook subscription.reactivated), et la prochaine échéance repart sur une période complète.Reverser votre solde
Un reversement (payout) vire tout ou partie de votre solde disponiblevers un de vos comptes Mobile Money. Le compte doit d'abord être validé(menu « Comptes Mobile Money » du dashboard). La demande passe en PENDING — le montant est réservé — puis notre équipe la traite.
POST/v1/payouts
curl -X POST https://api.fiveonepay.com/v1/payouts \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"accountId": "clzk8p2mm000108l0ah2v9d3f",
"amount": 100000,
"callbackUrl": "https://votre-site.com/webhooks/fiveonepay"
}'| Champ | Type | Requis | Description |
|---|---|---|---|
accountId | chaîne | Requis | Identifiant d'un compte Mobile Money validé (menu « Comptes Mobile Money » du dashboard). |
amount | entier | Requis | Montant à reverser en MGA (≥ 100), dans la limite du solde disponible. |
callbackUrl | URL | Optionnel | URL notifiée (POST signé) à la validation/refus. Nécessaire pour recevoir les webhooks. |
Réponse 201 Created — le reversement est PENDING :
{
"id": "clzy9q3n000208l0zt4w1p2e",
"reference": "pyt_9f2c1a7b3e5d8c04",
"amount": 100000,
"currency": "MGA",
"operator": "MVOLA",
"account_number": "0341234567",
"account_holder": "RAKOTO Jean",
"status": "PENDING",
"environment": "PRODUCTION",
"reviewed_at": null,
"created_at": "2026-07-03T10:00:00Z"
}solde − reversements en attente).Suivre les reversements
GET/v1/payouts
Liste de vos reversements.
GET/v1/payouts/:id
Détail d'un reversement.
Avec callbackUrl, vous recevez un webhook signé à la décision : payout.approved (solde débité) ou payout.rejected (montant libéré) — même signature X-FiveOne-Signature que les paiements.
Webhooks & statuts
Si vous fournissez callbackUrl, nous l'appelons en POST à chaque événement :
payment.success | Le paiement a été confirmé (SUCCESS). |
payment.expired | Le lien a expiré sans paiement (EXPIRED). |
payout.approved | Un reversement a été validé (solde débité). |
payout.rejected | Un reversement a été refusé (montant libéré). |
invoice.created | Une facture d'abonnement a été émise à l'échéance. |
subscription.past_due | Relances épuisées — l'abonnement est impayé. |
subscription.reactivated | Une facture impayée a été réglée — l'abonnement repasse actif. |
subscription.canceled | Un abonnement a été annulé. |
payment.successs'il est payé, ou payment.expireds'il ne l'est pas dans le délai. Vous n'avez donc pas à interroger l'API.{
"id": "evt_9f2b7c1a…",
"event": "payment.success",
"created_at": "2026-06-22T09:32:10Z",
"data": {
"id": "clzx8p2k9000108l0hf2v9m3d",
"status": "SUCCESS",
"amount": 50000,
"payable_amount": 50000,
"currency": "MGA",
"commission": 1375,
"net_amount": 48625,
"reference": "CMD-1042",
"fiveonepay_reference": "48205173",
"operator": "MVOLA",
"payer_name": "Rakoto Jean",
"payer_number": "0341234567",
"declared_payer_number": "0341234567",
"paid_at": "2026-06-22T09:32:08Z"
}
}payer_numberest le numéro constaté à l'encaissement, tandis que declared_payer_number conserve celui annoncé à la création. Ils coïncident normalement ; les comparer vous aide à trancher un litige.
Chaque envoi est signé. Vérifiez l'en-tête X-FiveOne-Signature (HMAC-SHA256 du corps brut avec votre secret whsec_…) avant de traiter l'événement :
import crypto from "node:crypto";
// rawBody = corps BRUT de la requête (pas l'objet JSON déjà parsé)
const signature = req.headers["x-fiveone-signature"];
const expected = crypto
.createHmac("sha256", process.env.FIVEONE_WEBHOOK_SECRET) // whsec_…
.update(rawBody)
.digest("hex");
if (signature !== expected) {
return res.status(400).send("Signature invalide");
}
// Idempotence : ignorez un X-FiveOne-Event-Id déjà traité.Statuts d'un paiement
| PENDING | En attente de paiement. |
| SUCCESS | Paiement réussi et confirmé. |
| EXPIRED | Expiré sans paiement (délai dépassé). |
Nous réessayons l'envoi (backoff) jusqu'à recevoir une réponse 2xx de votre serveur.
EXPIRED peut repasser à SUCCESSsi l'argent nous parvient après le délai (confirmation Mobile Money tardive). Vous recevez alors un payment.success après le payment.expired : traitez toujours payment.success comme faisant foi (de façon idempotente).Erreurs
L'API utilise les codes HTTP standard : 2xx succès, 4xx requête invalide (paramètres, authentification), 5xx erreur serveur. Le corps décrit l'erreur :
{
"statusCode": 400,
"message": ["amount doit valoir au moins 100 MGA."],
"error": "Bad Request"
}message peut être une chaîne ou un tableau (erreurs de validation).