Pay a mobile wallet in Kenya
Save a Kenyan mobile wallet payee on the right operator row, check the wallet's name and send shillings.
Use this recipe to save a Kenyan mobile wallet payee on the right operator row, check the wallet's name, and send shillings.
Who it is for. A payroll app for a Dubai logistics firm that pays a driver's mobile wallet in Nairobi every week.
Status. Not yet available. Ask support to confirm that Gravv has switched it on for your business. The steps below show what the route needs.
Money example
- The employer's USD account sends 200.00 USD. Say the fee on your plan is 1.00 USD.
- Say the rate is 129.00 KES per USD. The transfer response prices 25,800.00 KES for the driver.
- The driver's wallet receives the shillings.
Before you start
- The sender has a name:
first_nameandlast_name, orbusiness_namefor a business (GET /v1/customers/{customer_id}). Kenyan routes refuse a payout with no sender name. If you are unsure, sendadditional_information.remitter_full_name. - The USD account is
activeand funded.
Find out what is required
The operator row
curl --request GET \
--url 'https://api.gravv.xyz/v1/external-accounts/institutions?country_iso_code=KE&institution_type=mobile_money' \
--header 'Api-Key: <API_KEY>'
const response = await fetch("https://api.gravv.xyz/v1/external-accounts/institutions?country_iso_code=KE&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=KE&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": "KE",
"currency": "KES",
"id": "267a9823-de79-4bfc-99f2-d9edeb8e7b86",
"max_amount": 0,
"min_amount": 0,
"name": "Airtel Money Kenya"
},
{
"account_number_type": "phone_number",
"country_iso_code": "KE",
"currency": "KES",
"id": "0c6324d5-b26d-4cb3-8f6c-db84c763338b",
"max_amount": 0,
"min_amount": 0,
"name": "M-Pesa Kenya"
},
{
"account_number_type": "phone_number",
"country_iso_code": "KE",
"currency": "KES",
"id": "e8518e20-16f3-4390-86e6-55f14feee01d",
"max_amount": 7500000,
"min_amount": 1,
"name": "Airtel"
},
{
"account_number_type": "phone_number",
"country_iso_code": "KE",
"currency": "KES",
"id": "9121e0a3-fb54-4760-b9cb-c303efd3af1c",
"max_amount": 7500000,
"min_amount": 1,
"name": "M-Pesa"
}
],
"items_per_page": 10,
"page": 1,
"total_items": 4,
"total_pages": 1
},
"error": null
}
Pick the row named M-Pesa Kenya or Airtel Money Kenya. The rows with shorter names come from another list, and this route is not set up for them.
The phone number
A Kenyan mobile number is 254, then 7 or 1, then 8 digits. Gravv reads +254 712 345 678, 0712345678 and 712345678 all as 254712345678.
Check the number yourself before you save the payee. If the wallet's name check cannot find the number, the API refuses the save. Gravv refuses a number with any other wrong format only at payout.
The rate
Get the rate with GET /v1/transfer/rates?currency=KES.
Steps
1. Save the wallet payee
When you save the payee, Gravv looks up the wallet's registered name 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: d5ad1237-53fc-429e-96ff-32d91280f5eb' \
--data '
{
"customer_id": "877231ff-f5e2-4ca2-9e1d-11ee4be57723",
"payee_type": "mobile_money",
"currency": "KES",
"account_name": "Brian Otieno",
"phone_number": "+254712345678",
"institution_id": "0c6324d5-b26d-4cb3-8f6c-db84c763338b"
}
'
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": "d5ad1237-53fc-429e-96ff-32d91280f5eb",
},
body: JSON.stringify({
"customer_id": "877231ff-f5e2-4ca2-9e1d-11ee4be57723",
"payee_type": "mobile_money",
"currency": "KES",
"account_name": "Brian Otieno",
"phone_number": "+254712345678",
"institution_id": "0c6324d5-b26d-4cb3-8f6c-db84c763338b"
}),
});
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": "d5ad1237-53fc-429e-96ff-32d91280f5eb",
},
json={
"customer_id": "877231ff-f5e2-4ca2-9e1d-11ee4be57723",
"payee_type": "mobile_money",
"currency": "KES",
"account_name": "Brian Otieno",
"phone_number": "+254712345678",
"institution_id": "0c6324d5-b26d-4cb3-8f6c-db84c763338b",
},
)
print(response.status_code, response.json())
{
"data": {
"id": "5f22bdc3-72df-46be-b8a8-176c2942294f",
"institution_id": "0c6324d5-b26d-4cb3-8f6c-db84c763338b",
"payee_type": "mobile_money",
"institution_type": "mobile_money",
"institution_name": "M-Pesa Kenya",
"account_name": "BRIAN OTIENO",
"customer_id": "877231ff-f5e2-4ca2-9e1d-11ee4be57723",
"currency": "KES",
"institution_country_iso_code": "KE",
"status": "active",
"phone_number": "+254712345678",
"date_created": "2026-10-13T10:02:31.551Z"
},
"error": null
}
Show the user account_name, which is the wallet's registered name, and ask them to confirm that it is the driver.
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: f4b4216c-a048-45e2-a7fc-b0ef1d878340' \
--data '
{
"customer_id": "877231ff-f5e2-4ca2-9e1d-11ee4be57723",
"source": {
"source_type": "internal_account",
"id": "8925d474-9e46-4bc2-8d5b-9afd1b1a44e5"
},
"destination": {
"destination_type": "external_account",
"id": "5f22bdc3-72df-46be-b8a8-176c2942294f"
},
"amount": "200.00",
"description": "Week 41 wages",
"client_reference": "fleet-ke-wk41-0042",
"additional_information": {
"remitter_full_name": "Fatima Al Mansoori",
"remitter_country": "AE"
}
}
'
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": "f4b4216c-a048-45e2-a7fc-b0ef1d878340",
},
body: JSON.stringify({
"customer_id": "877231ff-f5e2-4ca2-9e1d-11ee4be57723",
"source": {
"source_type": "internal_account",
"id": "8925d474-9e46-4bc2-8d5b-9afd1b1a44e5"
},
"destination": {
"destination_type": "external_account",
"id": "5f22bdc3-72df-46be-b8a8-176c2942294f"
},
"amount": "200.00",
"description": "Week 41 wages",
"client_reference": "fleet-ke-wk41-0042",
"additional_information": {
"remitter_full_name": "Fatima Al Mansoori",
"remitter_country": "AE"
}
}),
});
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": "f4b4216c-a048-45e2-a7fc-b0ef1d878340",
},
json={
"customer_id": "877231ff-f5e2-4ca2-9e1d-11ee4be57723",
"source": {
"source_type": "internal_account",
"id": "8925d474-9e46-4bc2-8d5b-9afd1b1a44e5",
},
"destination": {
"destination_type": "external_account",
"id": "5f22bdc3-72df-46be-b8a8-176c2942294f",
},
"amount": "200.00",
"description": "Week 41 wages",
"client_reference": "fleet-ke-wk41-0042",
"additional_information": {
"remitter_full_name": "Fatima Al Mansoori",
"remitter_country": "AE",
},
},
)
print(response.status_code, response.json())
{
"data": {
"amount": "200",
"amount_currency": "USD",
"reference": "da01d483-2dd2-4965-8334-b2b416e2a23e",
"date_created": "2026-10-13T10:05:10.118Z",
"transfer_status": "pending",
"transaction_id": "187c4388-caab-4310-a4a3-87ec5152e533",
"local_amount": "25800",
"local_currency": "KES",
"fee": "1",
"fee_currency": "USD",
"requires_approval": false
},
"error": null
}
3. Follow it
Use the webhooks, or poll GET /v1/transactions/187c4388-caab-4310-a4a3-87ec5152e533. On the transaction, channel is mobile_money.
Bank accounts in Kenya
To pay a Kenyan bank account, follow the same steps with "payee_type": "bank_account", an account_number of 8 to 20 letters or digits, and a bank's institution_id from institution_type=bank. You can pay only the banks that the institutions list returns for KE.
What can go wrong
| Response | Why | Fix |
|---|---|---|
400 kenyan account numbers must be 8-20 alphanumeric characters |
The bank account number has the wrong format | Fix the number |
400 account verification failed: bank could not resolve account holder: invalid input when saving |
The wallet number is not registered | Check the number with the recipient |
400 a recipient with the same account name, account number and bank already exists, with data.payee_id |
A payee with the same name, phone number and operator exists | Use data.payee_id |
400 transfers to KE/KES are not available at this time |
The route is not open for your business | Contact support |
400 this payee is not yet set up for transfers — please contact support to complete its setup |
The payee has no operator institution_id |
Save the payee on the right operator row |
transfer.status.failed after acceptance, and a refund row |
The number is not a Kenyan mobile number in the form above, the sender has no name, or the amount is outside the route's limits | You cannot change a phone number, so save a new payee with the right number. Add remitter_full_name, or change the amount |
transfer.status.failed, but the transaction is still pending |
The transfer is in manual review because the outcome was unclear | Do not send it again. Contact support with the reference |
Webhooks you will receive
external_account.created.transfer.status.pending, several times, withworkflow_typeglobal_remittance.transfer.status.completedortransfer.status.failed.
Testing in sandbox
You can save the wallet payee in sandbox. Its account_name becomes the phone number itself (254712345678), so do not show it as a person's name.
Gravv accepts a payout, and the payout goes through several pending steps. Then Gravv sends transfer.status.failed with transfer_status manual_processing_required, while the transaction stays pending. A Kenya payout cannot complete in sandbox, so use it to test how you handle manual review.