Copied to clipboard!

RwandaPay API Reference

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.

Secure & Reliable

API key authentication, HMAC webhooks, idempotency, rate limiting

RESTful JSON API

Consistent JSON responses, standard HTTP methods, clear error codes

Webhook + Polling

Real-time webhooks with polling fallback for maximum reliability

Base URL: https://pay.rwandapay.rw/api/v1

Environment: Production (Live) ยท Test mode available with pk_test_* keys

Authentication

๐Ÿ” API Key Format: All API requests require X-Public-Key and X-Secret-Key headers.

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)

โš ๏ธ Important: Secret keys are only shown once when created. Store them securely and never commit them to version control.

Idempotency

What is Idempotency? Idempotency ensures that making the same request multiple times produces the same result as making it once. This is critical for financial operations where duplicate transactions could cause serious problems.

How It Works

1 Generate a unique Idempotency-Key for each financial operation
2 Include the key in the Idempotency-Key request header
3 RwandaPay stores the key with the first request's result
4 If you retry with the same key:
  • Same body โ†’ Returns the original result (no duplicate)
  • Different body โ†’ Returns 409 Conflict

Generating Idempotency Keys

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();
POST /v1/checkout/initialize New

Initialize Checkout

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

๐Ÿ’ก Note: This endpoint requires API authentication. The checkout page itself is public.

Request Parameters

ParameterTypeRequiredDescription
amountdecimal*Amount in RWF (min: 100, max: 1,000,000)
currencystringโ—‹Currency code (default: RWF)
tx_refstring*Your unique transaction reference
customer.namestring*Customer's full name
customer.phonestring*Customer's phone (format: 07XXXXXXXX)
customer.emailemail*Customer's email
redirect_urlurlโ—‹URL after payment completion
webhook_urlurlโ—‹Webhook URL for notifications
descriptionstringโ—‹Payment description
metaobjectโ—‹Custom metadata
Request Example
{
  "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"
  }
}
201 Created Response
{
  "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"
  }
}
POST /v1/checkout/{session_id}/process New

Process Payment

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

โš ๏ธ Important: This endpoint does NOT require API authentication. It uses the session's merchant_id for all operations.

Request Parameters

ParameterTypeRequiredDescription
phonestring*Customer phone (10 digits, e.g., 0788123456)
networkstring*Network: "MTN" or "Airtel"
customer_namestring*Customer's full name
emailemailโ—‹Customer's email
Request Example
{
  "phone": "0788123456",
  "network": "MTN",
  "customer_name": "Jane Smith",
  "email": "jane@example.com"
}
Success Response (Live Mode)
{
  "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"
  }
}
Success Response (Test Mode)
{
  "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"
  }
}
GET /v1/checkout/{reference}/verify

Verify Payment

Verifies the status of a payment. Used for polling from the checkout page.

Auth: โš ๏ธ No API Keys Required

Rate Limit: 30 requests/minute

๐Ÿ“Œ Note: This endpoint reads from the local database only. It is safe for frequent polling.

Response (Pending)
{
  "status": "pending",
  "completed": false,
  "success": false,
  "message": "Waiting for payment confirmation. Please check your phone.",
  "mode": "live",
  "reference": "d234d032-7386-4431-9bd8-90c53eab7e91"
}
Response (Successful)
{
  "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"
}
Response (Failed)
{
  "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"
}
POST /v1/collections New

Create Collection

Creates a payment request (collection) that generates a payment link for the customer.

Required Scope: collections:create

Idempotency: Required

Auth: API Keys Required

๐Ÿ’ก Best Practice: Always include a webhook_url to receive real-time payment confirmations.

Request Example
{
  "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"
  }
}
201 Created Response
{
  "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"
    }
  }
}
GET /v1/collections/{reference}

Get Collection

Retrieves the details of a collection request.

Required Scope: collections:read

Response
{
  "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"
  }
}
GET /v1/collections/{reference}/status New

Check Collection Status

Gets the real-time payment status of a collection.

Required Scope: collections:read

๐Ÿ“Œ Note: This endpoint reads from the local database only. Safe for frequent polling.

Response (Pending)
{
  "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"
  }
}
Response (Successful)
{
  "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"
  }
}
POST /v1/disbursements New

Create Disbursement (Withdrawal)

Initiates an automated withdrawal to a mobile money account.

Required Scope: disbursements:create

Idempotency: Required

โš ๏ธ Security: Disbursements use atomic fund reservation. Concurrent withdrawals cannot double-spend.

