---
title: "Convert naira to USDC"
description: "Sell a customer's naira from their NGN account for USDC in their own USDC wallet, at a quoted rate."
---
This recipe sells a customer's naira from their NGN account for USDC in their own USDC wallet, at a quoted rate.

**Who it is for.** A Lagos savings app whose customers keep naira on Gravv and want to move part of it into USDC in their own Gravv wallet.

**Status.** Available once Gravv sets an OTC rate for your business.

## Money example

Chidi has 1,600,000 NGN in his NGN account, and a USDC wallet.

1. You get a quote to sell 1,530,000 NGN for USDC. The rate is 0.00065, so Chidi gets 994.5 USDC. The price holds until `expires_at`.
2. Chidi taps "Convert". You place the order with that quote. Gravv takes 1,530,000 NGN from his NGN account at once, and the order is `pending`.
3. 994.5 USDC reaches his USDC wallet, and the order is `completed`.

## Before you start

- The NGN account is `active`, in NGN, and holds the sell amount plus any transfer fee on your pricing. The account is held on a wallet, so it has `wallet_address` and `blockchain_network`.
- The USDC wallet belongs to the same customer.
- A scoped key needs `fx:read` for the quote and `fx:order_create` for the order.

## Find out what is required

The quote is the real check. For a pair not priced for your business, the quote returns `quote currency pair is not supported`. Rates from `GET /v1/fx/rates` are for display only.

Read the source account:

<CodeGroup>
```bash title="cURL"
curl --request GET \
     --url https://api.gravv.xyz/v2/accounts/bd9c66b3-ad3c-4d6d-9a3d-1fa7bc8960a9 \
     --header 'Api-Key: <API_KEY>'
```
```javascript title="Node"
const response = await fetch("https://api.gravv.xyz/v2/accounts/bd9c66b3-ad3c-4d6d-9a3d-1fa7bc8960a9", {
  method: "GET",
  headers: {
    "Api-Key": process.env.GRAVV_API_KEY,
  },
});
console.log(response.status, await response.json());
```
```python title="Python"
import os

import requests

response = requests.get(
    "https://api.gravv.xyz/v2/accounts/bd9c66b3-ad3c-4d6d-9a3d-1fa7bc8960a9",
    headers={
        "Api-Key": os.environ["GRAVV_API_KEY"],
    },
)
print(response.status_code, response.json())
```
</CodeGroup>

`currency` must be NGN and `balance` must cover the sell amount. Gravv checks only the balance number, not the account's currency, so you must pick the right account.

## Steps

### 1. Check the destination wallet

Gravv does not check that an internal destination can hold the currency you buy.

<CodeGroup>
```bash title="cURL"
curl --request GET \
     --url https://api.gravv.xyz/v1/wallets/972a8469-1641-4f82-8b9d-2434e465e150 \
     --header 'Api-Key: <API_KEY>'
```
```javascript title="Node"
const response = await fetch("https://api.gravv.xyz/v1/wallets/972a8469-1641-4f82-8b9d-2434e465e150", {
  method: "GET",
  headers: {
    "Api-Key": process.env.GRAVV_API_KEY,
  },
});
console.log(response.status, await response.json());
```
```python title="Python"
import os

import requests

response = requests.get(
    "https://api.gravv.xyz/v1/wallets/972a8469-1641-4f82-8b9d-2434e465e150",
    headers={
        "Api-Key": os.environ["GRAVV_API_KEY"],
    },
)
print(response.status_code, response.json())
```
</CodeGroup>

Check that `customer_id` is Chidi's id and that a `USDC` asset is listed.

### 2. Get a quote

