Place an OTC order

Get a quote, place the order before it expires, and follow it to completed. Cancel only an order waiting for approval.

This page shows how to place an OTC order, track it, and cancel it. An OTC order exchanges one currency for another at the rate of a quote, and settles the proceeds to an account, a wallet, or a saved settlement instruction. You place it in two calls: get a quote, then create the order with the quote's quote_id.

A money example. You sell 1,000 USDC for naira at 1,368.27. The quote says you receive 1,368,270 NGN. You place the order with your NGN settlement instruction as the destination. The 1,000 USDC leaves your wallet at once, and the order turns completed when the 1,368,270 NGN reaches your bank account.

Only the NGN and USDC pair is set up for OTC orders today. A pair that the rates endpoints list can still fail to quote, with quote currency pair is not supported. The sandbox has no OTC rates yet, so every quote there fails with that error.

Step 1: Get a quote

Use the Get FX Quote endpoint. Send currency_pair as FROM_TO, with the currency you sell first. Send amount as a string that holds the amount you sell, in the FROM currency:

curl --request POST \
     --url https://api.gravv.xyz/v1/fx/quote \
     --header 'Api-Key: <API_KEY>' \
     --header 'Idempotency-Key: a0b5abbd-03a8-4a9e-8e44-59a4145f9bff' \
     --header 'Content-Type: application/json' \
     --data '
{
  "currency_pair": "USDC_NGN",
  "amount": "1000"
}
'

The response gives the rate, as units of the TO currency per 1 FROM, and the amount you will receive:

{
  "data": {
    "quote_id": "GRVOTC_3F9A1C",
    "from_currency": "USDC",
    "to_currency": "NGN",
    "to_amount": "1368270",
    "from_amount": "1000",
    "rate": "1368.27",
    "expires_at": "2026-10-12T09:25:00Z"
  },
  "error": null
}

The quote lasts until expires_at, about 5 minutes. You can use it once, and only to create an order. It does not lock a rate for a transfer. Sending the same Idempotency-Key again returns the same quote, even after it has expired, so use a new key for a new quote.

Step 2: Create the order

Use the Create order endpoint before the quote expires. The amounts come from the quote:

Field Type Description
quote_id string The quote_id from Step 1
source object Where the funds come from, as { "type": …, "id": … }. type is internal_account or internal_crypto_wallet. It must hold the quote's from_amount in the FROM currency
destination object Where the proceeds go, as { "type": …, "id": … }. type is internal_account, internal_crypto_wallet or external_account. Here, external_account means an active saved settlement instruction in the TO currency
client_reference string Optional. Your own reference
metadata object Optional. Gravv returns it on the order
curl --request POST \
     --url https://api.gravv.xyz/v1/fx/orders \
     --header 'Api-Key: <API_KEY>' \
     --header 'Idempotency-Key: 4ad6137f-8653-4b32-a37b-05f780e21161' \
     --header 'Content-Type: application/json' \
     --data '
{
  "quote_id": "GRVOTC_3F9A1C",
  "source": { "type": "internal_account", "id": "4b3a2c1d-0e9f-4a8b-9c7d-6e5f4a3b2c1d" },
  "destination": { "type": "external_account", "id": "b7e1c2d3-4f5a-4b6c-9d8e-7f6a5b4c3d2e" },
  "client_reference": "inv-2026-1042"
}
'

An order placed with an API key needs no approval. It starts as pending, and Gravv debits the source at once:

{
  "data": {
    "order_id": "2d4f6a8c-1b3d-4e5f-8a7b-9c0d1e2f3a4b",
    "quote_id": "GRVOTC_3F9A1C",
    "status": "pending"
  },
  "error": null
}

The order stays pending until Gravv delivers the proceeds, then becomes completed. Gravv sends no webhook for FX orders, so follow the order with Get order.

Every error is HTTP 400 with a message in error:

Error What to do
quote is invalid or expired Get a new quote
invalid order source / invalid order destination Use one of the types above, with an id
bad source: [400] source balance is not enough for currency: USDC, amount: 1000 Fund the source
bad destination: [400] invalid order destination Use an active settlement instruction in the TO currency
order source could not be charged The debit was refused, or its outcome is not known yet. Do not place the order again yet. List your pending orders and look for your client_reference. If the order is there, wait for it. If not, get a new quote

Order status

Status Description
pending Gravv accepted the order and is debiting, or has debited, the source. Delivery is in progress
completed Gravv delivered the proceeds
cancelled The order was cancelled while it waited for approval, or its debit failed and nothing moved
rejected Gravv rejected the order
waiting_approval A Dashboard order is waiting for an approver. Orders placed with an API key never wait

Track an order

Check a single order with Get order using its order_id:

curl --request GET \
     --url https://api.gravv.xyz/v1/fx/orders/2d4f6a8c-1b3d-4e5f-8a7b-9c0d1e2f3a4b \
     --header 'Api-Key: <API_KEY>'

List your orders with List orders, newest first. status is the only filter that works, and it must be in lower case (status=PENDING returns nothing). The API ignores date, side and search filters, so filter by created_at on your side. Page with page and items_per_page (max 100):

curl --request GET \
     --url 'https://api.gravv.xyz/v1/fx/orders?status=pending&items_per_page=20' \
     --header 'Api-Key: <API_KEY>'

Cancel an order

You can cancel only an order that is waiting_approval. That is a Dashboard order that Gravv has not yet taken any money for. Use Cancel order:

curl --request POST \
     --url https://api.gravv.xyz/v1/fx/orders/451b4cf3-6123-4df7-b656-af7229d4beef/cancel \
     --header 'Api-Key: <API_KEY>' \
     --header 'Idempotency-Key: f137e4c2-2281-4dfd-b1a2-e06f25ad4d1b'
{
  "data": {
    "id": "451b4cf3-6123-4df7-b656-af7229d4beef",
    "status": "cancelled"
  },
  "error": null
}

Do not cancel a pending order. Gravv has debited its source, or is debiting it. While the debit runs, Gravv refuses the cancel with order source debit is still in flight. Once the debit completes, Gravv accepts the cancel, but it does not return the money. Contact support instead.

Error What to do
order cannot be cancelled in status: <status> The order is already final, so there is nothing to cancel
order source debit is still in flight Do not cancel. Wait for completed
transaction not found Check the order id

If you use a scoped API key, it needs fx:order_cancel to cancel an order.

Was this page helpful?