Request Example
{
  "amount": 50000,
  "currency": "RWF",
  "destination": {
    "phone": "0788123456",
    "network": "MTN"
  },
  "reference": "WD-20260810-0001",
  "description": "Salary payout for August"
}
201 Created Response
{
  "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"
  }
}

๐Ÿ“Œ Note: Initial response shows status: "authorized". Poll GET /v1/disbursements/{reference} for final status.

GET /v1/disbursements/{reference}

Get Disbursement

Retrieves the details and current status of a disbursement.

Required Scope: disbursements:read

Response
{
  "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 Overview

๐Ÿ”— Webhook URL: https://pay.rwandapay.rw/webhooks/paypack

๐Ÿ“Œ Note: Webhooks are sent as POST requests with JSON payload.

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:

1

Payment Initiated

Customer initiates payment from hosted page.

2

Webhook Sent (Primary)

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" }
3

Polling (Fallback)

Hosted page polls /api/v1/checkout/{reference}/verify every 3 seconds.

  • 30 requests/minute rate limit
  • Returns cached status from database
  • Stops polling when payment is completed
4

Payment Confirmed

Customer redirected to success/failure page.

โš ๏ธ Important: Always return a 2xx status code. If you return a non-2xx response, RwandaPay will retry the webhook up to 3 times.

Webhook Setup Guide

1. Create a Webhook Endpoint

PHP Webhook Receiver
<?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.
}
?>

2. Node.js Webhook Receiver

Node.js / Express
// 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');
});

Webhook Event Reference

EventDescriptionWhen Sent
transaction:processedPayment processed (successful or failed)Immediately after provider response
payment.successfulPayment completed successfullyWhen payment is confirmed
payment.failedPayment failedWhen payment is rejected

Webhook Payload Structure

Full Webhook Payload Example
{
  "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"
}

Webhook Security

โš ๏ธ Always verify signatures: Webhooks are signed with HMAC-SHA256. Never trust unverified webhooks.

Signature Verification

Every webhook includes a X-Webhook-Signature header. Verify it using your webhook secret:

Signature Verification (PHP)
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...

Best Practices

โœ… DO:

  • Always verify the signature before processing
  • Use a secure secret (minimum 32 characters)
  • Return 200 OK quickly, process asynchronously
  • Log all webhook attempts for debugging
  • Use HTTPS for your webhook endpoint
  • Use event_id to deduplicate webhooks

โŒ DON'T:

  • Process webhooks without verification
  • Share your webhook secret with anyone
  • Return non-2xx responses (triggers retries)
  • Do heavy processing synchronously
  • Trust webhooks as the only payment confirmation

Idempotent Webhook Processing

Webhooks may be sent more than once. Use the event_id to deduplicate:

// Store processed event_ids in your database $eventId = $payload['event_id']; if (eventAlreadyProcessed($eventId)) { return; // Already processed, skip } markEventAsProcessed($eventId); // Process the webhook...

Error Codes

HTTP CodeError CodeDescriptionRetry?
400BAD_REQUESTMalformed request syntaxโŒ No
401AUTH_FAILEDInvalid API credentialsโŒ No
403MERCHANT_INACTIVEMerchant account is not activeโŒ No
409DUPLICATE_REFERENCETransaction reference already existsโŒ No
409IDEMPOTENCY_KEY_CONFLICTSame key with different parametersโŒ No
422VALIDATION_ERRORInvalid request parametersโœ… Yes (fix params)
429RATE_LIMIT_EXCEEDEDToo many requestsโœ… Yes (after delay)
500INTERNAL_SERVER_ERRORServer-side errorโœ… Yes (with idempotency)
Error Response Format
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The amount field is required.",
    "details": {
      "amount": ["The amount field is required."]
    }
  }
}

Rate Limits

Operation TypeRate LimitTime Window
Read operations100 requests1 minute
Write operations50 requests1 minute
Financial operations20 requests1 minute
Checkout polling30 requests1 minute

Rate Limit Headers: All responses include rate limit information.

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1628496000

Security

๐Ÿ” API Key Security

Secret keys are hashed with password_hash(). Raw secrets are never stored and displayed only once at creation.

๐Ÿ” Environment Isolation

Test keys (pk_test_*) cannot perform live operations. Live keys cannot be used in test mode.

๐Ÿ›ก๏ธ Atomic Fund Reservation

Withdrawals use SELECT ... FOR UPDATE row-level locking. Concurrent withdrawals cannot double-spend.

๐Ÿ“ Immutable Audit Trail

Financial records are append-only. Corrections create new transactions, never modify history.

Need Help?

Our developer support team is here to help you integrate.