Version 1.0 ยท Last updated August 2026
The RwandaPay API provides a secure, PCI-compliant way to integrate mobile money payments (MTN MoMo & Airtel Money) into your application. Built with defense-in-depth security, idempotency guarantees, and fine-grained API key scopes.
API key authentication, HMAC webhooks, idempotency, rate limiting
Consistent JSON responses, standard HTTP methods, clear error codes
Real-time webhooks with polling fallback for maximum reliability
| Header | Format | Description |
|---|---|---|
| X-Public-Key | pk_{environment}_{random} |
Your public API key (identifies your account) |
| X-Secret-Key | sk_{environment}_{random} |
Your secret API key (authenticates requests) |
Idempotency-Key for each financial operation
Idempotency-Key request header
409 ConflictJavaScript / Node.js
const key = crypto.randomUUID();
PHP
$key = bin2hex(random_bytes(16));
Python
import uuid; key = str(uuid.uuid4())
Java
String key = UUID.randomUUID().toString();
/v1/checkout/initialize
New
Creates a hosted checkout session. Returns a checkout URL where your customer can complete payment.
Required Scope: checkout:create
Idempotency: Required
Auth: API Keys Required
| Parameter | Type | Required | Description |
|---|---|---|---|
| amount | decimal | * | Amount in RWF (min: 100, max: 1,000,000) |
| currency | string | โ | Currency code (default: RWF) |
| tx_ref | string | * | Your unique transaction reference |
| customer.name | string | * | Customer's full name |
| customer.phone | string | * | Customer's phone (format: 07XXXXXXXX) |
| customer.email | * | Customer's email | |
| redirect_url | url | โ | URL after payment completion |
| webhook_url | url | โ | Webhook URL for notifications |
| description | string | โ | Payment description |
| meta | object | โ | Custom metadata |
{
"amount": 15000,
"currency": "RWF",
"tx_ref": "CHK-20260810-001",
"customer": {
"name": "Jane Smith",
"phone": "0788123456",
"email": "jane@example.com"
},
"description": "Payment for order #ORD-1234",
"redirect_url": "https://your-site.com/order/confirmation",
"webhook_url": "https://your-site.com/webhook/checkout",
"meta": {
"order_id": "ORD-1234"
}
}{
"success": true,
"message": "Checkout session created successfully",
"data": {
"reference": "CHK-20260810-001",
"session_id": "CHK-ABCDEFGHIJKLMNOP",
"payment_url": "https://pay.rwandapay.rw/checkout/CHK-ABCDEFGHIJKLMNOP",
"status": "pending",
"amount": 15000,
"currency": "RWF",
"mode": "live",
"expires_at": "2026-08-10T12:30:00.000000Z"
}
}/v1/checkout/{session_id}/process
New
Processes a payment from the hosted checkout page. This endpoint is called by the checkout page via AJAX.
Auth: โ ๏ธ No API Keys Required
Source: Browser AJAX
| Parameter | Type | Required | Description |
|---|---|---|---|
| phone | string | * | Customer phone (10 digits, e.g., 0788123456) |
| network | string | * | Network: "MTN" or "Airtel" |
| customer_name | string | * | Customer's full name |
| โ | Customer's email |
{
"phone": "0788123456",
"network": "MTN",
"customer_name": "Jane Smith",
"email": "jane@example.com"
}{
"status": "success",
"message": "Payment initiated. Please check your phone to complete the payment.",
"data": {
"reference": "d234d032-7386-4431-9bd8-90c53eab7e91",
"merchant_reference": "CHK-20260810-001",
"mode": "live",
"redirect_url": "https://pay.rwandapay.rw/checkout/waiting/d234d032-7386-4431-9bd8-90c53eab7e91",
"amount": 15000,
"currency": "RWF"
}
}{
"status": "success",
"message": "Test mode: Payment completed successfully!",
"data": {
"reference": "PAY-TEST-XXXXXXXXXXXX-20260817215342",
"merchant_reference": "CHK-20260810-001",
"mode": "test",
"redirect_url": "https://pay.rwandapay.rw/checkout/success/PAY-TEST-XXXXXXXXXXXX-20260817215342",
"amount": 15000,
"currency": "RWF"
}
}/v1/checkout/{reference}/verify
Verifies the status of a payment. Used for polling from the checkout page.
Auth: โ ๏ธ No API Keys Required
Rate Limit: 30 requests/minute
{
"status": "pending",
"completed": false,
"success": false,
"message": "Waiting for payment confirmation. Please check your phone.",
"mode": "live",
"reference": "d234d032-7386-4431-9bd8-90c53eab7e91"
}{
"status": "successful",
"completed": true,
"success": true,
"message": "Payment successful!",
"redirect_url": "https://pay.rwandapay.rw/checkout/success/PAY-LIVE-XXXXXXXXXXXX-20260817215342",
"amount": 15000,
"mode": "live",
"reference": "PAY-LIVE-XXXXXXXXXXXX-20260817215342"
}{
"status": "failed",
"completed": true,
"success": false,
"message": "Payment failed",
"redirect_url": "https://pay.rwandapay.rw/checkout/failed/PAY-LIVE-XXXXXXXXXXXX-20260817215342",
"reference": "PAY-LIVE-XXXXXXXXXXXX-20260817215342"
}/v1/collections
New
Creates a payment request (collection) that generates a payment link for the customer.
Required Scope: collections:create
Idempotency: Required
Auth: API Keys Required
{
"amount": 5000,
"currency": "RWF",
"reference": "INV-20260810-001",
"customer": {
"name": "John Doe",
"phone": "0788123456",
"email": "john@example.com"
},
"description": "Payment for invoice #INV-20260810-001",
"redirect_url": "https://your-merchant-site.com/payment-success",
"webhook_url": "https://your-merchant-site.com/webhook",
"expires_at": "2026-09-09T00:00:00Z",
"metadata": {
"order_id": "ORD-12345",
"product": "Premium Subscription"
}
}{
"success": true,
"message": "Collection created successfully",
"data": {
"reference": "COL-XXXXXXXXXX-20260810",
"status": "pending",
"amount_type": "fixed",
"amount": 5000,
"fee": 175,
"net_amount": 4825,
"currency": "RWF",
"payment_url": "https://pay.rwandapay.rw/pay/COL-XXXXXXXXXX-20260810",
"customer": {
"name": "John Doe",
"phone": "0788123456",
"email": "john@example.com"
},
"description": "Payment for invoice #INV-20260810-001",
"redirect_url": "https://your-merchant-site.com/payment-success",
"webhook_url": "https://your-merchant-site.com/webhook",
"success_redirect_url": "https://your-merchant-site.com/payment-success?reference=COL-XXXXXXXXXX-20260810&status=successful",
"failed_redirect_url": "https://your-merchant-site.com/payment-success?reference=COL-XXXXXXXXXX-20260810&status=failed",
"expires_at": "2026-09-09T00:00:00Z",
"created_at": "2026-08-10T12:00:00Z",
"metadata": {
"order_id": "ORD-12345",
"product": "Premium Subscription"
}
}
}/v1/collections/{reference}
Retrieves the details of a collection request.
Required Scope: collections:read
{
"success": true,
"data": {
"reference": "COL-XXXXXXXXXX-20260810",
"status": "completed",
"amount_type": "fixed",
"amount": 5000,
"fee": 175,
"net_amount": 4825,
"currency": "RWF",
"customer": {
"name": "John Doe",
"phone": "0788123456",
"email": "john@example.com"
},
"description": "Payment for invoice #INV-20260810-001",
"redirect_url": "https://your-merchant-site.com/payment-success",
"webhook_url": "https://your-merchant-site.com/webhook",
"payment_url": "https://pay.rwandapay.rw/pay/COL-XXXXXXXXXX-20260810",
"paypack_status": "successful",
"paypack_reference": "550e8400-e29b-41d4-a716-446655440000",
"paid_at": "2026-08-10T12:05:00Z",
"created_at": "2026-08-10T12:00:00Z"
}
}/v1/collections/{reference}/status
New
Gets the real-time payment status of a collection.
Required Scope: collections:read
{
"success": true,
"data": {
"reference": "COL-XXXXXXXXXX-20260810",
"collection_status": "pending",
"is_paid": false,
"is_processing": false,
"is_pending": true,
"is_expired": false,
"is_failed": false,
"amount": 5000,
"amount_paid": 0,
"currency": "RWF",
"payment_method": null,
"redirect_url": "https://your-merchant-site.com/payment-success",
"webhook_url": "https://your-merchant-site.com/webhook",
"paypack_status": "pending",
"paypack_reference": "550e8400-e29b-41d4-a716-446655440000",
"processing_status": "pending"
}
}{
"success": true,
"data": {
"reference": "COL-XXXXXXXXXX-20260810",
"collection_status": "completed",
"is_paid": true,
"is_processing": false,
"is_pending": false,
"is_expired": false,
"is_failed": false,
"amount": 5000,
"amount_paid": 5000,
"currency": "RWF",
"payment_method": "MTN",
"redirect_url": "https://your-merchant-site.com/payment-success",
"webhook_url": "https://your-merchant-site.com/webhook",
"paypack_status": "successful",
"paypack_reference": "550e8400-e29b-41d4-a716-446655440000",
"processing_status": "processed",
"success_redirect_url": "https://your-merchant-site.com/payment-success?reference=COL-XXXXXXXXXX-20260810&status=successful",
"redirect_to": "https://your-merchant-site.com/payment-success?reference=COL-XXXXXXXXXX-20260810&status=successful"
}
}/v1/disbursements
New
Initiates an automated withdrawal to a mobile money account.
Required Scope: disbursements:create
Idempotency: Required
{
"amount": 50000,
"currency": "RWF",
"destination": {
"phone": "0788123456",
"network": "MTN"
},
"reference": "WD-20260810-0001",
"description": "Salary payout for August"
}{
"success": true,
"data": {
"reference": "WD-20260810-0001",
"status": "authorized",
"amount": 50000,
"fee": 2000,
"net_amount": 48000,
"currency": "RWF",
"destination": {
"phone": "0788123456",
"network": "MTN"
},
"created_at": "2026-08-10T12:00:00Z"
}
}/v1/disbursements/{reference}
Retrieves the details and current status of a disbursement.
Required Scope: disbursements:read
{
"success": true,
"data": {
"reference": "WD-20260810-0001",
"status": "successful",
"amount": 50000,
"fee": 2000,
"net_amount": 48000,
"currency": "RWF",
"destination": {
"phone": "0788123456",
"network": "MTN"
},
"description": "Salary payout for August",
"paypack_reference": "550e8400-e29b-41d4-a716-446655440001",
"processing_time": "2026-08-10T12:05:00Z",
"created_at": "2026-08-10T12:00:00Z"
}
}Webhooks are RwandaPay's way of sending real-time notifications about payment events to your server. The system uses both webhooks and polling for maximum reliability:
Customer initiates payment from hosted page.
Paypack sends webhook to /webhooks/paypack with payment status.
{
"event_id": "245801aa-9a86-11f1-a305-deadd43720af",
"event_kind": "transaction:processed",
"status": "successful",
"paypack_reference": "d234d032-7386-4431-9bd8-90c53eab7e91"
}
Hosted page polls /api/v1/checkout/{reference}/verify every 3 seconds.
Customer redirected to success/failure page.
<?php
// webhook.php - Your webhook receiver endpoint
function verifySignature($rawBody, $signature, $secret) {
$expected = base64_encode(hash_hmac('sha256', $rawBody, $secret, true));
return hash_equals($expected, trim($signature));
}
// Get raw input
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$secret = 'your_webhook_secret';
// Verify signature
if (!verifySignature($rawBody, $signature, $secret)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
// Parse payload
$payload = json_decode($rawBody, true);
$event = $payload['event_kind'] ?? null;
$status = $payload['status'] ?? null;
$reference = $payload['paypack_reference'] ?? null;
// Process based on event type
if ($event === 'transaction:processed') {
if ($status === 'successful') {
handleSuccessfulPayment($reference, $payload);
} elseif ($status === 'failed') {
handleFailedPayment($reference, $payload);
}
}
// Always return 200 OK
http_response_code(200);
echo json_encode(['status' => 'success']);
function handleSuccessfulPayment($reference, $payload) {
$amount = $payload['amount'] ?? 0;
error_log("Payment successful: $reference - $amount RWF");
// Update your database, send notifications, etc.
}
function handleFailedPayment($reference, $payload) {
error_log("Payment failed: $reference");
// Update your database, notify customer, etc.
}
?>// webhook.js
const crypto = require('crypto');
const express = require('express');
const app = express();
// Raw body middleware
app.use(express.json({
verify: (req, res, buf) => {
req.rawBody = buf;
}
}));
function verifySignature(rawBody, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('base64');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
app.post('/webhook', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const secret = 'your_webhook_secret';
if (!verifySignature(req.rawBody, signature, secret)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const payload = req.body;
const event = payload.event_kind;
const status = payload.status;
const reference = payload.paypack_reference;
console.log(`Webhook received: ${event} - ${status}`);
if (event === 'transaction:processed') {
if (status === 'successful') {
handleSuccessfulPayment(reference, payload);
} else if (status === 'failed') {
handleFailedPayment(reference, payload);
}
}
res.json({ status: 'success' });
});
function handleSuccessfulPayment(reference, payload) {
console.log(`โ
Payment successful: ${reference} - ${payload.amount} RWF`);
// Update your database, send notifications, etc.
}
function handleFailedPayment(reference, payload) {
console.log(`โ Payment failed: ${reference}`);
// Update your database, notify customer, etc.
}
app.listen(3000, () => {
console.log('Webhook server running on port 3000');
});| Event | Description | When Sent |
|---|---|---|
| transaction:processed | Payment processed (successful or failed) | Immediately after provider response |
| payment.successful | Payment completed successfully | When payment is confirmed |
| payment.failed | Payment failed | When payment is rejected |
{
"event_id": "245801aa-9a86-11f1-a305-deadd43720af",
"event_kind": "transaction:processed",
"transaction_kind": "CASHIN",
"paypack_reference": "d234d032-7386-4431-9bd8-90c53eab7e91",
"status": "successful",
"amount": 100,
"number": "250788123456",
"network": "MTN",
"provider": "mtn",
"created_at": "2026-08-17T21:53:43.150125934Z"
}Every webhook includes a X-Webhook-Signature header. Verify it using your webhook secret:
function verifyWebhookSignature($rawBody, $signature, $secret) {
$expected = base64_encode(hash_hmac('sha256', $rawBody, $secret, true));
return hash_equals($expected, trim($signature));
}
// Usage
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$secret = 'whsec_your_webhook_secret';
if (!verifyWebhookSignature($rawBody, $signature, $secret)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
// Process webhook...โ DO:
โ DON'T:
| HTTP Code | Error Code | Description | Retry? |
|---|---|---|---|
| 400 | BAD_REQUEST | Malformed request syntax | โ No |
| 401 | AUTH_FAILED | Invalid API credentials | โ No |
| 403 | MERCHANT_INACTIVE | Merchant account is not active | โ No |
| 409 | DUPLICATE_REFERENCE | Transaction reference already exists | โ No |
| 409 | IDEMPOTENCY_KEY_CONFLICT | Same key with different parameters | โ No |
| 422 | VALIDATION_ERROR | Invalid request parameters | โ Yes (fix params) |
| 429 | RATE_LIMIT_EXCEEDED | Too many requests | โ Yes (after delay) |
| 500 | INTERNAL_SERVER_ERROR | Server-side error | โ Yes (with idempotency) |
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "The amount field is required.",
"details": {
"amount": ["The amount field is required."]
}
}
}| Operation Type | Rate Limit | Time Window |
|---|---|---|
| Read operations | 100 requests | 1 minute |
| Write operations | 50 requests | 1 minute |
| Financial operations | 20 requests | 1 minute |
| Checkout polling | 30 requests | 1 minute |
Secret keys are hashed with password_hash(). Raw secrets are never stored and displayed only once at creation.
Test keys (pk_test_*) cannot perform live operations. Live keys cannot be used in test mode.
Withdrawals use SELECT ... FOR UPDATE row-level locking. Concurrent withdrawals cannot double-spend.
Financial records are append-only. Corrections create new transactions, never modify history.
Our developer support team is here to help you integrate.