---
title: "Re-quote without selling twice"
description: "When a quote expires or the order call fails, get a new price and end up with exactly one order."
---
This recipe gets a new price when a quote expires or the order call fails, so that you end up with exactly one order.

**Who it is for.** Any app where a person looks at a price before confirming, so quotes expire and calls time out.

**Status.** Available.

## Money example

Bola wants to sell 200,000 NGN.

1. At 10:00 you get a quote of 130 USDC, which expires at 10:05.
2. Bola confirms at 10:07. The order returns `quote is invalid or expired`, and Gravv takes nothing.
3. You get a new quote with a new key. The new quote is 129.6 USDC and expires at 10:12. Bola confirms the new price.
4. You place the order with the new quote, and Gravv takes the 200,000 NGN once.

## Before you start

- The prerequisites of [Convert naira to USDC](/recipes/fx/convert-naira-to-usdc#before-you-start).
- Keep your own record for each conversion: your `client_reference`, the keys you used, and the `order_id` once you have it.

## Find out what is required

The quote's `expires_at` is the deadline. A quote lasts about 5 minutes, but always read the field. You can use a quote once.

## Steps

### 1. First quote

<CodeGroup>
```bash title="cURL"
curl --request POST \
     --url https://api.gravv.xyz/v1/fx/quote \
     --header 'Api-Key: <API_KEY>' \
     --header 'Idempotency-Key: 6e4e441e-707e-48aa-a0af-05af5807b43c' \
     --header 'Content-Type: application/json' \
     --data '
{
  "currency_pair": "NGN_USDC",
  "amount": "200000"
}
'
```
```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": "6e4e441e-707e-48aa-a0af-05af5807b43c",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "currency_pair": "NGN_USDC",
    "amount": "200000"
  }),
});
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": "6e4e441e-707e-48aa-a0af-05af5807b43c",
        "Content-Type": "application/json",
    },
    json={
        "currency_pair": "NGN_USDC",
        "amount": "200000",
    },
)
print(response.status_code, response.json())
```
</CodeGroup>

### 2. An order after expiry

If you place the order after `expires_at`, the API returns 400 `quote is invalid or expired`. No money moves.

### 3. Re-quote with a new key

Send the same body with a **new** `Idempotency-Key`. Reusing the first key replays the old, expired quote.

<CodeGroup>
```bash title="cURL"
curl --request POST \
     --url https://api.gravv.xyz/v1/fx/quote \
     --header 'Api-Key: <API_KEY>' \
     --header 'Idempotency-Key: 80d28a9d-50a7-4e1a-ba6a-61db6e79dd8d' \
     --header 'Content-Type: application/json' \
     --data '
{
  "currency_pair": "NGN_USDC",
  "amount": "200000"
}
'
```
```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": "80d28a9d-50a7-4e1a-ba6a-61db6e79dd8d",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "currency_pair": "NGN_USDC",
    "amount": "200000"
  }),
});
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": "80d28a9d-50a7-4e1a-ba6a-61db6e79dd8d",
        "Content-Type": "application/json",
    },
    json={
        "currency_pair": "NGN_USDC",
        "amount": "200000",
    },
)
print(response.status_code, response.json())
```
</CodeGroup>

```json title="Response"
{
  "data": {
    "quote_id": "GRVOTC_4E5F60",
    "from_currency": "NGN",
    "to_currency": "USDC",
    "to_amount": "129.6",
    "from_amount": "200000",
    "rate": "0.000648",
    "expires_at": "2026-10-13T10:12:00Z"
  },
  "error": null
}
```

Show the new price and ask Bola again.

### 4. Order with a new key and the same reference

<CodeGroup>
```bash title="cURL"
curl --request POST \
     --url https://api.gravv.xyz/v1/fx/orders \
     --header 'Api-Key: <API_KEY>' \
     --header 'Idempotency-Key: dd9aa615-0c74-4c23-ad8f-ca9dacc48f71' \
     --header 'Content-Type: application/json' \
     --data '
{
  "quote_id": "GRVOTC_4E5F60",
  "source": {
    "type": "internal_account",
    "id": "bd9c66b3-ad3c-4d6d-9a3d-1fa7bc8960a9"
  },
  "destination": {
    "type": "internal_crypto_wallet",
    "id": "972a8469-1641-4f82-8b9d-2434e465e150"
  },
  "client_reference": "conv-bola-0001"
}
'
```
```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": "dd9aa615-0c74-4c23-ad8f-ca9dacc48f71",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "quote_id": "GRVOTC_4E5F60",
    "source": {
      "type": "internal_account",
      "id": "bd9c66b3-ad3c-4d6d-9a3d-1fa7bc8960a9"
    },
    "destination": {
      "type": "internal_crypto_wallet",
      "id": "972a8469-1641-4f82-8b9d-2434e465e150"
    },
    "client_reference": "conv-bola-0001"
  }),
});
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": "dd9aa615-0c74-4c23-ad8f-ca9dacc48f71",
        "Content-Type": "application/json",
    },
    json={
        "quote_id": "GRVOTC_4E5F60",
        "source": {
            "type": "internal_account",
            "id": "bd9c66b3-ad3c-4d6d-9a3d-1fa7bc8960a9",
        },
        "destination": {
            "type": "internal_crypto_wallet",
            "id": "972a8469-1641-4f82-8b9d-2434e465e150",
        },
        "client_reference": "conv-bola-0001",
    },
)
print(response.status_code, response.json())
```
</CodeGroup>

### 5. If the order call times out or answers a 5xx

Retry with the **same** key and the same body. The API returns the result of the first call. While the first call still runs, the API returns 409 `idempotency_in_flight` with `Retry-After: 1`. Wait, then retry.

### 6. If the order could not be charged

On 400 `order source could not be charged`, list your pending orders and look for your `client_reference`. The list has no reference or date filter, so page from the newest order.

<CodeGroup>
```bash title="cURL"
curl --request GET \
     --url 'https://api.gravv.xyz/v1/fx/orders?status=pending&items_per_page=100' \
     --header 'Api-Key: <API_KEY>'
```
```javascript title="Node"
const response = await fetch("https://api.gravv.xyz/v1/fx/orders?status=pending&items_per_page=100", {
  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?status=pending&items_per_page=100",
    headers={
        "Api-Key": os.environ["GRAVV_API_KEY"],
    },
)
print(response.status_code, response.json())
```
</CodeGroup>

- If you find the order and it is `pending`, the debit may have happened. **Do not place another order.** [Follow it](/recipes/fx/follow-list-and-cancel-orders) and give support its `id`.
- If you do not find the order, or it is `cancelled`, Gravv took nothing. Re-quote and start again.

## What can go wrong

| Response | Why | Fix |
| :-- | :-- | :-- |
| 400 `quote is invalid or expired` | The quote passed `expires_at`, was already used, or belongs to another business | Get a new quote with a new key, and show the new price |
| The quote call returns an expired quote | You reused the key | Use a new key for every new quote |
| 422 `idempotency_key_reused` on the order | The same key with another body, such as a new `quote_id` | Use a new key for each new quote |
| A 400 comes back after you fixed the body | Gravv replays 4xx responses for 24 hours under the same key | Use a new key after any 4xx |
| 429 `Too many request` | More than 600 requests a minute | Back off, then retry with a **new** key |
| 400 `order source could not be charged` | The debit was refused or its outcome is not known | Follow step 6 before anything else |
| 503 `service_unavailable` | FX is unreachable | Retry later. For an order call, follow step 6 before you place the order again |
| You want "buy exactly 1,000 USDC" | Quotes take the amount you sell | Work out the sell amount from a display rate, quote it, and show the real `to_amount` |

## Webhooks you will receive

Gravv sends no webhooks for FX orders.

## Testing in sandbox

The sandbox needs an OTC rate for any quote. Without one, you can still test the error paths:

- An unknown `quote_id` returns `quote is invalid or expired`.
- A repeated key with the same body replays the 400.
- The same key with another body returns 422 `idempotency_key_reused`.

## Reference

- [Place an OTC order](/platform/foreign-exchange/place-an-otc-order)
- [List orders](/api-reference/fx/get-v1-fx-orders)
