Sell USDC for naira into your bank account
Save your Nigerian bank account as a settlement instruction, then sell USDC from your wallet and have Gravv pay the naira to that account.
Use this recipe to save your Nigerian bank account as a settlement instruction, then sell USDC from your wallet and have Gravv pay the naira to that account. A settlement instruction is a saved record of where Gravv sends the currency you buy.
Who it is for. A crypto business that holds USDC on Gravv and needs naira in its own Nigerian bank account to pay local costs.
Status. Available once Gravv sets an OTC rate for your business.
Money example
Your treasury wallet holds 5,000 USDC.
- You save your bank account once as an NGN settlement instruction. This tells Gravv where to send your naira.
- You get a quote to sell 1,000 USDC for NGN at a rate of 1,495.5. You receive 1,495,500 NGN.
- You place the order with the instruction as the destination. 1,000 USDC leaves your wallet at once.
- Gravv pays 1,495,500 NGN to your bank account, and the order status becomes
completed.
Before you start
- A USDC wallet or USDC account that belongs to your business and holds at least the amount you sell.
- An
activesettlement instruction whosecurrencyis the currency you buy, NGN. You can create it in step 1. - If you use a scoped key, it needs
fx:read,fx:order_create, andpayee:createto create the instruction.
Find out what is required
First list your NGN instructions, so you can reuse one if it exists. The currency filter is exact and case-sensitive.
curl --request GET \
--url 'https://api.gravv.xyz/v1/settlement-instructions?currency=NGN' \
--header 'Api-Key: <API_KEY>'
const response = await fetch("https://api.gravv.xyz/v1/settlement-instructions?currency=NGN", {
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/settlement-instructions?currency=NGN",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
},
)
print(response.status_code, response.json())
If a field is missing, the create call names it in the error. The order call checks that the instruction belongs to your business, is active, and is in the currency you buy.
Steps
1. Save the bank account, once
curl --request POST \
--url https://api.gravv.xyz/v1/settlement-instructions \
--header 'Api-Key: <API_KEY>' \
--header 'Idempotency-Key: b6f21d66-187b-4641-b121-dca35c4fd5c9' \
--header 'Content-Type: application/json' \
--data '
{
"instruction_type": "bank_account",
"account_name": "Harbor Digital Assets Ltd",
"currency": "NGN",
"is_default": true,
"account_number": "3012345678",
"bank_name": "Example Bank",
"bank_code": "011",
"country": "NG"
}
'
const response = await fetch("https://api.gravv.xyz/v1/settlement-instructions", {
method: "POST",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
"Idempotency-Key": "b6f21d66-187b-4641-b121-dca35c4fd5c9",
"Content-Type": "application/json",
},
body: JSON.stringify({
"instruction_type": "bank_account",
"account_name": "Harbor Digital Assets Ltd",
"currency": "NGN",
"is_default": true,
"account_number": "3012345678",
"bank_name": "Example Bank",
"bank_code": "011",
"country": "NG"
}),
});
console.log(response.status, await response.json());
import os
import requests
response = requests.post(
"https://api.gravv.xyz/v1/settlement-instructions",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
"Idempotency-Key": "b6f21d66-187b-4641-b121-dca35c4fd5c9",
"Content-Type": "application/json",
},
json={
"instruction_type": "bank_account",
"account_name": "Harbor Digital Assets Ltd",
"currency": "NGN",
"is_default": True,
"account_number": "3012345678",
"bank_name": "Example Bank",
"bank_code": "011",
"country": "NG",
},
)
print(response.status_code, response.json())
The API returns HTTP 200 with the instruction. In this example its id is 7e0161a9-34d6-4935-b764-d9eb3a7825f7 and its status is active.
Gravv does not check these bank details with any bank. Read them back to the account owner to confirm them. Send the currency in upper case.
2. Check the USDC balance
curl --request GET \
--url https://api.gravv.xyz/v1/wallets/b74d0fb1-32e7-4629-8fad-c1a606cb0fb3 \
--header 'Api-Key: <API_KEY>'
const response = await fetch("https://api.gravv.xyz/v1/wallets/b74d0fb1-32e7-4629-8fad-c1a606cb0fb3", {
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/wallets/b74d0fb1-32e7-4629-8fad-c1a606cb0fb3",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
},
)
print(response.status_code, response.json())
Check that the USDC balance is at least 1000.
3. Get a quote
curl --request POST \
--url https://api.gravv.xyz/v1/fx/quote \
--header 'Api-Key: <API_KEY>' \
--header 'Idempotency-Key: a339813d-6ce1-4d8e-b105-ac6fac7deedd' \
--header 'Content-Type: application/json' \
--data '
{
"currency_pair": "USDC_NGN",
"amount": "1000"
}
'
const response = await fetch("https://api.gravv.xyz/v1/fx/quote", {
method: "POST",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
"Idempotency-Key": "a339813d-6ce1-4d8e-b105-ac6fac7deedd",
"Content-Type": "application/json",
},
body: JSON.stringify({
"currency_pair": "USDC_NGN",
"amount": "1000"
}),
});
console.log(response.status, await response.json());
import os
import requests
response = requests.post(
"https://api.gravv.xyz/v1/fx/quote",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
"Idempotency-Key": "a339813d-6ce1-4d8e-b105-ac6fac7deedd",
"Content-Type": "application/json",
},
json={
"currency_pair": "USDC_NGN",
"amount": "1000",
},
)
print(response.status_code, response.json())
{
"data": {
"quote_id": "GRVOTC_C19E04",
"from_currency": "USDC",
"to_currency": "NGN",
"to_amount": "1495500",
"from_amount": "1000",
"rate": "1495.5",
"expires_at": "2026-10-13T11:05:00Z"
},
"error": null
}
4. Place the order to the instruction
Set the destination to the instruction. Here external_account means a settlement instruction, never a payee.
curl --request POST \
--url https://api.gravv.xyz/v1/fx/orders \
--header 'Api-Key: <API_KEY>' \
--header 'Idempotency-Key: 7094c507-fe4d-434c-b98b-daff98c595e1' \
--header 'Content-Type: application/json' \
--data '
{
"quote_id": "GRVOTC_C19E04",
"source": {
"type": "internal_crypto_wallet",
"id": "b74d0fb1-32e7-4629-8fad-c1a606cb0fb3"
},
"destination": {
"type": "external_account",
"id": "7e0161a9-34d6-4935-b764-d9eb3a7825f7"
},
"client_reference": "treasury-ngn-2026-10-13"
}
'
const response = await fetch("https://api.gravv.xyz/v1/fx/orders", {
method: "POST",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
"Idempotency-Key": "7094c507-fe4d-434c-b98b-daff98c595e1",
"Content-Type": "application/json",
},
body: JSON.stringify({
"quote_id": "GRVOTC_C19E04",
"source": {
"type": "internal_crypto_wallet",
"id": "b74d0fb1-32e7-4629-8fad-c1a606cb0fb3"
},
"destination": {
"type": "external_account",
"id": "7e0161a9-34d6-4935-b764-d9eb3a7825f7"
},
"client_reference": "treasury-ngn-2026-10-13"
}),
});
console.log(response.status, await response.json());
import os
import requests
response = requests.post(
"https://api.gravv.xyz/v1/fx/orders",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
"Idempotency-Key": "7094c507-fe4d-434c-b98b-daff98c595e1",
"Content-Type": "application/json",
},
json={
"quote_id": "GRVOTC_C19E04",
"source": {
"type": "internal_crypto_wallet",
"id": "b74d0fb1-32e7-4629-8fad-c1a606cb0fb3",
},
"destination": {
"type": "external_account",
"id": "7e0161a9-34d6-4935-b764-d9eb3a7825f7",
},
"client_reference": "treasury-ngn-2026-10-13",
},
)
print(response.status_code, response.json())
{
"data": {
"order_id": "6b65a6a4-8b81-48f6-b38a-088ca65ed389",
"quote_id": "GRVOTC_C19E04",
"status": "pending"
},
"error": null
}
5. Follow it
Poll GET /v1/fx/orders/6b65a6a4-8b81-48f6-b38a-088ca65ed389 until the order status is completed. Then use the order's destination_amount and client_reference to match the naira payment on your bank statement.
What can go wrong
| Response | Why | Fix |
|---|---|---|
400 instruction type must be "bank_account" or "crypto_wallet": invalid input |
instruction_type is missing or not in lower case |
Send it in lower case |
400 account name is required: invalid input / currency is required: invalid input |
A required field is missing | Add the missing field |
400 account number is required for bank accounts: invalid input / bank name is required for bank accounts: invalid input |
A bank instruction is missing one or both of these fields | Add both |
400 bad destination: [400] invalid order destination |
The id is a payee id, an unknown or deleted instruction, another business's instruction, or an instruction that is not in the currency you buy |
Use an active instruction whose currency is the quote's to_currency |
400 invalid order destination |
destination.type is ssi, account or wallet, or the destination has no id |
Send {"type": "external_account", "id": "<instruction id>"} |
The list with currency=ngn is empty |
The filter is exact and case-sensitive | Use the stored case, NGN |
| Two instructions for one bank account | Gravv does not check for duplicates | Reuse one and delete the other |
| Any quote or order error | Another page lists these errors | See Convert naira to USDC |
Webhooks you will receive
None. Gravv sends no webhooks for settlement instructions or FX orders.
Testing in sandbox
Settlement instructions work the same in sandbox as in live, because they are records only. To place an order, your sandbox business needs an OTC rate. Without one, the quote call returns quote currency pair is not supported. No real bank payment happens in sandbox.