Pay a mobile wallet in Rwanda, Uganda, Zambia or Congo

Pick the right operator row, save a mobile wallet payee with a phone number in the accepted form, and send local currency.

Use this recipe to pick the right operator row, save a mobile wallet payee with a phone number in an accepted format, and send local currency to Rwanda, Uganda, Zambia or the Republic of the Congo.

Who it is for. Jean-Paul, a Toronto engineer using your app, sends money to his sister's mobile wallet in Kigali. Another user pays a supplier's wallet in Lusaka.

Status. Available on the operator rows MTN MoMo Rwanda, MTN MoMo Uganda, MTN MoMo Zambia and MTN MoMo Congo (Republic of the Congo). Other operators in these countries are not yet available.

Money example

  1. Jean-Paul sends 150.00 USD. Say the fee on your plan is 1.00 USD.
  2. Say the rate is 1,420.00 RWF per USD. The transfer response prices 213,000.00 RWF for his sister (local_amount).
  3. Zambia works the same way. At a rate of 26.50 ZMW per USD, the transfer response prices 100.00 USD at 2,650.00 ZMW.

Before you start

  • The sender has a name and a country (first_name, last_name and address.country on the customer). This route does not pay without both. You can also send them as additional_information.remitter_full_name and remitter_country.
  • The USD account is active and funded.

Find out what is required

The currency

GET /v1/transfer/supported-countries?iso_code=RW returns RWF. ZM returns ZMW, and UG returns UGX. For CG, the list has no currency (currency_code is null). Use XAF, which the operator row for CG carries.

The operator row

curl --request GET \
 --url 'https://api.gravv.xyz/v1/external-accounts/institutions?country_iso_code=RW&institution_type=mobile_money' \
 --header 'Api-Key: <API_KEY>'
const response = await fetch("https://api.gravv.xyz/v1/external-accounts/institutions?country_iso_code=RW&institution_type=mobile_money", {
method: "GET",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
},
});
console.log(response.status, await response.json());
import os

import requests

response = requests.get(
"https://api.gravv.xyz/v1/external-accounts/institutions?country_iso_code=RW&institution_type=mobile_money",
headers={
    "Api-Key": os.environ["GRAVV_API_KEY"],
},
)
print(response.status_code, response.json())
{
  "data": {
    "items": [
      {
        "account_number_type": "phone_number",
        "country_iso_code": "RW",
        "currency": "RWF",
        "id": "e3077661-0359-4740-b6bb-bad5f4e965fd",
        "max_amount": 0,
        "min_amount": 0,
        "name": "MTN MoMo Rwanda"
      },
      {
        "account_number_type": "phone_number",
        "country_iso_code": "RW",
        "currency": "RWF",
        "id": "f6a7b8c9-0d1e-4f25-8a7b-8c9d0e1f2a33",
        "max_amount": 10000,
        "min_amount": 1,
        "name": "MTN Mobile Money"
      }
    ],
    "items_per_page": 10,
    "page": 1,
    "total_items": 2,
    "total_pages": 1
  },
  "error": null
}

Pick the row whose name is MTN MoMo followed by the country:

Country Operator row institution_id
Rwanda MTN MoMo Rwanda e3077661-0359-4740-b6bb-bad5f4e965fd
Zambia MTN MoMo Zambia 52a0b270-79a1-44ba-a21f-dfa4fac926a0
Uganda MTN MoMo Uganda 939461ba-a4f7-40e3-a3be-9e6cf076b0f3
Republic of the Congo MTN MoMo Congo 181402b4-4709-4d4e-b077-810927e99c9f

Older rows with other names can also appear for Rwanda and Zambia. This route does not pay payees saved on those rows. Ids can differ between sandbox and live, so confirm them from the list.

The phone number

Gravv ignores spaces, + and a leading 00.

