The RwandaPay API provides a simple, secure, and PCI-compliant way to integrate mobile money payments (MTN MoMo & Airtel Money) into your application. Our API follows industry-standard patterns similar to Stripe, Flutterwave, and Paystack — returning JSON responses with redirect URLs that you control.
< 200ms response time
Secure hosted checkout
Enterprise reliability
Dedicated assistance
All API requests require authentication using your API keys. Include the following headers with every request:
| X-Public-Key | Your public API key | pk_live_xxxxxxxx or pk_test_xxxxxxxx |
| X-Secret-Key | Your secret API key | sk_live_xxxxxxxx or sk_test_xxxxxxxx |
X-Public-Key: YOUR_PUBLIC_KEY
X-Secret-Key: YOUR_SECRET_KEY
Content-Type: application/json🔐 Getting your API keys
Log in to your merchant dashboard → Settings → API Keys to generate your API keys. Test keys (pk_test_*) work in sandbox mode and do NOT process real payments.
Use pk_test_* keys. Simulates payments without real money.
✨ Test Mode Features:
Use pk_live_* keys. Processes real money transactions.
💰 Live Mode Features:
https://pay.rwandapay.rw/api/v1The payment flow follows industry-standard patterns (Stripe/Flutterwave):
/checkout/initialize with payment details and your redirect_url.payment_url. Your app redirects customer to this hosted payment page.redirect_url with ?reference=xxx&status=successful&transaction_id=xxxwebhook_url for server-side verification./checkout/initializeCreates a new payment session and returns a secure payment URL. Your application MUST redirect the customer to this URL to complete payment.
| Parameter | Type | Required | Description |
|---|---|---|---|
| amount | decimal | * | Amount in RWF (min: 100, max: 1,000,000) |
| tx_ref | string | * | Unique transaction reference for your order (max 50 chars) |
| customer.name | string | * | Customer's full name |
| customer.email | * | Customer's email address (receipt sent here) | |
| customer.phone | string | * | Customer's phone number (e.g., 0788123456) |
| currency | string | ○ | Currency code (default: RWF, supported: RWF, USD, EUR) |
| redirect_url | url | ○ | URL to redirect customer after payment. If not provided, uses RwandaPay success page. |
| webhook_url | url | ○ | URL for payment confirmation webhook (server-to-server notification) |
| description | string | ○ | Payment description (max 500 chars) |
| meta | object | ○ | Additional metadata for your reference |
{
"success": true,
"message": "Checkout session created successfully",
"data": {
"reference": "ORDER-1780943716",
"session_id": "CHK-V0CFMJGNI8ZWIJBR",
"payment_url": "https://pay.rwandapay.rw/checkout/CHK-V0CFMJGNI8ZWIJBR",
"status": "pending",
"amount": 8000,
"currency": "RWF",
"mode": "test",
"expires_at": "2024-12-15T14:30:00Z"
}
}{
"success": true,
"message": "Checkout session created successfully",
"data": {
"reference": "ORDER-1780943716",
"session_id": "CHK-V0CFMJGNI8ZWIJBR",
"payment_url": "https://pay.rwandapay.rw/checkout/CHK-V0CFMJGNI8ZWIJBR",
"status": "pending",
"amount": 8000,
"currency": "RWF",
"mode": "live",
"expires_at": "2024-12-15T14:30:00Z"
}
}async function initializePayment(paymentData, apiKeys) {
// Validate input
if (!paymentData.amount || paymentData.amount < 100) {
throw new Error('Amount must be at least 100 RWF');
}
if (!paymentData.customer?.phone) {
throw new Error('Customer phone number is required');
}
// Disable button and show loading
const payButton = document.getElementById('pay-button');
if (payButton) {
payButton.disabled = true;
payButton.innerHTML = '<div class="spinner"></div> Processing...';
}
try {
const response = await fetch('https://pay.rwandapay.rw/api/v1/checkout/initialize', {
method: 'POST',
headers: {
'X-Public-Key': apiKeys.publicKey,
'X-Secret-Key': apiKeys.secretKey,
'Content-Type': 'application/json'
},
body: JSON.stringify({
amount: paymentData.amount,
tx_ref: paymentData.tx_ref || 'ORDER-' + Date.now(),
currency: paymentData.currency || 'RWF',
customer: {
name: paymentData.customer.name,
email: paymentData.customer.email,
phone: paymentData.customer.phone
},
redirect_url: paymentData.redirect_url || window.location.href + '/callback',
webhook_url: paymentData.webhook_url,
description: paymentData.description,
meta: paymentData.meta || {}
})
});
const result = await response.json();
if (result.success) {
// Redirect to hosted payment page
window.location.href = result.data.payment_url;
return result.data;
} else {
throw new Error(result.message || 'Payment initialization failed');
}
} catch (error) {
console.error('Payment error:', error);
showErrorToast(error.message);
return null;
} finally {
if (payButton) {
payButton.disabled = false;
payButton.innerHTML = 'Pay Now';
}
}
}
// Usage example
initializePayment({
amount: 5000,
tx_ref: 'ORDER-' + Date.now(),
customer: {
name: 'John Doe',
email: 'john@example.com',
phone: '0788123456'
},
redirect_url: 'https://yoursite.com/payment-callback',
webhook_url: 'https://yoursite.com/api/webhook',
description: 'Payment for Order #12345'
}, {
publicKey: 'YOUR_PUBLIC_KEY',
secretKey: 'YOUR_SECRET_KEY'
});🔑 Important: Payment URL
The payment_url contains the unique session ID. Use the reference from the response for polling status.
/checkout/{reference}/verifyCheck payment status using the reference from the checkout response. Poll this endpoint every 3-5 seconds until payment is confirmed.
{
"status": "pending",
"completed": false,
"success": false,
"message": "Waiting for payment confirmation...",
"elapsed_seconds": 15,
"mode": "live"
}{
"status": "successful",
"completed": true,
"success": true,
"message": "Payment successful!",
"redirect_url": "https://yoursite.com/callback?reference=ORDER-123&status=successful&transaction_id=PAY-XXX",
"amount": 5000,
"mode": "live"
}{
"status": "successful",
"completed": true,
"success": true,
"message": "Test payment successful!",
"redirect_url": "https://yoursite.com/callback?reference=ORDER-123&status=successful",
"amount": 5000,
"mode": "test"
}{
"status": "failed",
"completed": true,
"success": false,
"message": "Payment failed",
"redirect_url": "https://yoursite.com/callback?reference=ORDER-123&status=failed"
}function pollPaymentStatus(reference, redirectUrl) {
let attempts = 0;
const maxAttempts = 60; // 60 * 3 seconds = 3 minutes max
let interval;
function checkStatus() {
attempts++;
console.log(`Checking payment status (attempt ${attempts}/${maxAttempts})...`);
fetch(`https://pay.rwandapay.rw/api/v1/checkout/${reference}/verify`, {
headers: {
'Accept': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.completed === true) {
clearInterval(interval);
if (data.success === true) {
// Redirect to success URL
window.location.href = data.redirect_url || redirectUrl;
} else {
// Show error and redirect
showError(data.message);
setTimeout(() => {
window.location.href = data.redirect_url || '/';
}, 3000);
}
} else if (attempts >= maxAttempts) {
clearInterval(interval);
showError('Payment verification timeout. Please check your email.');
}
})
.catch(error => {
console.error('Polling error:', error);
if (attempts >= maxAttempts) {
clearInterval(interval);
showError('Unable to verify payment status.');
}
});
}
// Start polling every 3 seconds
interval = setInterval(checkStatus, 3000);
checkStatus(); // Check immediately
}(Your configured webhook URL)RwandaPay sends real-time payment status updates to your configured webhook URL. Always verify the payment on your server-side using webhooks — do not rely solely on client-side redirects.
{
"event": "payment.successful",
"data": {
"reference": "ORDER-12345",
"amount": 5000,
"currency": "RWF",
"status": "successful",
"customer": {
"name": "John Doe",
"email": "john@example.com",
"phone": "0788123456"
},
"paid_at": "2024-12-15T14:30:00Z",
"transaction_id": "PAY-LIVE-XXXXXXXX-20241215143000",
"paypack_reference": "550e8400-e29b-41d4-a716-446655440000",
"mode": "live"
},
"timestamp": "2024-12-15T14:30:05Z"
}<?php
// Your webhook endpoint (e.g., https://yoursite.com/api/webhook)
header('Content-Type: application/json');
$payload = json_decode(file_get_contents('php://input'), true);
// Verify webhook signature (implement your signature verification)
if (!verifyWebhookSignature($payload)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
if ($payload['event'] === 'payment.successful') {
$data = $payload['data'];
// Update your database
$orderId = $data['reference'];
$amount = $data['amount'];
$transactionId = $data['transaction_id'];
// Mark order as paid
$updated = DB::table('orders')
->where('reference', $orderId)
->where('status', 'pending')
->update([
'status' => 'paid',
'transaction_id' => $transactionId,
'paid_at' => now(),
'updated_at' => now()
]);
if ($updated) {
// Send confirmation email to customer
// Update inventory
// Trigger any business logic
Log::info("Payment confirmed for order: {$orderId}");
}
}
// Always return 200 OK to acknowledge receipt
http_response_code(200);
echo json_encode(['status' => 'ok']);{
"amount": 5000,
"tx_ref": "ORDER-12345",
"currency": "RWF",
"customer": {
"name": "John Doe",
"email": "john@example.com",
"phone": "0788123456"
},
"redirect_url": "https://yoursite.com/payment-callback",
"webhook_url": "https://yoursite.com/api/webhook",
"description": "Payment for Order #12345",
"meta": {
"cart_id": "cart_123",
"user_id": 456
}
}| HTTP Code | Meaning | When it happens |
|---|---|---|
| 200 OK | Success | Request processed successfully |
| 201 Created | Created | Checkout session created |
| 401 Unauthorized | Auth Failed | Invalid or missing API keys |
| 409 Conflict | Duplicate | tx_ref already exists |
| 422 Unprocessable | Validation Error | Missing or invalid required fields |
| 500 Server Error | Internal Error | Contact support if persists |
Accelerate your integration with our official SDKs:
PHP SDK
composer require rwandapay/php-sdkJavaScript SDK
npm install rwandapay-jsPython SDK
pip install rwandapayJava SDK
Maven: com.rwandapay| Error Code | HTTP Status | Description | Resolution |
|---|---|---|---|
| AUTH_FAILED | 401 | Invalid API credentials | Verify your API keys |
| DUPLICATE_REFERENCE | 409 | tx_ref already used | Use a unique reference |
| INITIALIZATION_FAILED | 500 | Could not create session | Contact support |
| SESSION_NOT_FOUND | 404 | Checkout session expired | Create new session |
| INSUFFICIENT_BALANCE | 422 | Customer has insufficient funds | Ask customer to retry |
| Rate Limit | Burst Limit | Reset Time |
|---|---|---|
| 500 requests/minute | 1000 requests/minute | Rolling window |
| 10,000 requests/day | - | Resets at 00:00 UTC |
When rate limit is exceeded, the API returns 429 Too Many Requests. Implement exponential backoff in your integration.
Our developer support team is here to help you integrate.