<CodeGroup>
```bash title="cURL"
curl --request POST \
     --url https://api.gravv.xyz/v1/fx/quote \
     --header 'Api-Key: <API_KEY>' \
     --header 'Idempotency-Key: e19fb31b-9e07-4fea-9e0b-5d5661e2cb51' \
     --header 'Content-Type: application/json' \
     --data '
{
  "currency_pair": "NGN_USDC",
  "amount": "1530000"
}
'
```
```javascript title="Node"
const response = await fetch("https://api.gravv.xyz/v1/fx/quote", {
  method: "POST",
  headers: {
    "Api-Key": process.env.GRAVV_API_KEY,
    "Idempotency-Key": "e19fb31b-9e07-4fea-9e0b-5d5661e2cb51",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "currency_pair": "NGN_USDC",
    "amount": "1530000"
  }),
});
console.log(response.status, await response.json());
```
```python title="Python"
import os

import requests

response = requests.post(
    "https://api.gravv.xyz/v1/fx/quote",
    headers={
        "Api-Key": os.environ["GRAVV_API_KEY"],
        "Idempotency-Key": "e19fb31b-9e07-4fea-9e0b-5d5661e2cb51",
        "Content-Type": "application/json",
    },
    json={
        "currency_pair": "NGN_USDC",
        "amount": "1530000",
    },
)
print(response.status_code, response.json())
```
</CodeGroup>

```json title="Response"
{
  "data": {
    "quote_id": "GRVOTC_7A41C2",
    "from_currency": "NGN",
    "to_currency": "USDC",
    "to_amount": "994.5",
    "from_amount": "1530000",
    "rate": "0.00065",
    "expires_at": "2026-10-13T10:05:00Z"
  },
  "error": null
}
```

`amount` is always what you **sell**. `rate` is units of USDC per 1 NGN. `to_amount` is not rounded, so round it for display only. Show Chidi `to_amount` and `expires_at`, and get his confirmation.

### 3. Place the order

Place the order before `expires_at`. Do not send an amount, because the quote sets it.

<CodeGroup>
```bash title="cURL"
curl --request POST \
     --url https://api.gravv.xyz/v1/fx/orders \
     --header 'Api-Key: <API_KEY>' \
     --header 'Idempotency-Key: 7ffdf36d-8cbf-418d-ab68-5bb07a6bacd2' \
     --header 'Content-Type: application/json' \
     --data '
{
  "quote_id": "GRVOTC_7A41C2",
  "source": {
    "type": "internal_account",
    "id": "bd9c66b3-ad3c-4d6d-9a3d-1fa7bc8960a9"
  },
  "destination": {
    "type": "internal_crypto_wallet",
    "id": "972a8469-1641-4f82-8b9d-2434e465e150"
  },
  "client_reference": "conv-chidi-000187",
  "metadata": {
    "app_user": "chidi"
  }
}
'
```
```javascript title="Node"
const response = await fetch("https://api.gravv.xyz/v1/fx/orders", {
  method: "POST",
  headers: {
    "Api-Key": process.env.GRAVV_API_KEY,
    "Idempotency-Key": "7ffdf36d-8cbf-418d-ab68-5bb07a6bacd2",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "quote_id": "GRVOTC_7A41C2",
    "source": {
      "type": "internal_account",
      "id": "bd9c66b3-ad3c-4d6d-9a3d-1fa7bc8960a9"
    },
    "destination": {
      "type": "internal_crypto_wallet",
      "id": "972a8469-1641-4f82-8b9d-2434e465e150"
    },
    "client_reference": "conv-chidi-000187",
    "metadata": {
      "app_user": "chidi"
    }
  }),
});
console.log(response.status, await response.json());
```
```python title="Python"
import os

import requests

response = requests.post(
    "https://api.gravv.xyz/v1/fx/orders",
    headers={
        "Api-Key": os.environ["GRAVV_API_KEY"],
        "Idempotency-Key": "7ffdf36d-8cbf-418d-ab68-5bb07a6bacd2",
        "Content-Type": "application/json",
    },
    json={
        "quote_id": "GRVOTC_7A41C2",
        "source": {
            "type": "internal_account",
            "id": "bd9c66b3-ad3c-4d6d-9a3d-1fa7bc8960a9",
        },
        "destination": {
            "type": "internal_crypto_wallet",
            "id": "972a8469-1641-4f82-8b9d-2434e465e150",
        },
        "client_reference": "conv-chidi-000187",
        "metadata": {
            "app_user": "chidi",
        },
    },
)
print(response.status_code, response.json())
```
</CodeGroup>

```json title="Response"
{
  "data": {
    "order_id": "17fc695a-07a0-4a6e-8822-e8f36c031199",
    "quote_id": "GRVOTC_7A41C2",
    "status": "pending"
  },
  "error": null
}
```

