Quick Start
This guide is designed for developers to help you integrate Papi into your website or application. Follow these steps to set up a payment system that works seamlessly for your customers.
Prerequisites
- Your application is online and ready to accept payments.
- You have your API Key from the Papi dashboard (see Where to Find Your API Key below).
- Choose a URL that you want Papi to redirect to after a payment success (
successUrl). - Choose a URL that you want Papi to redirect to after a payment failure (
failureUrl). - Choose a URL for your server that will receive payment notifications from Papi (
notificationUrl).
Where to Find Your API Key
- Log in to your dashboard: https://dashboard.papi.mg.
- In the top right corner, click your avatar icon (your profile picture or initials).
- Click Boutiques in the dropdown menu.
- Click on the application you want to use.
- Inside the application's dashboard, click the Developer tab.
- Under API Key, you will see your key (a long string).
Important: Keep your API Key secret. Anyone with this key can create payments on your behalf.
Workflow Overview
Before diving into the steps, here's how everything will fit together:
- A customer places an order on your website.
- You create a secure payment link using Papi.
- The customer is redirected to the payment page to complete their payment.
- Papi sends the payment result to your system using your notification URL (POST request).
- Your system verifies the notification and updates the order status (success or failure).
- The customer is shown the result on your website (success or failure).
Step 1: Generate a payment link
To let customers pay, you need to create a secure link that will direct them to a payment page. This link includes the payment amount, customer details, and the URL where Papi will notify your system about the payment result.
What You Need to Do
Send a POST request to the following endpoint:
POST https://app.papi.mg/dashboard/api/payment-links
Authentication
Every request must include your API Key in the headers:
Content-Type: application/json
Token: <YOUR_API_KEY>
Request body
Send a JSON body with the required and optional fields. Example:
{
"amount": 15000.0,
"clientName": "Client Name",
"reference": "ORDER-123",
"description": "Payment for Order #123",
"successUrl": "https://yourapp.com/payment-success",
"failureUrl": "https://yourapp.com/payment-failure",
"notificationUrl": "https://yourapp.com/payment-notify",
"validDuration": 60,
"provider": "MVOLA",
"payerEmail": "customer@example.com",
"payerPhone": "+261340000000",
"testReason": "Internal QA",
"isTestMode": false
}
| Field name | Type | Required | Description |
|---|---|---|---|
| clientName | string | ✓ | Customer's name. |
| amount | number | ✓ | Payment amount (>= 300). |
| reference | string | ✓ | Your unique reference for this payment. |
| description | string | ✓ | Payment description (max 255 characters). |
| successUrl | string | × | URL to redirect after success (http(s)://). |
| failureUrl | string | × | URL to redirect after failure (http(s)://). |
| notificationUrl | string | ✓ | URL to receive payment notifications (http(s)://). |
| validDuration | integer | × | Validity in minutes (>0). Default: 1. |
| provider | string | × | One of: MVOLA, ARTEL_MONEY, ORANGE_MONEY, BRED. |
| payerEmail | string | × | Customer's email address. |
| payerPhone | string | × | Customer's phone number (e.g., +261340000000). |
| testReason | string | × | Reason for test mode (if isTestMode=true). |
| isTestMode | boolean | × | Set to true to enable test mode. |
Success response
If your request is valid, you receive:
{
"data": {
"amount": 15000.0,
"currency": "MGA",
"linkCreationDateTime": 1723850012,
"linkExpirationDateTime": 1723853612,
"paymentLink": "https://pay.papi.mg/payment/abc123",
"clientName": "Client Name",
"paymentReference": "ORDER-123",
"description": "Payment for Order #123",
"successUrl": "https://yourapp.com/payment-success",
"failureUrl": "https://yourapp.com/payment-failure",
"notificationUrl": "https://yourapp.com/payment-notify",
"payerEmail": "customer@example.com",
"payerPhone": "+261340000000",
"notificationToken": "xyz789",
"testReason": "Internal QA",
"isTestMode": false
}
}
paymentLink— Redirect your customer to this URL to complete the payment.notificationToken— Store this and use it later to verify that notifications are genuine.
Error response
If something goes wrong, you may receive:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Le montant est requis"
}
}
Examples
- Java Spring
- Node
- NestJS
- React
- PHP
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.*;
@Service
public class PaymentService {
private static final String API_URL = "https://app.papi.mg/dashboard/api/payment-links";
private static final String API_KEY = "<YOUR_API_KEY>";
public Map<String, Object> createPaymentLink() {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Token", API_KEY);
Map<String, Object> body = new LinkedHashMap<>();
body.put("amount", 15000.0);
body.put("clientName", "Client Name");
body.put("reference", "ORDER-123");
body.put("description", "Payment for Order #123");
body.put("successUrl", "https://yourapp.com/payment-success");
body.put("failureUrl", "https://yourapp.com/payment-failure");
body.put("notificationUrl", "https://yourapp.com/payment-notify");
body.put("validDuration", 60);
body.put("provider", "MVOLA");
body.put("payerEmail", "customer@example.com");
body.put("payerPhone", "+261340000000");
HttpEntity<Map<String, Object>> request = new HttpEntity<>(body, headers);
ResponseEntity<Map> response = restTemplate.postForEntity(API_URL, request, Map.class);
Map<String, Object> data = (Map<String, Object>) response.getBody().get("data");
String paymentLink = (String) data.get("paymentLink");
String notificationToken = (String) data.get("notificationToken");
// Store notificationToken to verify notifications later
System.out.println("Payment Link: " + paymentLink);
return data;
}
}
const https = require('https');
const data = JSON.stringify({
amount: 15000.0,
clientName: 'Client Name',
reference: 'ORDER-123',
description: 'Payment for Order #123',
successUrl: 'https://yourapp.com/payment-success',
failureUrl: 'https://yourapp.com/payment-failure',
notificationUrl: 'https://yourapp.com/payment-notify',
validDuration: 60,
provider: 'MVOLA',
payerEmail: 'customer@example.com',
payerPhone: '+261340000000',
});
const options = {
hostname: 'app.papi.mg',
path: '/dashboard/api/payment-links',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Token': '<YOUR_API_KEY>',
'Content-Length': Buffer.byteLength(data),
},
};
const req = https.request(options, (res) => {
let body = '';
res.on('data', (chunk) => body += chunk);
res.on('end', () => {
const result = JSON.parse(body);
if (result.data?.paymentLink) {
const paymentLink = result.data.paymentLink;
const notificationToken = result.data.notificationToken;
// Store notificationToken to verify notifications later
console.log('Payment Link:', paymentLink);
} else {
console.error('Error:', result.error);
}
});
});
req.on('error', (e) => console.error('Error:', e));
req.write(data);
req.end();
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';
@Injectable()
export class PaymentService {
constructor(private readonly httpService: HttpService) {}
async createPaymentLink(): Promise<{ paymentLink: string; notificationToken: string }> {
const { data } = await firstValueFrom(
this.httpService.post(
'https://app.papi.mg/dashboard/api/payment-links',
{
amount: 15000.0,
clientName: 'Client Name',
reference: 'ORDER-123',
description: 'Payment for Order #123',
successUrl: 'https://yourapp.com/payment-success',
failureUrl: 'https://yourapp.com/payment-failure',
notificationUrl: 'https://yourapp.com/payment-notify',
validDuration: 60,
provider: 'MVOLA',
payerEmail: 'customer@example.com',
payerPhone: '+261340000000',
},
{
headers: {
'Content-Type': 'application/json',
Token: '<YOUR_API_KEY>',
},
},
),
);
// Store data.data.notificationToken to verify notifications later
return {
paymentLink: data.data.paymentLink,
notificationToken: data.data.notificationToken,
};
}
}
// React (frontend) must call your own backend — never expose your API key in browser code.
async function initiatePayment(order: { amount: number; reference: string }) {
const response = await fetch('/api/create-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
amount: order.amount,
clientName: 'Client Name',
reference: order.reference,
description: `Payment for order ${order.reference}`,
}),
});
const { paymentLink } = await response.json();
// Redirect the customer to Papi's payment page
window.location.href = paymentLink;
}
<?php
$apiUrl = "https://app.papi.mg/dashboard/api/payment-links";
$apiKey = "<YOUR_API_KEY>";
$data = [
"amount" => 15000.0,
"clientName" => "Client Name",
"reference" => "ORDER-123",
"description" => "Payment for Order #123",
"successUrl" => "https://yourapp.com/payment-success",
"failureUrl" => "https://yourapp.com/payment-failure",
"notificationUrl" => "https://yourapp.com/payment-notify",
"validDuration" => 60,
"provider" => "MVOLA",
"payerEmail" => "customer@example.com",
"payerPhone" => "+261340000000",
];
$options = [
"http" => [
"header" => "Content-Type: application/json\r\nToken: $apiKey\r\n",
"method" => "POST",
"content" => json_encode($data),
],
];
$context = stream_context_create($options);
$response = file_get_contents($apiUrl, false, $context);
if ($response !== false) {
$result = json_decode($response, true);
if (isset($result["data"]["paymentLink"])) {
$paymentLink = $result["data"]["paymentLink"];
$notificationToken = $result["data"]["notificationToken"] ?? null;
// Store $notificationToken to verify notifications later
echo "Payment Link: $paymentLink\n";
} else {
echo "Error: " . json_encode($result["error"] ?? $result) . "\n";
}
} else {
echo "Failed to generate payment link.\n";
}
?>
Step 2: Redirect the customer to the payment page
Once the payment link is generated, your customer must complete the payment using that link.
What You Need to Do
- Extract
paymentLinkfrom the response of Step 1. - Redirect the customer to that URL (e.g. open in the same tab, new tab, or WebView in mobile apps).
Mobile apps: Use a WebView or the device's default browser. Note that some platforms reset WebViews when the app goes to the background; handle this to avoid losing state.
Examples
- Java Spring
- Node (Express)
- NestJS
- React
- PHP
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
@Controller
public class CheckoutController {
private final PaymentService paymentService;
public CheckoutController(PaymentService paymentService) {
this.paymentService = paymentService;
}
@PostMapping("/checkout")
public void checkout(HttpServletResponse response) throws IOException {
Map<String, Object> data = paymentService.createPaymentLink();
String paymentLink = (String) data.get("paymentLink");
response.sendRedirect(paymentLink);
}
}
// In your Express route handler, after obtaining paymentLink from Step 1:
app.post('/checkout', async (req, res) => {
const { paymentLink } = await createPaymentLink(req.body); // your Step 1 function
res.redirect(302, paymentLink);
});
import { Controller, Post, Res } from '@nestjs/common';
import { Response } from 'express';
@Controller('checkout')
export class CheckoutController {
constructor(private readonly paymentService: PaymentService) {}
@Post()
async checkout(@Res() res: Response) {
const { paymentLink } = await this.paymentService.createPaymentLink();
return res.redirect(302, paymentLink);
}
}
// After receiving paymentLink from your backend (Step 1):
function redirectToPayment(paymentLink: string) {
window.location.href = paymentLink;
// To open in a new tab instead:
// window.open(paymentLink, '_blank');
}
<?php
// After obtaining $paymentLink from Step 1:
header("Location: " . $paymentLink);
exit();
?>
Step 3: Set up a notification endpoint
After the customer completes the payment on Papi's payment page, Papi sends a POST request to your notificationUrl to inform your system of the payment result.
What you need to do
- Create an endpoint that accepts POST requests and reads the JSON body (the URL you gave as
notificationUrlin Step 1). - In this script, verify the notification, then update your system (e.g. mark the order as paid or failed).
Notification payload (example)
Papi sends a JSON body like:
{
"paymentStatus": "SUCCESS",
"paymentMethod": "MVOLA",
"currency": "MGA",
"amount": 15000,
"fee": 500,
"clientName": "Client Name",
"description": "Payment for Order #123",
"merchantPaymentReference": "MERCHANT-0001",
"paymentReference": "ORDER-123",
"notificationToken": "xyz789",
"message": "Payment completed successfully.",
"payerEmail": "customer@example.com",
"payerPhone": "+261340000000"
}
| Field name | Type | Description |
|---|---|---|
| paymentStatus | string | SUCCESS, PENDING, or FAILED. |
| paymentMethod | string | The method used (e.g. MVOLA). |
| currency | string | Currency code. |
| amount | integer | Paid amount. |
| fee | integer | Transaction fee. |
| clientName | string | Customer's name. |
| description | string | Your description. |
| merchantPaymentReference | string | Payment system reference. |
| paymentReference | string | Your reference (same as in Step 1). |
| notificationToken | string | Use to verify authenticity. |
| message | string | Additional details. |
| payerEmail | string | Customer email. |
| payerPhone | string | Customer phone. |
How to verify the notification
- Check that
paymentReferencematches the reference you sent when creating the payment link. - Check that
notificationTokenmatches the token you received in the Step 1 response.
If both match, treat the notification as genuine and update your records.
Examples
- Java Spring
- Node
- NestJS
- React
- PHP
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
public class NotificationController {
@PostMapping("/payment-notify")
public ResponseEntity<Void> handleNotification(@RequestBody Map<String, Object> payload) {
String paymentReference = (String) payload.getOrDefault("paymentReference", "");
String notificationToken = (String) payload.getOrDefault("notificationToken", "");
String paymentStatus = (String) payload.getOrDefault("paymentStatus", "");
Number amount = (Number) payload.getOrDefault("amount", 0);
// Verify against the values stored when creating the payment link
String expectedToken = "xyz789"; // Retrieve from your database
String expectedReference = "ORDER-123"; // Retrieve from your database
if (!paymentReference.equals(expectedReference) || !notificationToken.equals(expectedToken)) {
return ResponseEntity.status(403).build();
}
if ("SUCCESS".equals(paymentStatus)) {
// Mark order as paid in your database
System.out.println("Payment " + paymentReference + ": SUCCESS (amount: " + amount + ")");
} else if ("FAILED".equals(paymentStatus)) {
// Handle failure
System.out.println("Payment " + paymentReference + ": FAILED");
} else {
System.out.println("Payment " + paymentReference + ": " + paymentStatus);
}
return ResponseEntity.ok().build();
}
}
const express = require('express');
const router = express.Router();
router.post('/payment-notify', express.json(), (req, res) => {
const { paymentReference, notificationToken, paymentStatus, amount } = req.body;
// Verify against the values stored when creating the payment link
const expectedToken = 'xyz789'; // Retrieve from your database
const expectedReference = 'ORDER-123'; // Retrieve from your database
if (paymentReference !== expectedReference || notificationToken !== expectedToken) {
return res.status(403).end();
}
if (paymentStatus === 'SUCCESS') {
console.log(`Payment ${paymentReference}: SUCCESS (amount: ${amount})`);
// Mark order as paid in your database
} else if (paymentStatus === 'FAILED') {
console.log(`Payment ${paymentReference}: FAILED`);
// Handle failure
} else {
console.log(`Payment ${paymentReference}: ${paymentStatus}`);
}
res.status(200).end();
});
module.exports = router;
import { Controller, Post, Body, HttpCode, HttpStatus, ForbiddenException } from '@nestjs/common';
interface NotificationPayload {
paymentReference: string;
notificationToken: string;
paymentStatus: string;
amount: number;
}
@Controller()
export class NotificationController {
@Post('payment-notify')
@HttpCode(HttpStatus.OK)
handleNotification(@Body() payload: NotificationPayload): void {
const { paymentReference, notificationToken, paymentStatus, amount } = payload;
// Verify against the values stored when creating the payment link
const expectedToken = 'xyz789'; // Retrieve from your database
const expectedReference = 'ORDER-123'; // Retrieve from your database
if (paymentReference !== expectedReference || notificationToken !== expectedToken) {
throw new ForbiddenException();
}
if (paymentStatus === 'SUCCESS') {
console.log(`Payment ${paymentReference}: SUCCESS (amount: ${amount})`);
// Mark order as paid in your database
} else if (paymentStatus === 'FAILED') {
console.log(`Payment ${paymentReference}: FAILED`);
} else {
console.log(`Payment ${paymentReference}: ${paymentStatus}`);
}
}
}
// Notification endpoints are server-side — React handles the result display.
// Use this hook to poll your backend for the payment status after redirect.
import { useEffect, useState } from 'react';
function usePaymentStatus(reference: string) {
const [status, setStatus] = useState<'PENDING' | 'SUCCESS' | 'FAILED'>('PENDING');
useEffect(() => {
const interval = setInterval(async () => {
const res = await fetch(`/api/payment-status?reference=${reference}`);
const data = await res.json();
if (data.status === 'SUCCESS' || data.status === 'FAILED') {
setStatus(data.status);
clearInterval(interval);
}
}, 3000);
return () => clearInterval(interval);
}, [reference]);
return status;
}
<?php
// payment-notify.php (or the path matching your notificationUrl)
$data = json_decode(file_get_contents("php://input"), true);
if (!$data) {
http_response_code(400);
exit();
}
$paymentReference = $data["paymentReference"] ?? "";
$notificationToken = $data["notificationToken"] ?? "";
$paymentStatus = $data["paymentStatus"] ?? "";
$amount = $data["amount"] ?? 0;
// Verify: compare with the reference and notificationToken you stored when creating the link
$expectedToken = "xyz789"; // Retrieve the token you stored in Step 1 for this payment
$expectedReference = "ORDER-123"; // Or from your database for this order
if ($paymentReference !== $expectedReference || $notificationToken !== $expectedToken) {
http_response_code(403);
exit();
}
if ($paymentStatus === "SUCCESS") {
// Mark order as paid in your database
error_log("Payment $paymentReference: SUCCESS (amount: $amount)");
} elseif ($paymentStatus === "FAILED") {
error_log("Payment $paymentReference: FAILED");
// Handle failure (e.g. notify the customer)
} else {
// PENDING or other
error_log("Payment $paymentReference: $paymentStatus");
}
http_response_code(200); // Tell Papi the notification was received
?>
Step 4: Create pages for payment results
When the payment process is complete, Papi shows the user a success or failure message, then redirects them to the URLs you provided in Step 1 (successUrl and failureUrl).
What you need to do
Option 1: Use a single URL for both success and failure (e.g. return the user to the home page or order details).
Option 2: Use two separate pages:
- A page for successful payments, served at the URL you set as
successUrl(e.g. next steps, delivery info). - A page for failed payments, served at the URL you set as
failureUrl(e.g. "Payment failed", "Try again" or contact support).
Test mode
To test your integration:
isTestMode=true— Marks the transaction as a test (note: may still move real money depending on configuration).- Application Test Mode (cards only) — In your dashboard, enable Test Mode in the Application settings. You can use this test card:
- Card Number: 4000 0000 0000 5126
- Expiry: 01/2028
- CVV: 123
Note: Mobile money does not support non-real test transactions.