V-Pay Docs
Home Docs Introduction

V-Pay API Documentation

Last updated May 2026  ·  API Version 1.0

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.

Base URL:   https://vonsung.rw/api/  — All API endpoints are relative to this base URL.

What you can build

Mobile Money Payments
Accept MTN MoMo Money directly in your app
No-Code Payment Links
Generate shareable payment pages via API
Real-time Webhooks
Get notified instantly on every payment event
Customer Management
Store and retrieve customer payment profiles

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.

1
Create a V-Pay account
Sign up to create your merchant account. Your sandbox environment is ready immediately — no approval needed.
2
Get your API keys
Go to Dashboard → API Settings. Copy your sandbox_key for testing, or live_key for production.
3
Create authentication token
Send a POST request to /auth/ by passing apiUser and apiKey in your request header.
4
Send a request to pay
Send a POST request to /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.
5
Listen for the result
Poll /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

JSON 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.

Header
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cxxxxxxxxxxxxx
Never expose your API key client-side. Always make API calls from your server. Sandbox user starts with vpay_sandbox_ and live user with vpay_live_.

Key types

KeyPrefixDescription
Sandbox Keysk_sandbox_For testing. Payments are simulated — no real money moves.
Live Keysk_live_For production. Real payments are processed.
Manage your API keys from Dashboard → Settings → Live API (or Sandobox). You can rotate or revoke keys at any time.

Environments

V-Pay provides two isolated environments. Use sandbox for all development and testing — it mirrors production exactly without moving real funds.

EnvironmentBase URLNotes
Sandboxhttps://vonsung.rw/api/
Use sandbox apiUser and apiKey
Simulated payments, full webhook support, test phone numbers available
Livehttps://vonsung.rw/api/
Use live apiUser and apiKey
Real transactions processed against Mobile Money & Bank Cards

Sandbox test numbers

PhoneNetworkSimulated result
0780000000MTN MoMoAlways succeeds
0790000000MTN MoMoAlways 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.

POST /pay/

Request parameters

ParameterTypeDescription
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;
payee_message tip: Avoid colons (:) and other special characters in the description field when using MTN MoMo. These can cause silent payment failures at the network level.

Response fields

FieldTypeDescription
statusstringpending, successful, or failed - always pending on initiation.
transaction_idstringV-Pay's unique transaction identifier. Use this to poll status.
messagestringHuman-readable status message.
payment_urlstringURL to the payment page (if applicable).
merchantstringMerchant name.
created_atstringISO 8601 timestamp of transaction creation.

Standard Response

JSON 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

JSON 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

200
Payment prompt sent successfully. Poll or wait for webhook.
400
Bad request — missing or invalid parameters.
401
Unauthorized — invalid or missing API key or authentication token is expired.
409
Conflict — reference already used for a different transaction.
500
Server error — retry with exponential backoff.

Check Payment Status

Retrieve the current status of a transaction by its ID. Poll this endpoint if you're not using webhooks.

GET /transaction/status/

Payment status values

StatusMeaning
pendingPrompt sent, waiting for customer to confirm on their phone.
successfulCustomer confirmed and funds have been received.
failedCustomer cancelled, timed out, or insufficient funds.
cancelledExplicitly cancelled by merchant via the API.
refundedPayment has been refunded to the customer.
disputedPayment is under dispute.
Polling recommendation: check every 5 seconds up to 3 minutes, then treat as failed if still pending. Better: use webhooks to avoid polling entirely.

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

payment.successful
Customer confirmed and payment was received. Funds are in your V-Pay balance.
payment.failed
Payment was rejected — timeout, cancellation, or insufficient funds.
payment.pending
Prompt sent, awaiting customer confirmation.
payment_link.paid
A payment was received via one of your payment links.
settlement.completed
Funds have been settled to your registered bank account.

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.

Don't see your language? The V-Pay API is fully REST-based — any HTTP client works. Request an SDK →

Error Codes

All errors return a JSON body with status, error_code, and message fields.

Error Response
{
  "status": "error",
  "error_code": "INVALID_PHONE",
  "message": "The phone number provided is not a valid MTN Rwanda number."
}
Error CodeHTTPDescription
INVALID_KEY401API key is missing, expired, or does not match the environment.
INVALID_PHONE400Phone number format invalid or not on the specified network.
INVALID_AMOUNT400Amount is below the minimum (100 RWF) or non-integer.
DUPLICATE_REFERENCE409The reference ID has already been used in another transaction.
PAYMENT_TIMEOUT200Customer did not respond to the MoMo prompt within 3 minutes.
INSUFFICIENT_FUNDS200Customer's mobile wallet has insufficient balance.
NETWORK_ERROR502The mobile money network returned an error. Retry.
RATE_LIMITED429Too many requests. See rate limits.

Rate Limits

V-Pay applies rate limits per API key to ensure stability for all users.

PlanRequests / minuteRequests / day
Starter20 req/min1,000 req/day
Growth120 req/min50,000 req/day
EnterpriseCustomCustom

Rate limit headers are returned on every response:

Response Headers
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 118
X-RateLimit-Reset: 1716471600

Changelog

NEW
v1.0.0 — May 2026
Initial stable API release. Payment initiation, status check, payment links, webhooks, customer management, and full sandbox support.
FIX
v0.9.2 — March 2026
Fixed MTN MoMo silent failure caused by colon characters in payee_message. Improved error response bodies for network-level rejections.
v0.9
v0.9.0 — January 2026
Beta release. Added Airtel Money support, webhook signature verification, and payment link QR code generation.