Send money to Ghana
Pay a mobile wallet or bank account in Ghana in cedis, send the sender's details up front, and resume a transfer that pauses for sender details.
Use this recipe to pay a mobile wallet or bank account in Ghana in cedis, send the sender's details up front, and resume a transfer that pauses for sender details.
Who it is for. Kwame Mensah, a nurse in London who uses your app's USD balance, sends money to his mother's mobile wallet in Kumasi and sometimes pays his landlord's Ghanaian bank account.
Status. Not yet available. Ask support to confirm it is switched on for your business. The steps below show what the route needs.
Money example
- Kwame sends 300.00 USD from his USD account. Say the fee on your plan is 2.00 USD.
- Say the rate is 15.20 GHS per USD. The transfer response prices 4,560.00 GHS for his mother.
- The route needs the sender's full name, date of birth, email and home address. Kwame's profile has no date of birth, so the transfer returns
paused, and nothing is paid out yet. - You resume the transfer with his date of birth and address, and it goes ahead.
Before you start
- Kwame's customer profile is as complete as you can make it: first and last name, email,
date_of_birth, and an address withaddress_line1,city,postal_codeandcountry(GET /v1/customers/{customer_id}). Any missing detail causes a pause. - A business sender must still give a person's name and date of birth, such as a director's, because the route pays person to person.
- His USD account is
activeand funded.
Find out what is required
- The currency.
GET /v1/transfer/supported-countries?iso_code=GHreturnsGHS. - The wallet operator or bank. List them with
institution_type=mobile_moneyorinstitution_type=bank.
curl --request GET \
--url 'https://api.gravv.xyz/v1/external-accounts/institutions?country_iso_code=GH&institution_type=mobile_money' \
--header 'Api-Key: <API_KEY>'
const response = await fetch("https://api.gravv.xyz/v1/external-accounts/institutions?country_iso_code=GH&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=GH&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": "GH",
"currency": "GHS",
"id": "c060a286-a785-4f76-b866-a40e8fe7570a",
"max_amount": 10000,
"min_amount": 1,
"name": "Airtel Money"
},
{
"account_number_type": "phone_number",
"country_iso_code": "GH",
"currency": "GHS",
"id": "ad28c4e4-c303-4505-aa2f-7df94e0588cd",
"max_amount": 10000,
"min_amount": 1,
"name": "MTN Mobile Money"
}
],
"items_per_page": 10,
"page": 1,
"total_items": 2,
"total_pages": 1
},
"error": null
}
If account_number_type is phone_number, the payee carries phone_number. If it is bank_account_number, the payee carries account_number, which is 10 to 16 digits in Ghana.
- The rate. Get it with
GET /v1/transfer/rates?currency=GHS. - The sender's address form.
GET /v1/transfer/supported-countries-for-address?iso_code=GBlists the country's states. - The sender details the route needs. Only the pause itself tells you these. No call lists them in advance, so send them up front when you have them.
Steps
1. Save the wallet payee
Put the number in phone_number, in international format with +. Gravv does not check or reformat the number when you save the payee, so check it yourself.
curl --request POST \
--url https://api.gravv.xyz/v1/external-accounts \
--header 'Api-Key: <API_KEY>' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: 5e674882-3611-413b-b7f6-d11ed371f64b' \
--data '
{
"customer_id": "b4dab3d6-6a7f-4c66-9cc9-483404cd8463",
"payee_type": "mobile_money",
"currency": "GHS",
"account_name": "Akosua Mensah",
"phone_number": "+233244123456",
"institution_id": "ad28c4e4-c303-4505-aa2f-7df94e0588cd",
"institution_country_iso_code": "GH"
}
'
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": "5e674882-3611-413b-b7f6-d11ed371f64b",
},
body: JSON.stringify({
"customer_id": "b4dab3d6-6a7f-4c66-9cc9-483404cd8463",
"payee_type": "mobile_money",
"currency": "GHS",
"account_name": "Akosua Mensah",
"phone_number": "+233244123456",
"institution_id": "ad28c4e4-c303-4505-aa2f-7df94e0588cd",
"institution_country_iso_code": "GH"
}),
});
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": "5e674882-3611-413b-b7f6-d11ed371f64b",
},
json={
"customer_id": "b4dab3d6-6a7f-4c66-9cc9-483404cd8463",
"payee_type": "mobile_money",
"currency": "GHS",
"account_name": "Akosua Mensah",
"phone_number": "+233244123456",
"institution_id": "ad28c4e4-c303-4505-aa2f-7df94e0588cd",
"institution_country_iso_code": "GH",
},
)
print(response.status_code, response.json())
The API returns HTTP 200 with status active and the payee id. In this example the id is d1762ec1-9f61-4eed-a04b-21925a55dbe4.
For the landlord's bank account instead, send "payee_type": "bank_account", "account_number": "1234567890123" and a bank's institution_id. You can check a bank account's holder first with verify. That call never checks a phone number.
2. Send, with the sender's details up front
Gravv uses the fields in additional_information before the profile, so the transfer does not pause. Gravv splits remitter_full_name at the first space into a first and a last name. The email always comes from the customer profile.
curl --request POST \
--url https://api.gravv.xyz/v1/transfer \
--header 'Api-Key: <API_KEY>' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: 3233634a-8ff7-4561-9111-a0b28c463ebc' \
--data '
{
"customer_id": "b4dab3d6-6a7f-4c66-9cc9-483404cd8463",
"source": {
"source_type": "internal_account",
"id": "245b9314-abed-4632-8fb1-ea2ca70baa3c"
},
"destination": {
"destination_type": "external_account",
"id": "d1762ec1-9f61-4eed-a04b-21925a55dbe4"
},
"amount": "300.00",
"description": "For Mum, October",
"client_reference": "kwame-gh-2026-10-007",
"additional_information": {
"remitter_full_name": "Kwame Mensah",
"remitter_date_of_birth": "1988-04-17",
"remitter_country": "GB",
"remitter_address": {
"address_line1": "27 Ashburnham Road",
"city": "London",
"postal_code": "SW10 0PL",
"country": "GB"
}
}
}
'
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": "3233634a-8ff7-4561-9111-a0b28c463ebc",
},
body: JSON.stringify({
"customer_id": "b4dab3d6-6a7f-4c66-9cc9-483404cd8463",
"source": {
"source_type": "internal_account",
"id": "245b9314-abed-4632-8fb1-ea2ca70baa3c"
},
"destination": {
"destination_type": "external_account",
"id": "d1762ec1-9f61-4eed-a04b-21925a55dbe4"
},
"amount": "300.00",
"description": "For Mum, October",
"client_reference": "kwame-gh-2026-10-007",
"additional_information": {
"remitter_full_name": "Kwame Mensah",
"remitter_date_of_birth": "1988-04-17",
"remitter_country": "GB",
"remitter_address": {
"address_line1": "27 Ashburnham Road",
"city": "London",
"postal_code": "SW10 0PL",
"country": "GB"
}
}
}),
});
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": "3233634a-8ff7-4561-9111-a0b28c463ebc",
},
json={
"customer_id": "b4dab3d6-6a7f-4c66-9cc9-483404cd8463",
"source": {
"source_type": "internal_account",
"id": "245b9314-abed-4632-8fb1-ea2ca70baa3c",
},
"destination": {
"destination_type": "external_account",
"id": "d1762ec1-9f61-4eed-a04b-21925a55dbe4",
},
"amount": "300.00",
"description": "For Mum, October",
"client_reference": "kwame-gh-2026-10-007",
"additional_information": {
"remitter_full_name": "Kwame Mensah",
"remitter_date_of_birth": "1988-04-17",
"remitter_country": "GB",
"remitter_address": {
"address_line1": "27 Ashburnham Road",
"city": "London",
"postal_code": "SW10 0PL",
"country": "GB",
},
},
},
)
print(response.status_code, response.json())
3. If the transfer is paused
If the request and the profile lack sender details, the response looks like this:
{
"data": {
"amount": "300",
"amount_currency": "USD",
"reference": "3019b5b3-1ad0-4f00-809a-8e52f727ba0d",
"date_created": "2026-10-13T09:20:11.204Z",
"transfer_status": "paused",
"transaction_id": "d12e8ee7-1965-4e9c-8fbf-c341a9c79f0f",
"fee": "2",
"fee_currency": "USD",
"requires_approval": false,
"status": "awaiting_remitter_information",
"park_reason": "remitter_information",
"pause_reason": "sender_details_required",
"requirements": [
{
"type": "remitter_date_of_birth",
"description": "resume with additional_information.remitter_date_of_birth: the sender's date of birth as YYYY-MM-DD"
},
{
"type": "remitter_address",
"description": "resume with additional_information.remitter_address: address_line1, city and postal_code of the sender"
}
],
"expires_at": "2026-10-16T09:20:11Z"
},
"error": null
}
Each requirements[].type names a field to send: remitter_full_name, remitter_date_of_birth, remitter_address, remitter_country or customer_email. For customer_email, add an email to the customer, then resume. The pause ends at expires_at, 72 hours later by default.
4. Resume it
The id in the path is the transfer reference. Use a new Idempotency-Key on every attempt. The resume call refuses unknown fields.
curl --request POST \
--url https://api.gravv.xyz/v1/transfer/3019b5b3-1ad0-4f00-809a-8e52f727ba0d/resume \
--header 'Api-Key: <API_KEY>' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: 9e18276b-99b0-4345-b9c6-36c24be4487f' \
--data '
{
"additional_information": {
"remitter_date_of_birth": "1988-04-17",
"remitter_address": {
"address_line1": "27 Ashburnham Road",
"city": "London",
"postal_code": "SW10 0PL",
"country": "GB"
}
}
}
'
const response = await fetch("https://api.gravv.xyz/v1/transfer/3019b5b3-1ad0-4f00-809a-8e52f727ba0d/resume", {
method: "POST",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
"Content-Type": "application/json",
"Idempotency-Key": "9e18276b-99b0-4345-b9c6-36c24be4487f",
},
body: JSON.stringify({
"additional_information": {
"remitter_date_of_birth": "1988-04-17",
"remitter_address": {
"address_line1": "27 Ashburnham Road",
"city": "London",
"postal_code": "SW10 0PL",
"country": "GB"
}
}
}),
});
console.log(response.status, await response.json());
import os
import requests
response = requests.post(
"https://api.gravv.xyz/v1/transfer/3019b5b3-1ad0-4f00-809a-8e52f727ba0d/resume",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
"Content-Type": "application/json",
"Idempotency-Key": "9e18276b-99b0-4345-b9c6-36c24be4487f",
},
json={
"additional_information": {
"remitter_date_of_birth": "1988-04-17",
"remitter_address": {
"address_line1": "27 Ashburnham Road",
"city": "London",
"postal_code": "SW10 0PL",
"country": "GB",
},
},
},
)
print(response.status_code, response.json())
{
"data": {
"transfer_id": "3019b5b3-1ad0-4f00-809a-8e52f727ba0d",
"status": "approved_awaiting_execution",
"transfer_status": "pending",
"resumed": true
},
"error": null
}
If a detail is still missing, the API returns HTTP 200 with resumed: false and the remaining requirements. Send those with another new key. For customer_email, update the customer with PUT /v1/customers/{customer_id}, then resume with {}.
5. Follow it
Use the webhooks, or poll GET /v1/transactions/d12e8ee7-1965-4e9c-8fbf-c341a9c79f0f until its status is completed or failed.
What can go wrong
| Response | Why | Fix |
|---|---|---|
400 ghanaian account numbers must be 10-16 digits |
The bank account number is too short or too long | Ask for the full number |
400 phone number is required for mobile money payees: invalid input |
The phone number is in account_number |
Put it in phone_number |
400 account number may contain only letters and digits; '+' is not allowed: invalid input |
You sent a +233… number in account_number |
Put it in phone_number |
400 the payee is missing required information for this transfer type — please update the payee details |
Gravv's record of the operator or bank lacks the route's code | Contact support with the payee id |
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 with an operator institution_id |
400 transfers to GH/GHS are not available at this time |
The route is not open for your business | Contact support |
200 transfer_status paused, pause_reason sender_details_required |
The request and the profile lacked a sender detail | Resume with the listed fields |
400 remitter_date_of_birth must be a date as YYYY-MM-DD |
The date is in the wrong format, such as 17/04/1988 |
Send 1988-04-17 |
400 remitter_date_of_birth cannot be in the future |
The year has a typo | Fix the date |
400 invalid request body: json: unknown field "remitter_email" |
The resume call does not accept this field | Add the email to the customer profile |
400 this transfer is not paused |
The transfer was already resumed, or was never paused | Read the transaction |
410 ERROR_CODE_TRANSFER_PAUSE_EXPIRED |
The pause is past expires_at |
Send a new transfer with the details included |
410 ERROR_CODE_TRANSFER_PAUSE_CANCELLED |
Gravv cancelled the pause | Send a new transfer |
400 insufficient balance on resume |
The balance was spent while the transfer was paused | Fund the account and resume again |
transfer.status.failed with reason sender_details_expired |
Nobody resumed the transfer before the pause ended. Gravv returned the fee | Send again with the details |
transfer.status.failed, but the transaction is still pending |
The transfer is in manual review | Do not send it again. Contact support with the reference |
Webhooks you will receive
external_account.created.- If the transfer pauses,
transfer.requires_information, withreasonsender_details_requiredand the requirements inlimit_details.requirements. - After the resume,
transfer.resumed. transfer.status.pending, several times, thentransfer.status.completedortransfer.status.failed. If a pause runs out, Gravv sendstransfer.status.failedwithreasonsender_details_expired(orsender_details_cancelled).
Testing in sandbox
You can save Ghana payees in sandbox. They are active, and wallets get no name check. But Ghana payouts in sandbox return the payee is missing required information for this transfer type — please update the payee details today, so you cannot produce a payout or the sender-details pause.
Sandbox does not let you create a customer without date_of_birth. Once the route works, use a business sender to try the pause.