Aller au contenu
FiveOne Pay
Documentation

L'API FiveOne Pay

Encaissez du Mobile Money en quelques appels : création de paiement, suivi et webhooks signés.

Démarrage

Démarrage rapide

FiveOne Pay est une API REST pour encaisser des paiements Mobile Money à Madagascar : MVola, Orange Money et Airtel Money. Base URL :

http
https://api.fiveonepay.com/v1

Les montants sont en Ariary (MGA), entiers — 50 000 MGA s'écrit 50000. Premier appel :

POST /v1/paymentsbash
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"
  }'
Démarrage

Authentification

Authentifiez chaque requête avec votre clé secrète dans l'en-tête Authorization :

http
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).
Tester en Sandbox— dès qu'un paiement est complet (opérateur et numéro du payeur connus), il passe seul en 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.
Sécurité — Ne partagez jamais votre clé secrète côté client. En cas de fuite, révoquez-la depuis votre tableau de bord.
Paiements

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.

ChampTypeRequisDescription
amountentierRequisMontant à encaisser en Ariary (MGA), sans décimales. Minimum 100.
referencechaîneRequisVotre référence de commande (≤ 120 caractères), renvoyée dans les webhooks. Unique par marchand + environnement.
operatorchaîneOptionnelMVOLA, ORANGE_MONEY ou AIRTEL_MONEY. Si omis, le client choisit son opérateur sur la page de paiement.
descriptionchaîneOptionnelLibellé affiché au client (≤ 255 caractères).
payerNumberchaîneOptionnelTé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é.
callbackUrlURLPour les webhooksURL notifiée (POST signé) à chaque événement de paiement.
successUrlURLOptionnelRedirection du client après un paiement réussi.
cancelUrlURLOptionnelRedirection du client en cas d'annulation.

Réponse 201 Created — le paiement créé (commission = 2,75 % ; min 100, max 16 500 MGA) :

201 Createdjson
{
  "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.

Requête idempotentebash
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" }'
Paiements

Récupérer & lister

GET/v1/payments/:id

Récupérer un paiementbash
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).

Lister les paiementsbash
curl "https://api.fiveonepay.com/v1/payments?status=SUCCESS&page=1&limit=20" \
  -H "Authorization: Bearer sk_test_…"
200 OKjson
{
  "data": [ { "id": "clzx…", "status": "SUCCESS", "amount": 50000, … } ],
  "pagination": { "page": 1, "limit": 20, "total": 134, "total_pages": 7 }
}
Paiements

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

Créer un lienbash
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"
  }'
ChampTypeRequisDescription
amountentierRequisMontant fixe payé par chaque client, en MGA (≥ 100).
descriptionchaîneOptionnelCe que voit le client (ex. « Gourde inox 1L »).
operatorchaîneOptionnelMVOLA, ORANGE_MONEY ou AIRTEL_MONEY. Si omis, le client choisit.

Réponse 201 Created — partagez le champ url avec vos clients :

201 Createdjson
{
  "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"
}
Suivi — Les paiements faits via un lien émettent les webhooks habituels ; 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.

Abonnements

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

Créer un planbash
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
  }'
ChampTypeRequisDescription
namestringRequisNom du plan (ex. « Abonnement Pro »).
amountintegerRequisMontant en Ariary (≥ 100), facturé à chaque échéance.
intervalstringRequisUnité : day | week | month | year.
intervalCountintegerOptionnelMultiplicateur (1–365, défaut 1). Ex. day × 7 = tous les 7 jours.
operatorstringOptionnelMVOLA | ORANGE_MONEY | AIRTEL_MONEY, ou omis (le client choisit).
maxRetriesintegerOptionnelNb de relances avant impayé (défaut 3).
retryEveryHrsintegerOptionnelDélai entre relances, en heures (défaut 24).
graceHrsintegerOptionnelGrâce après la dernière relance avant PAST_DUE (défaut 0).
Deux contraintes à connaître
  • 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) refuse 3 × 24 h.
  • En Sandbox, amount n’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

Créer un clientbash
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"
  }'
ChampTypeRequisDescription
phonestringRequisNuméro Mobile Money du payeur (0XXXXXXXXX).
namestringOptionnelNom du client.
emailstringOptionnelReçoit les rappels de paiement par e-mail.
externalRefstringOptionnelVotre 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

Créer un abonnementbash
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…"
  }'
ChampTypeRequisDescription
customerIdstringRequisClient existant. Ou fournissez un objet customer inline.
planIdstringRequisPlan auquel abonner le client.
callbackUrlstringOptionnelURL notifiée (POST signé) pour les événements de cet abonnement.
startAtstringOptionnelDate ISO de la 1re échéance (utile si billNow=false).
billNowbooleanOptionnelFacturer la 1re période immédiatement (défaut true).

Réponse 201 Created — si billNow est actif, la première facture est émise :

201 Createdjson
{
  "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"
  }
}
Quel lien partager au client ? Utilisez 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).
RaccourciPOST /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

Lister les factures en attentebash
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.

Une facture, avec son journal de rappelsjson
{
  "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

Relancerbash
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…" }
Limites— une facture ne peut être relancée manuellement qu'une fois par heure et 3 fois par jour (429 sinon). Une facture déjà réglée renvoie 400. Ces rappels apparaissent avec type: "MANUAL" dans reminders[].
Régularisation — une facture impayée (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.
Reversements

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

Demander un reversementbash
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"
  }'
ChampTypeRequisDescription
accountIdchaîneRequisIdentifiant d'un compte Mobile Money validé (menu « Comptes Mobile Money » du dashboard).
amountentierRequisMontant à reverser en MGA (≥ 100), dans la limite du solde disponible.
callbackUrlURLOptionnelURL notifiée (POST signé) à la validation/refus. Nécessaire pour recevoir les webhooks.

Réponse 201 Created — le reversement est PENDING :

201 Createdjson
{
  "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"
}
Disponible — le montant ne peut pas dépasser votre solde disponible (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.

Intégration

Webhooks & statuts

Si vous fournissez callbackUrl, nous l'appelons en POST à chaque événement :

payment.successLe paiement a été confirmé (SUCCESS).
payment.expiredLe lien a expiré sans paiement (EXPIRED).
payout.approvedUn reversement a été validé (solde débité).
payout.rejectedUn reversement a été refusé (montant libéré).
invoice.createdUne facture d'abonnement a été émise à l'échéance.
subscription.past_dueRelances épuisées — l'abonnement est impayé.
subscription.reactivatedUne facture impayée a été réglée — l'abonnement repasse actif.
subscription.canceledUn abonnement a été annulé.
Garantie — chaque paiement reçoit toujours un événement terminal : 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.
Webhook reçujson
{
  "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 :

Vérifier la signaturejavascript
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

PENDINGEn attente de paiement.
SUCCESSPaiement réussi et confirmé.
EXPIREDExpiré sans paiement (délai dépassé).

Nous réessayons l'envoi (backoff) jusqu'à recevoir une réponse 2xx de votre serveur.

Paiement tardif — Rarement, un paiement 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).
Intégration

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 :

400 Bad Requestjson
{
  "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).

429 Too Many Requests — les endpoints de connexion sont limités (anti brute-force). En cas de dépassement, patientez une minute avant de réessayer.