3. Intégrez Papi à votre application web
Intégrez Papi dans votre application, notamment la gestion des flux de paiement, des notifications et des paramètres spécifiques à l'environnement.
Environnements
Papi fournit un seul endpoint de production. Pour les tests, utilisez les indicateurs mode test décrits ci-dessous.
- Créer un lien de paiement (transactions en direct) :
https://app.papi.mg/engine/api/payment-links - Relire un lien de paiement :
https://app.papi.mg/engine/api/payment-links/{merchantPaymentReference}(voir Relire un lien de paiement)
https://app.papi.mg/dashboard/api/payment-links est l'URL historique, toujours acceptée pour les intégrations existantes ; utilisez l'URL engine pour les nouvelles.
Vue d'ensemble du flux de paiement
Le processus de paiement se compose des étapes suivantes :
- Obtenez votre clé API depuis le tableau de bord.
- Créez un lien de paiement : Envoyez une requête API pour générer un lien de paiement unique pour le client.
- Redirigez le client : Utilisez le
paymentLinkretourné pour envoyer le client vers la page de paiement sécurisée. - Traitement du paiement : Le client finalise le paiement sur la page sécurisée.
- Recevez une notification : Après le paiement, Papi appelle votre
notificationUrlavec le statut final. - Vérifiez et gérez le résultat : Vérifiez d'abord l'en-tête
X-Papi-Signature, puis contrôlez lenotificationTokenet lemerchantPaymentReference, et mettez à jour votre application. Voir Sécuriser les notifications (callbacks).
Guide pas à pas pour implémenter l'intégration des paiements
Ce guide vous accompagne pas à pas pour intégrer l'API de paiement, depuis la création des pages de redirection jusqu'à la gestion des notifications.
Vous devez configurer deux pages sur votre site web vers lesquelles les clients seront redirigés après le paiement :
- URL de succès – La page affichée après un paiement réussi (ex. : confirmation de commande).
- URL d'échec – La page affichée après un paiement échoué (ex. : message d'erreur avec option de réessai).
Notez ces deux URLs – vous en aurez besoin lors de la création du lien de paiement.
L'endpoint de notification est une URL de callback que vous implémentez pour recevoir les mises à jour de statut automatiques de Papi. Il doit accepter les requêtes POST.
Pourquoi http://localhost:… ne fonctionne pas. Papi appelle votre notificationUrl depuis ses propres serveurs, sur l'Internet public. localhost (comme 127.0.0.1 ou une IP privée type 192.168.x.x) désigne la machine qui fait l'appel : depuis les serveurs de Papi, cette adresse ne pointe pas vers votre poste. La notification n'arrivera donc jamais.
Exposez temporairement votre port local. Utilisez un tunnel qui publie une URL HTTPS publique redirigée vers votre serveur local, par exemple :
# ngrok — expose le port local 3000
ngrok http 3000
# Cloudflare Tunnel — équivalent
cloudflared tunnel --url http://localhost:3000
L'outil affiche une URL publique, par exemple https://abcd-1234.ngrok-free.app.
Construisez la notificationUrl publique. Concaténez l'URL du tunnel et le chemin de votre endpoint :
https://abcd-1234.ngrok-free.app/api/papi/notification
C'est cette valeur (et non http://localhost:3000/api/papi/notification) que vous envoyez dans le champ notificationUrl lors de la création du lien de paiement.
L'URL du tunnel est temporaire. Avec la plupart des offres gratuites, elle change à chaque redémarrage du tunnel (et expire après un délai d'inactivité). Après un redémarrage, récupérez la nouvelle URL et créez un nouveau lien de paiement avec la notificationUrl mise à jour — les liens déjà créés continuent de pointer vers l'ancienne adresse, devenue morte.
Observez la requête reçue et testez les deux cas. Journalisez le corps POST reçu dès l'entrée de votre handler, avant tout traitement. Les tunnels fournissent aussi une interface d'inspection des requêtes (ngrok : http://127.0.0.1:4040) qui permet de voir en-têtes, corps et code de réponse, et de rejouer une requête sans refaire un paiement. Testez ensuite :
- Succès : effectuez un paiement complet et vérifiez la réception d'un
paymentStatusàSUCCESS. - Échec : annulez le paiement ou laissez le lien expirer (
validDurationcourt) pour recevoirFAILED, et vérifiez que votre code ne valide pas la commande.
Précautions minimales
- Utilisez l'URL HTTPS du tunnel, jamais l'URL
http://. - Vérifiez systématiquement l'en-tête
X-Papi-Signature, puis lenotificationTokenet lemerchantPaymentReference, avant de mettre à jour vos données — l'URL du tunnel est publique et n'importe qui peut y envoyer une requête. Voir Sécuriser les notifications (callbacks). - Ne mettez jamais votre clé API dans l'URL de notification (ni dans une query string) : elle apparaîtrait dans les journaux du tunnel et des serveurs intermédiaires.
- Arrêtez le tunnel dès la fin du test : tant qu'il tourne, votre serveur local est accessible depuis Internet.
Exemple de corps de notification envoyé par Papi
{
"paymentStatus": "SUCCESS",
"paymentMethod": "MVOLA",
"currency": "MGA",
"amount": 15000,
"fee": 500,
"clientName": "Nom du client",
"description": "Paiement pour la commande #123",
"merchantPaymentReference": "ORDER-123",
"paymentReference": "c1f4a5b0-6f5e-4e1b-9f0e-2b7d8a9c3d21",
"notificationToken": "xyz789",
"message": "Paiement effectué avec succès.",
"payerEmail": "client@example.com",
"payerPhone": "+261340000000"
}
Explication des champs de notification
| Champ | Type | Description |
|---|---|---|
paymentStatus | string | SUCCESS, PENDING ou FAILED. |
paymentMethod | string | La méthode utilisée (MVOLA, AIRTEL_MONEY, ORANGE_MONEY, BRED). |
currency | string | Code de la devise (toujours MGA). |
displayCurrency | string | La devise que le payeur a vue sur le formulaire (toujours MGA aujourd'hui). |
amount | integer | Montant payé. |
estimatedAmount | integer | Le montant exprimé en displayCurrency (égal à amount tant que seul MGA est pris en charge). |
fee | integer | Frais de transaction déduits. |
clientName | string | Nom du client tel que fourni. |
description | string | La description du paiement que vous avez envoyée. |
merchantPaymentReference | string | Votre référence pour ce paiement — la reference que vous avez envoyée à la création du lien. |
paymentReference | string | La référence de Papi pour ce paiement (un UUID). Elle est retournée comme papiPaymentReference lorsque vous relisez le lien. |
notificationToken | string | Jeton retourné lors de la création du lien de paiement – utilisez-le comme contrôle d'authenticité supplémentaire, après la signature. |
message | string | Informations supplémentaires lisibles par l'humain. |
payerEmail | string | Adresse email du client (si fournie). |
payerPhone | string | Numéro de téléphone du client (si fourni). |
Vérification de la notification
Papi signe chaque notification qu'il envoie à votre notificationUrl. Avant de mettre à jour vos données :
- Vérifiez la signature contenue dans l'en-tête
X-Papi-Signature, avec le secret de signature de votre application. - Vérifiez que
merchantPaymentReferenceetnotificationTokencorrespondent aux valeurs de votre lien de paiement.
Consultez Sécuriser les notifications (callbacks) pour les en-têtes, le secret de signature, la construction de la signature, des exemples de code (Node.js, PHP, Python, Java), un vecteur de test et les bonnes pratiques pour sécuriser votre endpoint.
Une notification n'est envoyée qu'une fois. Si votre endpoint était indisponible ou si l'appel s'est perdu, ne laissez pas la commande en suspens : relisez le lien de paiement avec GET /engine/api/payment-links/{merchantPaymentReference} (voir Relire un lien de paiement) et utilisez le paymentStatus retourné. Vous pouvez aussi renvoyer la notification depuis le tableau de bord : voir Notifications en échec.
Vous enverrez une requête POST pour générer un lien de paiement.
Le corps doit inclure les URLs créées aux étapes 1 et 2, ainsi que les détails du paiement.
Endpoint
POST https://app.papi.mg/engine/api/payment-links
En-têtes
{
"Content-Type": "application/json",
"Token": "<VOTRE_CLÉ_API>"
}
Exemple de corps de requête
{
"amount": 15000.0,
"clientName": "Nom du client",
"reference": "ORDER-123",
"description": "Paiement pour la commande #123",
"successUrl": "https://votreapp.com/paiement-succes",
"failureUrl": "https://votreapp.com/paiement-echec",
"notificationUrl": "https://votreapp.com/paiement-notif",
"validDuration": 60,
"provider": "MVOLA",
"payerEmail": "client@example.com",
"payerPhone": "+261340000000",
"testReason": "QA interne",
"isTestMode": false
}
Explication des champs de la requête
| Champ | Type | Requis | Description |
|---|---|---|---|
clientName | string | ✓ | Nom du client. |
amount | number | ✓ | Montant du paiement (minimum 300). |
reference | string | ✓ | Votre identifiant unique pour ce paiement (ex. : ID de commande). |
description | string | ✓ | Description courte (max 255 caractères). |
successUrl | string | ✗ | URL de redirection après le succès (doit commencer par http(s)://). |
failureUrl | string | ✗ | URL de redirection après l'échec (doit commencer par http(s)://). |
notificationUrl | string | ✗ | Votre endpoint qui reçoit les notifications de paiement (http(s)://). Fortement recommandé ; sans elle, relisez l'issue avec le GET ci-dessous. |
validDuration | integer | ✗ | Durée de validité du lien en heures (défaut : 1, doit être > 0). |
provider | string | ✗ | Restreindre à un seul prestataire : MVOLA, AIRTEL_MONEY, ORANGE_MONEY, BRED. |
payerEmail | string | ✗ | Adresse email du client. |
payerPhone | string | ✗ | Numéro de téléphone du client. |
testReason | string | ✗ | Raison de l'utilisation du mode test (apparaît dans le tableau de bord). |
isTestMode | boolean | ✗ | Définir à true pour activer le mode test (voir la section « Mode Test » ci-dessous). |
Tous les champs, règles de validation, codes d'erreur et exemples : API de création de lien de paiement.
Effectuez la requête avec le corps et les en-têtes de l'étape 3.
En cas de succès, vous recevez une réponse comme celle-ci :
{
"data": {
"amount": 15000.0,
"currency": "MGA",
"linkCreationDateTime": 1788065989011,
"linkExpirationDateTime": 1788281989011,
"paymentLink": "https://payment-form.papi.mg/yourshop/payments/eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJDbGllbnQgTmFtZSIsImV4cCI6MTc4ODI4MTk4OX0.Yx8kQ0rN2mVvL5tJ7pF3cHqA9sWbZ1dE4gT6uK0nX2Y",
"clientName": "Nom du client",
"paymentReference": "ORDER-123",
"description": "Paiement pour la commande #123",
"successUrl": "https://votreapp.com/paiement-succes",
"failureUrl": "https://votreapp.com/paiement-echec",
"notificationUrl": "https://votreapp.com/paiement-notif",
"payerEmail": "client@example.com",
"payerPhone": "+261340000000",
"notificationToken": "xyz789",
"testReason": "QA interne",
"isTestMode": false
}
}
Champs importants de la réponse
| Champ | Description |
|---|---|
paymentLink | L'URL vers laquelle le client doit être redirigé pour payer. Sa forme est https://payment-form.papi.mg/<code-de-votre-application>/payments/<jwt>, où le JWT est émis par Papi et expire avec le lien. |
notificationToken | Conservez-le – vous pourrez le comparer au notificationToken des futures notifications, comme contrôle supplémentaire après la signature. |
paymentReference | Dans cette réponse, il reprend votre reference — la même valeur que celle retournée comme merchantPaymentReference dans les notifications et dans la relecture. Ce n'est pas la référence de paiement de Papi, qui s'appelle paymentReference dans la notification et papiPaymentReference dans la relecture. |
Envoyer deux fois la même requête est sans risque : voir Réessais et idempotence.
Choisissez comment envoyer le client vers la page de paiement. Deux flux sont pris en charge – choisissez celui qui correspond à votre UX.
Option A — Redirection standard
Extrayez le paymentLink de la réponse et redirigez le client :
- Applications web : Ouvrez le lien dans un nouvel onglet de navigateur ou effectuez une redirection HTTP.
- Applications mobiles : Utilisez une WebView ou le navigateur par défaut de l'appareil.
Conseil : Certaines plateformes réinitialisent les WebViews lorsque l'application passe en arrière-plan – gérez cela avec soin pour éviter la perte d'état.
Une fois redirigé, le client finalise le paiement sur la page sécurisée de Papi. Après la transaction, il est renvoyé vers votre successUrl ou failureUrl, et votre notificationUrl reçoit le statut final.

Option B — Fenêtre pop-up intégrée à l'application
Gardez le client sur votre site en ouvrant le paymentLink dans une fenêtre
pop-up et en écoutant un événement postMessage envoyé par Papi lorsque le
paiement est confirmé.
1. Ouvrir la pop-up de paiement
Ouvrez l'URL retournée par le serveur dans une nouvelle fenêtre pop-up. Cela
doit se produire à l'intérieur d'un gestionnaire d'événement utilisateur
(ex. un événement submit de formulaire) — sinon le navigateur bloquera la
pop-up. Nous utilisons JavaScript pour la démonstration, mais la même logique
s'applique dans n'importe quel framework frontend.
var paymentWindow = window.open(
paymentLink, // URL de l'étape précédente
'payment-form-window', // nom de fenêtre réutilisable
'width=500,height=700' // dimensions de la pop-up
);
Conservez une référence à paymentWindow — vous en aurez besoin pour vérifier
que le message provient bien de cette pop-up.
2. Écouter le succès du paiement
Une fois la pop-up ouverte, enregistrez un écouteur message sur la fenêtre
parente. Papi publiera { type: 'PAYMENT_STATUS' } lorsque le paiement est
confirmé.
let PAPI_ORIGIN = 'https://payment-form.papi.mg';
let paymentMessageHandler = null;
let paymentSuccess = false;
function teardownPaymentMessageListener() {
if (paymentMessageHandler) {
window.removeEventListener('message', paymentMessageHandler);
paymentMessageHandler = null;
}
}
function onPaymentSuccessMessage () {
// Votre logique de gestion du succès ici
}
function setupPaymentMessageListener() {
teardownPaymentMessageListener();
paymentMessageHandler = function (event) {
// 1. Rejeter tout ce qui ne vient pas de l'origine Papi
if (event.origin !== PAPI_ORIGIN) return;
// 2. Rejeter tout ce qui ne vient pas de la pop-up ouverte
if (event.source !== paymentWindow) return;
// 3. Ne réagir qu'aux charges utiles PAYMENT_STATUS
if (!event.data || event.data.type !== 'PAYMENT_STATUS') return;
// 4. Éviter le double traitement
if (paymentSuccess) return;
paymentSuccess = true;
teardownPaymentMessageListener();
onPaymentSuccessMessage();
};
window.addEventListener('message', paymentMessageHandler);
}
Appelez setupPaymentMessageListener() immédiatement après window.open()
afin de ne manquer aucun message entre les deux appels :
paymentWindow = window.open(paymentLink, 'payment-form-window', 'width=500,height=700');
setupPaymentMessageListener();
3. Garde-fous de sécurité
Trois vérifications doivent toutes passer avant d'agir sur un message :
| Vérification | Pourquoi c'est important |
|---|---|
event.origin === PAPI_ORIGIN | Rejette les messages provenant de tout autre domaine. Doit correspondre exactement — schéma + hôte, sans barre oblique finale. |
event.source === paymentWindow | Rejette les messages des iframes ou onglets non liés qui partagent la même origine. |
event.data.type === 'PAYMENT_STATUS' | Ignore les autres trafics postMessage sur la même page (analytics, widgets, etc.). |
4. Nettoyage
Retirez toujours l'écouteur une fois terminé — en cas de succès, d'annulation ou lorsque l'utilisateur ferme la pop-up :
// En cas de succès → géré automatiquement dans l'écouteur ci-dessus
// En cas d'annulation (l'utilisateur clique sur « Annuler »)
teardownPaymentMessageListener();
if (paymentWindow && !paymentWindow.closed) { paymentWindow.close(); }
paymentWindow = null;
Pièges courants
| Symptôme | Cause |
|---|---|
| L'écouteur ne se déclenche jamais | Mauvaise correspondance de PAPI_ORIGIN (http vs https, barre oblique finale, port). |
| L'écouteur se déclenche pour des messages non liés | Garde-fou event.source === paymentWindow manquant. |
| La redirection se produit deux fois | Indicateur paymentSuccess manquant ou écouteur non retiré après le premier déclenchement. |
event.source est null | Pop-up fermée avant l'envoi du message — assurez-vous que le message est envoyé avant window.close(). |
Réessais et idempotence
La création d'un lien de paiement est idempotente sur la reference, dans votre boutique, tant que le lien est actif (ni expiré, ni payé, ni désactivé) :
Vous refaites un POST avec la même reference… | Réponse de Papi |
|---|---|
…même amount et même devise, pendant que le premier lien est actif | 200 avec le lien existant : même paymentLink, même notificationToken. La description, les URLs et les coordonnées du payeur du réessai sont ignorées. |
…un amount ou une devise différents, pendant que le premier lien est actif | 409 avec le code PAYMENT_LINK_CONFLICT. Attendez l'expiration du lien ou utilisez une autre référence. |
| …après que le premier lien a été payé, a expiré ou a été désactivé | 200 avec un nouveau lien. |
Un réessai réseau ou un double envoi du checkout ne peut donc jamais laisser deux liens payables pour une même commande. Les requêtes simultanées sont sérialisées : elles reçoivent toutes le même lien.
{
"error": {
"code": "PAYMENT_LINK_CONFLICT",
"message": "Un lien de paiement actif existe déjà pour la référence ORDER-123 avec un montant ou une devise différents. Attendez son expiration ou utilisez une autre référence."
}
}
Les liens créés à la main depuis le tableau de bord sont hors de cette règle : ils ne sont ni retournés ni une raison de refuser.
Règles d'idempotence détaillées, conseils de réessai et codes d'erreur : API de création de lien de paiement.
Relire un lien de paiement
Tout lien créé avec POST /payment-links peut être relu par votre référence marchande. C'est ainsi que vous récupérez l'issue d'un paiement dont la notification ne vous est jamais parvenue, et que vous vérifiez où en est un lien avant d'en réémettre un.
Tous les champs, règles de statut, codes d'erreur et exemples : API de statut de lien de paiement.
Endpoint
GET https://app.papi.mg/engine/api/payment-links/{merchantPaymentReference}
{merchantPaymentReference} est votre référence marchande : le champ reference envoyé à la création du lien, la même valeur que le merchantPaymentReference des réponses et des notifications. Ce n'est pas la référence de paiement Papi. Elle est résolue dans la boutique à laquelle appartient votre Token. Si plusieurs liens ont été créés sous la même référence, un lien payé est retourné s'il en existe un, quel que soit son âge ; sinon le plus récent.
En-têtes
{
"Token": "<VOTRE_CLE_API>"
}
Exemple de réponse
{
"data": {
"linkStatus": "PAID",
"paymentStatus": "SUCCESS",
"paymentMethod": "MVOLA",
"currency": "MGA",
"displayCurrency": "MGA",
"amount": 15000.0,
"clientName": "Nom du client",
"description": "Paiement pour la commande #123",
"merchantPaymentReference": "ORDER-123",
"papiPaymentReference": "c1f4a5b0-6f5e-4e1b-9f0e-2b7d8a9c3d21",
"notificationToken": "xyz789",
"message": null,
"payerEmail": "client@example.com",
"payerPhone": "+261340000000",
"paymentLink": "https://payment-form.papi.mg/votreboutique/payments/eyJhbGciOiJIUzI1NiJ9...",
"shortLink": "https://link.papi.mg/8NW7R",
"linkCreationDateTime": 1788065989011,
"linkExpirationDateTime": 1788281989011,
"isTestMode": false
}
}
Champs de la réponse
Les champs portent les mêmes noms et la même signification que le corps de la notification, à une exception près : la référence Papi s'appelle ici papiPaymentReference (paymentReference dans la notification) pour ne jamais être confondue avec votre merchantPaymentReference.
| Champ | Type | Description |
|---|---|---|
linkStatus | string | État du lien : ACTIVE (encore payable), EXPIRED, PAID ou DISABLED. PAID l'emporte sur DISABLED et EXPIRED. |
paymentStatus | string | Issue du paiement effectué via le lien : SUCCESS, PENDING ou FAILED — les mêmes valeurs que la notification. null tant que personne n'a tenté de payer. |
paymentMethod | string | Prestataire utilisé par le payeur (MVOLA, AIRTEL_MONEY, ORANGE_MONEY, BRED). null tant qu'aucune tentative de paiement n'existe. |
currency | string | Code de la devise (toujours MGA). |
displayCurrency | string | Devise affichée au payeur. |
amount | number | Montant du lien. |
clientName | string | Nom du client tel que fourni. |
description | string | La description que vous avez envoyée. |
merchantPaymentReference | string | Votre reference de la requête de création. |
papiPaymentReference | string | Référence Papi de la tentative de paiement — le paymentReference de la notification. null tant qu'aucune tentative n'existe. |
notificationToken | string | Le jeton retourné à la création du lien. |
message | string | Motif d'échec de la tentative de paiement, s'il y en a un. |
payerEmail | string | Email du client (si fourni). |
payerPhone | string | Numéro de téléphone du client (si fourni). |
paymentLink | string | L'URL de paiement, telle que retournée à la création. |
shortLink | string | Forme courte de l'URL de paiement, si elle a été générée. |
linkCreationDateTime | integer | Date de création, en millisecondes epoch. |
linkExpirationDateTime | integer | Date d'expiration, en millisecondes epoch. |
isTestMode | boolean | Indique si le lien a été marqué comme test. |
Lire les deux statuts ensemble
linkStatus | paymentStatus | Signification |
|---|---|---|
ACTIVE | null | Personne n'a encore tenté de payer. |
ACTIVE | FAILED | La dernière tentative a été refusée ou abandonnée ; le payeur peut réessayer sur le même lien. |
ACTIVE | PENDING | Une tentative est en cours chez le prestataire. Revérifiez sous peu. |
PAID | SUCCESS | Le paiement est passé. Confirmez la commande. |
EXPIRED | null / FAILED | Le lien a expiré avant un paiement réussi. Créez-en un nouveau. |
DISABLED | null / FAILED | Le lien a été désactivé depuis le tableau de bord. |
Erreurs
| Statut | Signification |
|---|---|
401 | En-tête Token absent ou inconnu. |
404 | Aucun lien avec cette reference dans votre boutique. Une référence utilisée par une autre boutique renvoie aussi 404. |
Mode Test
Papi propose deux façons de tester votre intégration :
1. Indicateur isTestMode dans la requête
- Définissez
"isTestMode": truedans le corps de la requête. - La transaction est marquée comme test dans votre tableau de bord, mais de l'argent réel est quand même déplacé.
- Utile pour les tests de bout en bout avec de vrais prestataires (sauf pour le mobile money, qui ne prend pas en charge les transactions de test non réelles).
2. Mode Test de l'application (cartes uniquement)
- Dans les paramètres de votre boutique dans le tableau de bord, activez le Mode Test.
- Utilisez les informations de la carte de test suivante :
- Numéro de carte :
4000 0000 0000 5126 - Date d'expiration :
01/2028 - CVV :
123
- Numéro de carte :
- Cela simule un paiement par carte sans mouvement de fonds réels.
Récapitulatif du flux de travail
- Obtenez votre clé API depuis le tableau de bord (
Icône avatar → Boutiques → sélectionnez la boutique → onglet Développeur). - Créez un lien de paiement en envoyant une requête
POSTavec les champs requis. Les réessais sont sans risque : la mêmereferenceavec le même montant retourne le même lien. - Redirigez le client vers le
paymentLinkretourné. - Recevez une notification sur votre
notificationUrllorsque le statut du paiement change. - Vérifiez la notification : contrôlez d'abord l'en-tête
X-Papi-Signature, puismerchantPaymentReferenceetnotificationToken. Voir Sécuriser les notifications (callbacks). - Mettez à jour vos enregistrements et informez le client.
- Relisez le lien avec
GET /engine/api/payment-links/{merchantPaymentReference}dès qu'une notification manque ou qu'une issue doit être revérifiée.
En suivant ces étapes, vous pouvez accepter des paiements en ligne de manière sécurisée avec Papi. Testez toujours minutieusement en mode test avant de passer en production.