Country Dial code Digits after it Accepted examples
Rwanda 250 9 +250788123456, 0788123456, 788123456
Zambia 260 9 +260961234567, 0961234567, 961234567
Uganda 256 9 +256772123456, 0772123456, 772123456
Republic of the Congo 242 9, keeping its leading 0 +242067700123, 067700123

Gravv refuses a number in any other format at payout, not when you save the payee.

Steps

1. Save the payee

When you save the payee, Gravv checks the wallet's name with the operator and replaces account_name with it.

curl --request POST \
 --url https://api.gravv.xyz/v1/external-accounts \
 --header 'Api-Key: <API_KEY>' \
 --header 'Content-Type: application/json' \
 --header 'Idempotency-Key: a396d467-ed6a-425c-9a80-c1ef779e4495' \
 --data '
{
"customer_id": "e0c40bf4-c1a6-4714-bde1-316a1c15e956",
"payee_type": "mobile_money",
"currency": "RWF",
"account_name": "Claudine Uwase",
"phone_number": "+250788123456",
"institution_id": "e3077661-0359-4740-b6bb-bad5f4e965fd"
}
'
const response = await fetch("https://api.gravv.xyz/v1/external-accounts", {
method: "POST",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
"Content-Type": "application/json",
"Idempotency-Key": "a396d467-ed6a-425c-9a80-c1ef779e4495",
},
body: JSON.stringify({
"customer_id": "e0c40bf4-c1a6-4714-bde1-316a1c15e956",
"payee_type": "mobile_money",
"currency": "RWF",
"account_name": "Claudine Uwase",
"phone_number": "+250788123456",
"institution_id": "e3077661-0359-4740-b6bb-bad5f4e965fd"
}),
});
console.log(response.status, await response.json());
import os

import requests

response = requests.post(
"https://api.gravv.xyz/v1/external-accounts",
headers={
    "Api-Key": os.environ["GRAVV_API_KEY"],
    "Content-Type": "application/json",
    "Idempotency-Key": "a396d467-ed6a-425c-9a80-c1ef779e4495",
},
json={
    "customer_id": "e0c40bf4-c1a6-4714-bde1-316a1c15e956",
    "payee_type": "mobile_money",
    "currency": "RWF",
    "account_name": "Claudine Uwase",
    "phone_number": "+250788123456",
    "institution_id": "e3077661-0359-4740-b6bb-bad5f4e965fd",
},
)
print(response.status_code, response.json())
{
  "data": {
    "id": "ad2fee6e-161f-4b04-ac5f-39ed4e9182bc",
    "institution_id": "e3077661-0359-4740-b6bb-bad5f4e965fd",
    "payee_type": "mobile_money",
    "institution_type": "mobile_money",
    "institution_name": "MTN MoMo Rwanda",
    "account_name": "CLAUDINE UWASE",
    "customer_id": "e0c40bf4-c1a6-4714-bde1-316a1c15e956",
    "currency": "RWF",
    "institution_country_iso_code": "RW",
    "status": "active",
    "phone_number": "+250788123456",
    "date_created": "2026-10-13T11:00:04.871Z"
  },
  "error": null
}

2. Send

curl --request POST \
 --url https://api.gravv.xyz/v1/transfer \
 --header 'Api-Key: <API_KEY>' \
 --header 'Content-Type: application/json' \
 --header 'Idempotency-Key: 04bd7913-20a3-4369-84a4-8232f1cdae2a' \
 --data '
{
"customer_id": "e0c40bf4-c1a6-4714-bde1-316a1c15e956",
"source": {
"source_type": "internal_account",
"id": "5151220a-b97c-442a-8cf5-b48ec96c8779"
},
"destination": {
"destination_type": "external_account",
"id": "ad2fee6e-161f-4b04-ac5f-39ed4e9182bc"
},
"amount": "150.00",
"description": "School fees",
"client_reference": "jp-rw-2026-10-003",
"additional_information": {
"remitter_full_name": "Jean-Paul Habimana",
"remitter_country": "CA"
}
}
'
const response = await fetch("https://api.gravv.xyz/v1/transfer", {
method: "POST",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
"Content-Type": "application/json",
"Idempotency-Key": "04bd7913-20a3-4369-84a4-8232f1cdae2a",
},
body: JSON.stringify({
"customer_id": "e0c40bf4-c1a6-4714-bde1-316a1c15e956",
"source": {
  "source_type": "internal_account",
  "id": "5151220a-b97c-442a-8cf5-b48ec96c8779"
},
"destination": {
  "destination_type": "external_account",
  "id": "ad2fee6e-161f-4b04-ac5f-39ed4e9182bc"
},
"amount": "150.00",
"description": "School fees",
"client_reference": "jp-rw-2026-10-003",
"additional_information": {
  "remitter_full_name": "Jean-Paul Habimana",
  "remitter_country": "CA"
}
}),
});
console.log(response.status, await response.json());
import os

