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.

  1. You save your bank account once as an NGN settlement instruction. This tells Gravv where to send your naira.
  2. You get a quote to sell 1,000 USDC for NGN at a rate of 1,495.5. You receive 1,495,500 NGN.
  3. You place the order with the instruction as the destination. 1,000 USDC leaves your wallet at once.
  4. 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 active settlement instruction whose currency is 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, and payee:create to 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.

Reference

Was this page helpful?