The naira debit has already started. Save `order_id`.

### 4. Follow the order

<CodeGroup>
```bash title="cURL"
curl --request GET \
     --url https://api.gravv.xyz/v1/fx/orders/17fc695a-07a0-4a6e-8822-e8f36c031199 \
     --header 'Api-Key: <API_KEY>'
```
```javascript title="Node"
const response = await fetch("https://api.gravv.xyz/v1/fx/orders/17fc695a-07a0-4a6e-8822-e8f36c031199", {
  method: "GET",
  headers: {
    "Api-Key": process.env.GRAVV_API_KEY,
  },
});
console.log(response.status, await response.json());
```
```python title="Python"
import os

import requests

response = requests.get(
    "https://api.gravv.xyz/v1/fx/orders/17fc695a-07a0-4a6e-8822-e8f36c031199",
    headers={
        "Api-Key": os.environ["GRAVV_API_KEY"],
    },
)
print(response.status_code, response.json())
```
</CodeGroup>

Poll until `status` is `completed`, `cancelled` or `rejected`. `pending` covers both "debit running" and "debited, waiting to settle". There is no progress field.

### 5. See where the money went

- For the naira, `GET /v1/transactions?account_id=bd9c66b3-ad3c-4d6d-9a3d-1fa7bc8960a9&direction=out` shows an outgoing row of 1,530,000 NGN whose `description` is `OTC order 17fc695a-07a0-4a6e-8822-e8f36c031199 source debit`.
- For the USDC, `GET /v1/wallets/972a8469-1641-4f82-8b9d-2434e465e150` shows the balance 994.5 higher once the order is `completed`.

## What can go wrong

| Response | Why | Fix |
| :-- | :-- | :-- |
| 400 `quote currency pair is not supported` | The pair is not priced for your business, or its format is wrong | Send `FROM_TO` (`NGN_USDC`). If the format is right, [ask for the pair](/recipes/fx/check-whether-a-pair-can-be-traded) |
| 400 `quote amount must be a positive number` | The amount is missing, zero or negative | Send a positive decimal string |
| 400 `invalid request encoding` | You sent `amount` as a number | Send a string, such as `"amount": "1530000"` |
| 400 `quote is invalid or expired` on the order | The quote is expired, already used, another business's, or missing. The API checks the quote first | [Re-quote with a new key](/recipes/fx/re-quote-without-selling-twice) |
| 400 `invalid order source` / `invalid order destination` | The `type` is not allowed, or the `id` is missing | Use `internal_account` or `internal_crypto_wallet` as the source. Use either of those or `external_account` as the destination |
| 400 `bad source: [400] invalid internal account id` | The id is unknown, or belongs to another business | Use your own account id |
| 400 `bad source: [400] source balance is not enough for currency: NGN, amount: 1530000` | The source balance is below the sell amount | Fund the source, then re-quote |
| 400 `bad destination: [400] invalid order destination` | The destination is unknown or not yours | Fix the id |
| 400 `bad source: [503] service unavailable: …` | A lookup did not respond | Retry later with a new quote and key |
| 400 `order source could not be charged` | The debit was refused, or its outcome is not known yet | **Do not place it again yet.** [Look for the order](/recipes/fx/re-quote-without-selling-twice#6-if-the-order-could-not-be-charged) |
| 403 `feature_access_required` | Your business has no OTC trade desk access | Request access |
| 403 `forbidden`, `required_any` `fx:order_create` | The scoped key lacks the permission | Grant `fx:order_create` to the key |
| The order stays `pending` a long time | The order is waiting to settle, and you have nothing to send | Keep polling, and contact support with `order_id`. Do not cancel |
| The order is `cancelled` without you cancelling | The naira debit failed and moved nothing | Check the balance and re-quote |

## Webhooks you will receive

Gravv sends no webhooks for an FX order. Poll the order.

## Testing in sandbox

The sandbox has no OTC rates today. Every quote returns `quote currency pair is not supported`. Ask support to set a rate on your sandbox business before you build the flow.

## Reference

- [Place an OTC order](/platform/foreign-exchange/place-an-otc-order)
- [Get FX quote](/api-reference/fx/post-v1-fx-quote)
- [Create order](/api-reference/fx/post-v1-fx-orders)
