Version 1.0 ยท Last updated September 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} |
Identifies your merchant account. |
| X-Secret-Key | sk_{environment}_{random} |
Authenticates the request (never expose client-side). |
curl -X POST https://api.rwandapay.rw/api/v1/checkout/initialize \
-H "X-Public-Key: pk_live_01ccdc1..." \
-H "X-Secret-Key: sk_live_..." \
-H "Idempotency-Key: 9dcca83f-b379-4320-b..." \
-H "Content-Type: application/json" \
-d '{
"amount": 15000,
"currency": "RWF",
"tx_ref": "CHK-20260928-001",
"customer": {
"name": "Jane Smith",
"phone": "0788123456",
"email": "jane@example.com"
}
}'Idempotency-Key for each financial operation.
Idempotency-Key request header.
409 Conflict.JavaScript / 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
Auth Required
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 (alphanumeric, dashes, underscores only) |
| customer.name | string | * | Customer's full name |
| customer.phone | string | * | Customer's phone โ 10 digits, starts with 0 |
| customer.email | * | Customer's email | |
| redirect_url | url | โ | URL the customer is redirected to after payment |
| webhook_url | url | โ | Webhook URL for real-time notifications |
| description | string | โ | Payment description |
| meta | object | โ | Custom metadata returned with the transaction |
{
"amount": 15000,
"currency": "RWF",
"tx_ref": "CHK-20260928-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-20260928-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-09-28T12:30:00.000000Z"
}
}/v1/checkout/session/{session_id}
Auth Required
Retrieves the details of a previously created checkout session.
Required Scope: checkout:read
{
"success": true,
"data": {
"session_id": "CHK-ABCDEFGHIJKLMNOP",
"reference": "CHK-20260928-001",
"status": "pending",
"amount": 15000,
"currency": "RWF",
"mode": "live",
"payment_url": "https://pay.rwandapay.rw/checkout/CHK-ABCDEFGHIJKLMNOP",
"customer": {
"name": "Jane Smith",
"phone": "0788123456",
"email": "jane@example.com"
},
"expires_at": "2026-09-28T12:30:00.000000Z",
"created_at": "2026-09-28T12:00:00.000000Z"
}
}/v1/checkout/{session_id}/process
Public
Processes a payment from the hosted checkout page. 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 starting with 0 |
| network | string | * | 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-20260928-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-20260928120403",
"merchant_reference": "CHK-20260928-001",
"mode": "test",
"redirect_url": "https://pay.rwandapay.rw/checkout/success/PAY-TEST-XXXXXXXXXXXX-20260928120403",
"amount": 15000,
"currency": "RWF"
}
}/v1/checkout/{reference}/verify
Public
Verifies the status of a payment. Used for polling from the checkout page.
Auth: โ ๏ธ No API Keys Required
Rate Limit: 30 requests/minute per IP
{
"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-20260928120403",
"amount": 15000,
"mode": "live",
"reference": "PAY-LIVE-XXXXXXXXXXXX-20260928120403"
}{
"status": "failed",
"completed": true,
"success": false,
"message": "Payment failed. Please check if you have enough funds on your mobile money and try again.",
"redirect_url": "https://pay.rwandapay.rw/checkout/failed/PAY-LIVE-XXXXXXXXXXXX-20260928120403",
"reference": "PAY-LIVE-XXXXXXXXXXXX-20260928120403"
}/v1/collections
Auth Required
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-20260928-001",
"customer": {
"name": "John Doe",
"phone": "0788123456",
"email": "john@example.com"
},
"description": "Payment for invoice #INV-20260928-001",
"redirect_url": "https://your-merchant-site.com/payment-success",
"webhook_url": "https://your-merchant-site.com/webhook",
"expires_at": "2026-10-08T00:00:00Z",
"metadata": {
"order_id": "ORD-12345",
"product": "Premium Subscription"
}
}{
"success": true,
"message": "Collection created successfully",
"data": {
"reference": "COL-XXXXXXXXXX-20260928",
"status": "pending",
"amount_type": "fixed",
"amount": 5000,
"fee": 175,
"net_amount": 4825,
"currency": "RWF",
"payment_url": "https://pay.rwandapay.rw/pay/COL-XXXXXXXXXX-20260928",
"customer": {
"name": "John Doe",
"phone": "0788123456",
"email": "john@example.com"
},
"description": "Payment for invoice #INV-20260928-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-20260928&status=successful",
"failed_redirect_url": "https://your-merchant-site.com/payment-success?reference=COL-XXXXXXXXXX-20260928&status=failed",
"expires_at": "2026-10-08T00:00:00Z",
"created_at": "2026-09-28T12:00:00Z",
"metadata": {
"order_id": "ORD-12345",
"product": "Premium Subscription"
}
}
}/v1/collections
Auth Required
Retrieves a paginated list of collections for your merchant account.
Required Scope: collections:read
| Parameter | Type | Description |
|---|---|---|
| status | string | Filter by status (pending, completed, failed, cancelled) |
| amount_type | string | Filter by type (fixed, custom) |
| from_date | date | Filter by created_at >= this date |
| to_date | date | Filter by created_at <= this date |
| search | string | Search by reference, customer name, phone, or email |
| per_page | integer | Results per page (default: 20) |
{
"success": true,
"data": [
{
"reference": "COL-XXXXXXXXXX-20260928",
"status": "completed",
"amount": 5000,
"fee": 175,
"net_amount": 4825,
"currency": "RWF",
"payment_url": "https://pay.rwandapay.rw/pay/COL-XXXXXXXXXX-20260928",
"created_at": "2026-09-28T12:00:00Z",
"paid_at": "2026-09-28T12:05:00Z"
}
],
"meta": {
"current_page": 1,
"last_page": 1,
"per_page": 20,
"total": 1
}
}/v1/collections/{reference}
Auth Required
Retrieves the details of a collection request.
Required Scope: collections:read
{
"success": true,
"data": {
"reference": "COL-XXXXXXXXXX-20260928",
"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-20260928-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-20260928",
"paypack_status": "successful",
"paypack_reference": "550e8400-e29b-41d4-a716-446655440000",
"paid_at": "2026-09-28T12:05:00Z",
"created_at": "2026-09-28T12:00:00Z"
}
}/v1/collections/{reference}/status
Auth Required
Gets the real-time payment status of a collection.
Required Scope: collections:read
{
"success": true,
"data": {
"reference": "COL-XXXXXXXXXX-20260928",
"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-20260928",
"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",
"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-20260928&status=successful",
"redirect_to": "https://your-merchant-site.com/payment-success?reference=COL-XXXXXXXXXX-20260928&status=successful"
}
}{
"success": true,
"data": {
"reference": "COL-XXXXXXXXXX-20260928",
"collection_status": "failed",
"is_paid": false,
"is_processing": false,
"is_pending": false,
"is_expired": false,
"is_failed": true,
"amount": 5000,
"amount_paid": 0,
"currency": "RWF",
"payment_method": "MTN",
"paypack_status": "failed",
"paypack_reference": "550e8400-e29b-41d4-a716-446655440000",
"processing_status": "processed",
"failed_redirect_url": "https://your-merchant-site.com/payment-success?reference=COL-XXXXXXXXXX-20260928&status=failed",
"redirect_to": "https://your-merchant-site.com/payment-success?reference=COL-XXXXXXXXXX-20260928&status=failed"
}
}/v1/collections/{reference}
Auth Required
Cancels a pending collection. Only works if the collection hasn't been paid.
Required Scope: collections:write
{
"success": true,
"message": "Collection cancelled successfully"
}/v1/balance
Auth Required
New
Returns your current merchant balance breakdown: available, ledger, reserved, pending, and totals.
Required Scope: balance:read
{
"success": true,
"data": {
"balance": 81447.50,
"balance_formatted": "81,447.50",
"ledger_balance": 81447.50,
"reserved_balance": 10400.00,
"pending_balance": 0,
"held_balance": 5100.00,
"total_collected": 119300.00,
"total_withdrawn": 30000.00,
"total_refunded": 0.00,
"total_fees": 1483.50,
"currency": "RWF",
"pending_withdrawals": 1,
"last_reconciled_at": "2026-09-28T15:04:58.000000Z"
}
}/v1/balance/transactions
Auth Required
Returns a paginated list of ledger entries affecting your balance.
Required Scope: balance:read
{
"success": true,
"data": [
{
"id": 42,
"type": "credit",
"amount": 96.50,
"balance_after": 81447.50,
"description": "Payment from Customer",
"reference": "PAY-LIVE-XXXXXXXXXXXX",
"created_at": "2026-09-28T12:05:00Z"
}
],
"meta": {
"current_page": 1,
"last_page": 1,
"per_page": 25,
"total": 1
}
}/v1/payments/collect
Auth Required
New
Initiates a direct mobile money collection without the hosted checkout flow.
Required Scope: payments:create
Idempotency: Required
{
"amount": 100,
"currency": "RWF",
"phone": "0788123456",
"network": "MTN",
"reference": "PAY-20260928-001",
"customer": {
"name": "Jane Smith",
"email": "jane@example.com"
}
}{
"success": true,
"message": "Collection initiated. Please check the customer's phone.",
"data": {
"reference": "PAY-20260928-001",
"paypack_reference": "550e8400-e29b-41d4-a716-446655440000",
"status": "pending",
"amount": 100,
"currency": "RWF"
}
}/v1/payments/{reference}
Auth Required
Returns the current status of a direct payment.
{
"success": true,
"data": {
"reference": "PAY-20260928-001",
"status": "successful",
"amount": 100,
"fee": 3.50,
"net_amount": 96.50,
"currency": "RWF",
"paypack_reference": "550e8400-e29b-41d4-a716-446655440000",
"completed_at": "2026-09-28T12:05:00Z"
}
}/v1/payments/{reference}/refund
Auth Required
Issues a full or partial refund for a successful payment.
Required Scope: payments:refund
Idempotency: Required
{
"amount": 100,
"reason": "Customer requested refund"
}{
"success": true,
"message": "Refund initiated successfully",
"data": {
"reference": "REF-20260928-001",
"payment_reference": "PAY-20260928-001",
"amount": 100,
"status": "pending"
}
}/v1/transactions
Auth Required
Returns a paginated list of all transactions for your merchant account.
Required Scope: transactions:read
{
"success": true,
"data": [
{
"reference": "PAY-LIVE-XXXXXXXXXXXX",
"type": "payment",
"amount": 100,
"fee": 3.50,
"net_amount": 96.50,
"currency": "RWF",
"status": "successful",
"customer_name": "Jane Smith",
"customer_phone": "0788123456",
"payment_method": "mtn_momo",
"created_at": "2026-09-28T12:00:00Z"
}
],
"meta": { "current_page": 1, "last_page": 1, "per_page": 25, "total": 1 }
}/v1/transactions/{reference}
Auth Required
Retrieves full details of a single transaction.
{
"success": true,
"data": {
"reference": "PAY-LIVE-XXXXXXXXXXXX",
"type": "payment",
"source": "api_payment",
"amount": 100,
"fee": 3.50,
"net_amount": 96.50,
"currency": "RWF",
"status": "successful",
"payment_method": "mtn_momo",
"customer": {
"name": "Jane Smith",
"phone": "0788123456",
"email": "jane@example.com"
},
"balance_before": 81351.00,
"balance_after": 81447.50,
"completed_at": "2026-09-28T12:05:00Z",
"created_at": "2026-09-28T12:00:00Z"
}
}/v1/disbursements
Auth Required
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-20260928-0001",
"description": "Salary payout for September"
}{
"success": true,
"data": {
"reference": "WD-20260928-0001",
"status": "authorized",
"amount": 50000,
"fee": 2000,
"net_amount": 48000,
"currency": "RWF",
"destination": {
"phone": "0788123456",
"network": "MTN"
},
"created_at": "2026-09-28T12:00:00Z"
}
}/v1/disbursements
Auth Required
Returns a paginated list of disbursements.
{
"success": true,
"data": [
{
"reference": "WD-20260928-0001",
"status": "successful",
"amount": 50000,
"fee": 2000,
"net_amount": 48000,
"currency": "RWF",
"created_at": "2026-09-28T12:00:00Z"
}
],
"meta": { "current_page": 1, "last_page": 1, "per_page": 25, "total": 1 }
}/v1/disbursements/{reference}
Auth Required
Retrieves the details and current status of a disbursement.
Required Scope: disbursements:read
{
"success": true,
"data": {
"reference": "WD-20260928-0001",
"status": "successful",
"amount": 50000,
"fee": 2000,
"net_amount": 48000,
"currency": "RWF",
"destination": {
"phone": "0788123456",
"network": "MTN"
},
"description": "Salary payout for September",
"paypack_reference": "550e8400-e29b-41d4-a716-446655440001",
"processing_time": "2026-09-28T12:05:00Z",
"created_at": "2026-09-28T12:00:00Z"
}
}/v1/disbursements/{reference}/status
Auth Required
Lightweight status check for a disbursement โ safe for frequent polling.
{
"success": true,
"data": {
"reference": "WD-20260928-0001",
"status": "successful",
"is_completed": true,
"is_pending": false,
"is_failed": false,
"amount": 50000,
"currency": "RWF"
}
}RwandaPay uses both webhooks and polling for maximum reliability. The dual-path pipeline means a payment is confirmed even if one path fails:
Customer initiates payment from the hosted page.
MTN sends a webhook to api.rwandapay.rw/api/webhooks/mtn with the payment status.
{
"financialTransactionId": "30861309738",
"externalId": "PAY-LIVE-XXXXXXXXXXXX-20260928120403",
"amount": "100",
"currency": "RWF",
"payer": { "partyIdType": "MSISDN", "partyId": "250788123456" },
"status": "SUCCESSFUL"
}
The hosted page polls /api/v1/checkout/{reference}/verify every 3 seconds.
RwandaPay forwards the normalized event to your webhook_url with an HMAC signature.
<?php
// webhook.php โ Your webhook receiver endpoint
function verifySignature(string $rawBody, string $signature, string $secret): bool {
$expected = base64_encode(hash_hmac('sha256', $rawBody, $secret, true));
return hash_equals($expected, trim($signature));
}
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$secret = 'whsec_your_webhook_secret';
if (!verifySignature($rawBody, $signature, $secret)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
$payload = json_decode($rawBody, true);
$event = $payload['event_kind'] ?? null;
$status = $payload['status'] ?? null;
$reference = $payload['paypack_reference'] ?? null;
if ($event === 'transaction:processed') {
if ($status === 'successful') {
handleSuccessfulPayment($reference, $payload);
} elseif ($status === 'failed') {
handleFailedPayment($reference, $payload);
}
}
http_response_code(200);
echo json_encode(['status' => 'success']);
function handleSuccessfulPayment(string $reference, array $payload): void {
error_log("Payment successful: $reference");
}
function handleFailedPayment(string $reference, array $payload): void {
error_log("Payment failed: $reference");
}
?>// webhook.js
const crypto = require('crypto');
const express = require('express');
const app = express();
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');
const a = Buffer.from(expected);
const b = Buffer.from(signature || '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post('/webhook', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const secret = 'whsec_your_webhook_secret';
if (!verifySignature(req.rawBody, signature, secret)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const { event_kind, status, paypack_reference } = req.body;
console.log(`Webhook: ${event_kind} โ ${status}`);
if (event_kind === 'transaction:processed') {
status === 'successful'
? handleSuccess(paypack_reference, req.body)
: handleFailure(paypack_reference, req.body);
}
res.json({ status: 'success' });
});
function handleSuccess(ref, body) { console.log(`โ
${ref} โ ${body.amount} RWF`); }
function handleFailure(ref, body) { console.log(`โ ${ref}`); }
app.listen(3000, () => console.log('Webhook server running on :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 |
| collection.created | Collection was created via API | Immediately after POST /v1/collections |
{
"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-09-28T21:53:43.150125Z"
}function verifyWebhookSignature(string $rawBody, string $signature, string $secret): bool {
$expected = base64_encode(hash_hmac('sha256', $rawBody, $secret, true));
return hash_equals($expected, trim($signature));
}
$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:
event_id to deduplicate.โ 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 |
| 404 | NOT_FOUND | Resource does not exist | โ 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.