Quick Starts
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
- PHP
- JavaScript
- Python
- Java
- Go
- NestJS
- React
<?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";
}
?>
const response = await fetch("https://app.papi.mg/dashboard/api/payment-links", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Token": "<YOUR_API_KEY>",
},
body: 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 data = await response.json();
if (data.data?.paymentLink) {
const paymentLink = data.data.paymentLink;
const notificationToken = data.data.notificationToken;
// Store notificationToken to verify notifications later
console.log("Payment Link:", paymentLink);
} else {
console.error("Error:", data.error);
}
import requests
url = "https://app.papi.mg/dashboard/api/payment-links"
headers = {
"Content-Type": "application/json",
"Token": "<YOUR_API_KEY>",
}
payload = {
"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",
}
response = requests.post(url, headers=headers, json=payload)
data = response.json()
if "data" in data and "paymentLink" in data["data"]:
payment_link = data["data"]["paymentLink"]
notification_token = data["data"].get("notificationToken")
# Store notification_token to verify notifications later
print("Payment Link:", payment_link)
else:
print("Error:", data.get("error"))
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class PapiPayment {
public static void main(String[] args) throws Exception {
String apiUrl = "https://app.papi.mg/dashboard/api/payment-links";
String apiKey = "<YOUR_API_KEY>";
String body = """
{
"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"
}
""";
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(apiUrl))
.header("Content-Type", "application/json")
.header("Token", apiKey)
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("Response: " + response.body());
// Parse with a JSON library (e.g. Jackson, Gson) to extract paymentLink and notificationToken
}
}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
func main() {
apiURL := "https://app.papi.mg/dashboard/api/payment-links"
apiKey := "<YOUR_API_KEY>"
payload := map[string]interface{}{
"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",
}
jsonBody, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonBody))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Token", apiKey)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error:", err)
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println("Response:", string(body))
// Unmarshal body to extract paymentLink and notificationToken
}
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;
}
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
- PHP
- JavaScript (Express)
- Python (Flask)
- Java (Servlet)
- Go
- NestJS
- React
<?php
// After obtaining $paymentLink from Step 1:
header("Location: " . $paymentLink);
exit();
?>
// 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);
});
from flask import redirect
@app.route('/checkout', methods=['POST'])
def checkout():
payment_link = create_payment_link() # your Step 1 function
return redirect(payment_link, code=302)
// In your servlet or Spring MVC controller, after obtaining paymentLink from Step 1:
response.sendRedirect(paymentLink);
// In your HTTP handler, after obtaining paymentLink from Step 1:
http.Redirect(w, r, paymentLink, http.StatusFound)
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');
}
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.
Example PHP notification endpoint
<?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") {
file_put_contents("log.txt", "Payment $paymentReference: SUCCESS (amount: $amount)\n", FILE_APPEND);
// Update your database to mark the order as paid
} elseif ($paymentStatus === "FAILED") {
file_put_contents("log.txt", "Payment $paymentReference: FAILED\n", FILE_APPEND);
// Handle failure (e.g. notify the customer)
} else {
// PENDING or other
file_put_contents("log.txt", "Payment $paymentReference: $paymentStatus\n", FILE_APPEND);
}
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.