import requests

response = requests.post(
"https://api.gravv.xyz/v1/transfer",
headers={
    "Api-Key": os.environ["GRAVV_API_KEY"],
    "Content-Type": "application/json",
    "Idempotency-Key": "04bd7913-20a3-4369-84a4-8232f1cdae2a",
},
json={
    "customer_id": "e0c40bf4-c1a6-4714-bde1-316a1c15e956",
    "source": {
        "source_type": "internal_account",
        "id": "5151220a-b97c-442a-8cf5-b48ec96c8779",
    },
    "destination": {
        "destination_type": "external_account",
        "id": "ad2fee6e-161f-4b04-ac5f-39ed4e9182bc",
    },
    "amount": "150.00",
    "description": "School fees",
    "client_reference": "jp-rw-2026-10-003",
    "additional_information": {
        "remitter_full_name": "Jean-Paul Habimana",
        "remitter_country": "CA",
    },
},
)
print(response.status_code, response.json())
{
  "data": {
    "amount": "150",
    "amount_currency": "USD",
    "reference": "c62d1693-2a10-4897-8b1d-279d3d64a646",
    "date_created": "2026-10-13T11:02:40.300Z",
    "transfer_status": "pending",
    "transaction_id": "22a31043-d85f-44b8-abfd-94bd2924a529",
    "local_amount": "213000",
    "local_currency": "RWF",
    "fee": "1",
    "fee_currency": "USD",
    "requires_approval": false
  },
  "error": null
}

3. Follow it

Use the webhooks, or poll GET /v1/transactions/22a31043-d85f-44b8-abfd-94bd2924a529. Gravv asks the operator for the status, so the completion arrives as a later webhook.

What can go wrong

Response Why Fix
400 account verification failed: bank could not resolve account holder: invalid input when saving The number is not an active wallet Check the number
400 account verification temporarily unavailable; please retry: invalid input The operator's name check is down Retry with a new key
400 transfers to RW/RWF are not supported at this time No route can pay this country and currency now Contact support
400 this payee is not yet set up for transfers — please contact support to complete its setup The payee is on another row, or has no institution_id Save the payee on the right operator row
400 destination currency is required for international transfers The payee has no currency Use the country's currency: RWF, ZMW, UGX or XAF
transfer.status.failed after acceptance, and a refund row The phone number is not in an accepted format, the sender's name or country is missing, or the operator refused the amount Save a new payee with the right number, add the sender details, or change the amount
transfer.status.failed, but the transaction is still pending The operator's response was unclear, so a person checks it Do not send it again. Contact support with the reference

Webhooks you will receive

  1. external_account.created.
  2. transfer.status.pending, several times.
  3. transfer.status.completed or transfer.status.failed.

Testing in sandbox

Sandbox does not simulate the wallet name check. It calls the operator's own test system, so the results depend on that system.

  • A Uganda payee on +256772123456 was saved, and a 5.00 USD payout completed within 3 minutes.
  • Rwanda numbers were refused at save time.
  • Zambia returned account verification temporarily unavailable on repeated tries.

Reference

Was this page helpful?