V-Pay API Documentation
Welcome to the V-Pay API. This documentation covers everything you need to integrate payments into your application — from authentication to webhooks.
The V-Pay API is a RESTful HTTP API. All requests are made over HTTPS and all responses are returned as JSON. We support MTN Mobile Money, Airtel Money, and Bank Transfer — the payment channels Rwandans use every day.
https://vonsung.rw/api/
— All API endpoints are relative to this base URL.
What you can build
Quick Start
Get from zero to your first payment in four steps. This guide uses cURL but the same steps apply to any HTTP client.
sandbox_key for testing, or live_key for production./auth/ by passing apiUser and apiKey in your request header./pay/ with the customer's phone number, amount, external reference and other required parameters in the request body. You should use the access token generated in the previous step to authorize the request./transaction/status/ or — better — set up a webhook to receive real-time payment confirmation.Authentication
# Generate access token curl --location --request POST 'https://vonsung.rw/api/auth/' \ --header 'apiUser': 'your_api_user' \ --header 'apiKey': 'your_api_key' \ --data ''
import requests url = "https://vonsung.rw/api/auth/" payload = "" headers = { 'apiUser': 'your_api_user', 'apiKey': 'your_api_key' } response = requests.request("POST", url, headers=headers, data=payload)
var myHeaders = new Headers(); myHeaders.append("apiUser", "your_api_user"); myHeaders.append("apiKey", "your_api_key"); var raw = ""; var requestOptions = { method: 'POST', headers: myHeaders, body: raw, redirect: 'follow' }; fetch("https://vonsung.rw/api/auth/", requestOptions) .then(response => response.text()) .then(result => console.log(result)) .catch(error => console.log('error', error));
$curl = curl_init(); curl_setopt_array($curl, array( CURLOPT_URL => 'http://127.0.0.1:8000/api/auth/', CURLOPT_RETURNTRANSFER => true, CURLOPT_ENCODING => '', CURLOPT_MAXREDIRS => 10, CURLOPT_TIMEOUT => 0, CURLOPT_FOLLOWLOCATION => true, CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, CURLOPT_CUSTOMREQUEST => 'POST', CURLOPT_HTTPHEADER => array( 'apiUser': 'your_api_user', 'apiKey': 'your_api_key' ), )); $response = curl_exec($curl); curl_close($curl); echo $response;
Response
{
"access":"eyJhbGciOiJIUzI1NiIsInR5cxxxxxxxxxxxxx",
"refresh":"eyJhbGciOiJIUzI1NiIsInR5cxxxxxxxxxxxxx",
"mode":"live",
"expires_in":"300.0 seconds"
}
The V-Pay API uses bearer token authorization. Use the access token generated in the authentication response to authorize V-Pay API calls.
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cxxxxxxxxxxxxx
vpay_sandbox_ and live user with vpay_live_.Key types
| Key | Prefix | Description |
|---|---|---|
| Sandbox Key | sk_sandbox_ | For testing. Payments are simulated — no real money moves. |
| Live Key | sk_live_ | For production. Real payments are processed. |
Environments
V-Pay provides two isolated environments. Use sandbox for all development and testing — it mirrors production exactly without moving real funds.
| Environment | Base URL | Notes |
|---|---|---|
| Sandbox | https://vonsung.rw/api/Use sandbox apiUser and apiKey | Simulated payments, full webhook support, test phone numbers available |
| Live | https://vonsung.rw/api/Use live apiUser and apiKey | Real transactions processed against Mobile Money & Bank Cards |
Sandbox test numbers
| Phone | Network | Simulated result |
|---|---|---|
| 0780000000 | MTN MoMo | Always succeeds |
| 0790000000 | MTN MoMo | Always succeeds |
Initiate Payment
Send a payment prompt to a customer's mobile money account. The customer receives a USSD push notification and confirms on their phone.
Request parameters
| Parameter | Type | Description |
|---|---|---|
| amount required | integer | Payment amount in RWF. Minimum: 100. |
| currency required | string | Must be RWF for MTN Mobile Money. If not, it will be converted using our exchange rates. |
| method required | string | Payment method: mtnrw, card, or all. |
| phone required | string | Customer's phone number. Format: 07XXXXXXXX. |
| reference required | string | Your internal order or reference ID. Must be unique per request. |
| description optional | string | A human-readable description shown to the payer. Max 80 chars. Avoid special characters. |
| send_receipt optional | boolean | Set to true by default - specifies whether to send a receipt to the customer. |
| callback_url optional | string | URL to receive the webhook payload when the payment status changes. |
| metadata optional | object | Key-value pairs attached to this transaction. Returned in webhook payloads. |
# Pay API request curl --location 'https://vonsung.rw/api/pay/' \ --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxxxx' \ --header 'Content-Type: application/json' \ --header 'apiUser: your_api_user' \ --header 'apiKey: your_api_key' \ --data-raw '{ "amount": amount, "currency": currency, "payer_name": "Payer full name", "payer_email": "payer@example.com", "payer_phone": "0790000000", "external_id": "TXNID_5473xxxxxx", "payee_message": "Short message", "send_receipt": true, "payment_method": "card" }'
import requests import json url = "https://vonsung.rw/api/pay/" payload = json.dumps({ "amount": amount, "currency": currency, "payer_name": "Payer full name", "payer_email": "payer@example.com", "payer_phone": "0790000000", "external_id": "TXNID_5473xxxxxx", "payee_message": "Short message", "send_receipt": True, "payment_method": "card" }) headers = { 'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxxxx', 'Content-Type': 'application/json', 'apiUser': 'your_api_user', 'apiKey': 'your_api_key' } response = requests.request("POST", url, headers=headers, data=payload) print(response.text)
var settings = { "url": "https://vonsung.rw/api/pay/", "method": "POST", "timeout": 0, "headers": { "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxxxx", "Content-Type": "application/json", "apiUser": "your_api_user", "apiKey": "your_api_key" }, "data": JSON.stringify({ "amount": amount, "currency": "currency", "payer_name": "Payer full name", "payer_email": "payer@example.com", "payer_phone": "0790000000", "external_id": "TXNID_5473xxxxxx", "payee_message": "Short message", "send_receipt": true, "payment_method": "card" }) }; $.ajax(settings).done(function (response) { console.log(response); });
$curl = curl_init(); curl_setopt_array($curl, array( CURLOPT_URL => 'https://vonsung.rw/api/pay/', CURLOPT_RETURNTRANSFER => true, CURLOPT_ENCODING => '', CURLOPT_MAXREDIRS => 10, CURLOPT_TIMEOUT => 0, CURLOPT_FOLLOWLOCATION => true, CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, CURLOPT_CUSTOMREQUEST => 'POST', CURLOPT_POSTFIELDS => '{ "amount": amount, "currency": currency, "payer_name": "Payer full name", "payer_email": "payer@example.com", "payer_phone": "0790000000", "external_id": "TXNID_5473xxxxxx", "payee_message": "Short message", "send_receipt": true, "payment_method": "card" }', CURLOPT_HTTPHEADER => array( 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxxxx', 'Content-Type: application/json', 'apiUser: your_api_user', 'apiKey: your_api_key' ) )); $response = curl_exec($curl); curl_close($curl); echo $response;
:) and other special characters in the description field when using MTN MoMo. These can cause silent payment failures at the network level.Response fields
| Field | Type | Description |
|---|---|---|
| status | string | pending, successful, or failed - always pending on initiation. |
| transaction_id | string | V-Pay's unique transaction identifier. Use this to poll status. |
| message | string | Human-readable status message. |
| payment_url | string | URL to the payment page (if applicable). |
| merchant | string | Merchant name. |
| created_at | string | ISO 8601 timestamp of transaction creation. |
Standard Response
{
"status":"PENNDING",
"message":"Payment link created. Redirect the customer to payment_url.",
"transaction_id":"VP86583387",
"merchant":"VONSUNG",
"payment_url":"https://vonsung.rw/api/checkout/3f9aab1b-39e6-4188-a239-81a4xxxxxxx"
}
Self-hosted MoMo payment Response
{
"status_code":200,
"status":"SUCCESSFUL",
"message":"Transaction completed successfully.",
"amount":"240.00",
"currency":"RWF",
"transaction_date":"2026-05-30 08:03:32 PM",
"timezone":"Africa/Kigali",
"transaction_date_gmt":"2026-05-30 06:03:32 PM",
"time_taken":"16 sec",
"payer_name":"Celestin Niyomugabo",
"payer_phone":"250790000000",
"mode":"live",
"transaction_id":"VP86583387",
"merchant":"VONSUNG",
"external_id":"TXN_TDTAGD1HFJS"
}
Response codes
Check Payment Status
Retrieve the current status of a transaction by its ID. Poll this endpoint if you're not using webhooks.
Payment status values
| Status | Meaning |
|---|---|
| pending | Prompt sent, waiting for customer to confirm on their phone. |
| successful | Customer confirmed and funds have been received. |
| failed | Customer cancelled, timed out, or insufficient funds. |
| cancelled | Explicitly cancelled by merchant via the API. |
| refunded | Payment has been refunded to the customer. |
| disputed | Payment is under dispute. |
Payment Links
Create shareable payment pages programmatically. Useful for e-commerce, invoicing, or no-code payment collection via WhatsApp or email.
Request parameters
| Parameter | Type | Description |
|---|---|---|
| name required | string | Display name for the payment link page. |
| amount optional | integer | Fixed amount in RWF. If omitted, payer enters their own amount. |
| type required | string | one_off (single use) or multiple (reusable). |
| description optional | string | Shown on the payment page to the payer. |
| redirect_url optional | string | URL the payer is sent to after successful payment. |
{
"status": "success",
"link_id": "lnk_9f3a2b",
"slug": "monthly-fee-jun",
"url": "https://pay.vpay.rw/monthly-fee-jun",
"qr_code": "https://api.vpay.rw/v1/payment-links/lnk_9f3a2b/qr",
"amount": 10000,
"type": "multiple",
"active": true
}
Webhooks
Webhooks allow V-Pay to push payment events to your server in real time — eliminating the need to poll for status updates.
Register a webhook endpoint by passing a callback_url in your payment request, or configure a global endpoint from Dashboard → API Settings → Webhooks.
Event types
Verifying webhook signatures
Every webhook request includes a X-VPay-Signature header — an HMAC-SHA256 signature of the raw request body using your webhook secret.
import hmac, hashlib def verify_webhook(payload_body, signature, secret): expected = hmac.new( secret.encode(), payload_body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, signature) # In your Django view: sig = request.headers.get("X-VPay-Signature") if not verify_webhook(request.body, sig, WEBHOOK_SECRET): return HttpResponse(status=401)
const crypto = require("crypto"); function verifyWebhook(body, signature, secret) { const expected = crypto .createHmac("sha256", secret) .update(body) .digest("hex"); return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signature) ); } // In your Express handler: const sig = req.headers["x-vpay-signature"]; if (!verifyWebhook(req.rawBody, sig, process.env.WEBHOOK_SECRET)) { return res.status(401).send("Unauthorized"); }
SDKs & Libraries
Official client libraries to get up and running faster. Each SDK wraps the REST API with idiomatic methods and built-in error handling.
Error Codes
All errors return a JSON body with status, error_code, and message fields.
{
"status": "error",
"error_code": "INVALID_PHONE",
"message": "The phone number provided is not a valid MTN Rwanda number."
}
| Error Code | HTTP | Description |
|---|---|---|
| INVALID_KEY | 401 | API key is missing, expired, or does not match the environment. |
| INVALID_PHONE | 400 | Phone number format invalid or not on the specified network. |
| INVALID_AMOUNT | 400 | Amount is below the minimum (100 RWF) or non-integer. |
| DUPLICATE_REFERENCE | 409 | The reference ID has already been used in another transaction. |
| PAYMENT_TIMEOUT | 200 | Customer did not respond to the MoMo prompt within 3 minutes. |
| INSUFFICIENT_FUNDS | 200 | Customer's mobile wallet has insufficient balance. |
| NETWORK_ERROR | 502 | The mobile money network returned an error. Retry. |
| RATE_LIMITED | 429 | Too many requests. See rate limits. |
Rate Limits
V-Pay applies rate limits per API key to ensure stability for all users.
| Plan | Requests / minute | Requests / day |
|---|---|---|
| Starter | 20 req/min | 1,000 req/day |
| Growth | 120 req/min | 50,000 req/day |
| Enterprise | Custom | Custom |
Rate limit headers are returned on every response:
X-RateLimit-Limit: 120 X-RateLimit-Remaining: 118 X-RateLimit-Reset: 1716471600