FX
Query rates and execute FX trades.
Flutterwave’s FX service allows you to convert currencies directly for your desired use case. This provides easy access to liquidity for your payment operations.
The FX service is not configured on accounts by default, request this feature to be enabled on your account to use this feature.
Converting foreign currency with this service involves four key steps:
- Request a quote(RFQ) for your desired currency exchange.
- Review the pricing received via webhooks or by querying the quote information.
- Initiate the trade using the
quote_idto lock in the quoted rates. - Confirm the trade execution via webhook notifications.
FX quotes and trading operations are available on weekdays only (Monday through Friday).
Requesting an FX Quote (RFQ).
A quote defines the exchange rate and details between a specific currency pair. Every trade requires a fresh quote; once a quote is used to execute a trade, it is locked and cannot be reused for subsequent transactions.
This service exclusively supports trading for NGN/USD, GHS/USD, and USD/NGN currency pairs.
To get a quote, send a request to the RFQ endpoint with the following parameters:
base_currency: The currency you are selling, e.g.NGN,USD,GHS.target_currency: The currency you want to receive, e.g.NGN,USD.quantity: The amount of thebase_currencyyou want to transact.- Example: If
base_currencyisNGNandtarget_currencyisUSD, a quantity of 2,000,000 means you are spending 2,000,000 NGN to buy USD. - Constraint: The minimum trade amount is $1,000 USD (or its equivalent in the base currency).
- Example: If
reference: A unique identifier for the transaction, used for reconciliation. If omitted, the system will automatically generate one.
curl --location 'https://api.flutterwave.com/v3/rfq' \
--header 'Authorization: Bearer {{YOUR_SECRET_KEY}}' \
--header 'Content-Type: application/json' \
--data '{
"base_currency": "NGN",
"target_currency": "USD",
"quantity": 15000000,
"reference": "{{YOUR_UNIQUE_REFERENCE}}"
}'
{
"status": "success",
"message": "Request for quote submitted.",
"data": {
"id": "c085aab5-5938-46a7-90a9-22420c2a8d6e",
"reference": "c9f6539747ef11f1",
"quantity": 1617759,
"instrument": "NGN/USD",
"quote": null,
"status": "NEW",
"created_at": "2026-02-06T10:03:22.000Z"
}
}
{
"status": "error",
"message": "target_currency is required",
"data": null
}
{
"status": "error",
"message": "A quote with this reference already exists.",
"data": null
}
Check Quote Status
When a quote is first generated, its initial status isNEW. To fetch the latest status of a quote, send a GET request to the check quote status endpoint using the quote id (data.id) returned from the RFQ response.
curl --location 'https://api.flutterwave.com/v3/rfq/{{YOUR_QUOTE_ID}}' \
--header 'Authorization: Bearer {{YOUR_SECRET_KEY}}'
Quote Statuses
A quote can have one of the following statuses:
READY– The quote is active, locked in, and available to execute a trade.PROCESSING– A trade has been initiated using this quote.EXPIRED– The quote's 5-minute validity window has passed; it can no longer be used.FAILED– The quote generation failed. This typically occurs due to unsupported currency pairs or failing to meet the minimum trade amount ($1,000 USD or its equivalent).
The quote Object
If the quote's status is READY, the response includes a quote object with the following details:
best_available_rate: The real-time exchange rate applied to the trade. This rate converts theapproved_quantityinto thetotal_value.approved_quantity: The actual amount in thebase_currencyapproved for the trade. Note: This may differ from your requestedquantitybased on liquidity or account limits.total_value: The final converted amount you will receive in thetarget_currency. This is calculated as:approved_quantity×best_available_rate.expiry: The exact date and time the quote expires.
If you are satisfied with the quote details, you can proceed to initiate the trade.
{
"status":"success",
"message":"Quote fetched successfully.",
"data":{
"id":"c085aab5-5938-46a7-90a9-22420c2a8d6e",
"reference":"c9f6539747ef11f1",
"quantity":1617759,
"instrument":"NGN/USD",
"quote":{
"best_available_rate":"0.000659",
"approved_quantity":1517759,
"total_value":1000.203181,
"expiry":"2026-02-06T10:12:02.990Z"
},
"status":"READY",
"created_at":"2026-02-06T10:03:22.000Z",
"complete_message":"Your RFQ is ready"
}
}
{
"event":"quote.completed",
"event.type":"QUOTE",
"data":{
"id":"c085aab5-5938-46a7-90a9-22420c2a8d6e",
"reference":"c9f6539747ef11f1",
"quantity":1617759,
"instrument":"NGN/USD",
"quote":{
"best_available_rate":0.000659,
"approved_quantity":1517759,
"total_value":1000.203181,
"expiry":"2026-02-06T10:12:02.990Z"
},
"status":"READY",
"created_at":"2026-02-06T10:03:22.000Z",
"complete_message":"Your RFQ is ready"
}
}
Quotes expire after 5 minutes. Always check that your quote is still active before initiating a trade to prevent your request from failing.
{
"status": "success",
"message": "Quote fetched successfully.",
"data": {
"id": "c085aab5-5938-46a7-90a9-22420c2a8d6e",
"reference": "c9f6539747ef11f1",
"quantity": 1617759,
"instrument": "NGN/USD",
"quote": {
"best_available_rate": "0.000659",
"approved_quantity": 1517759,
"total_value": 1000.203181,
"expiry": "2026-02-06T10:12:02.990Z"
},
"status": "EXPIRED",
"created_at": "2026-02-06T10:03:22.000Z",
"complete_message": "Your RFQ is expired"
}
}
Initiating a Trade
Once you have a valid quote, you can proceed to execute your trade. To initiate the transaction, send a POST request to the create trade endpoint with the following parameters:
quote_id: The ID of the quote you want to execute.narration: A short note for your own records describing the purpose of the transaction.
curl --location 'https://api.flutterwave.com/v3/trade' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer {{YOUR_SECRET_KEY}}' \
--data '{
"quote_id": "570e21b4-7cf2-4048-bd1b-284bd45762c6",
"narration": "sample exchange"
}'
Response fields
A successfully initiated trade response includes the following key details within the data object:
quantity: The initial amount passed when requesting a quote.approved_quantity: The actual amount in thebase_currencyapproved for execution.price: The final converted amount you will receive in thetarget currency.
{
"status": "success",
"message": "Trade initiated successfully.",
"data": {
"id": "30b85c03-9124-4418-b30b-0495ffbdc633",
"quantity": 1617759,
"approved_quantity": 1517759,
"price": 1000.203181,
"instrument": "NGN/USD",
"status": "NEW",
"recipient": {
"id": "200004751",
"name": "MythCo."
},
"narration": "sample exchange",
"reference": "TRD-86ade016977958e2",
"callback_url": "https://www.helloajadi.com/api/flw/dev",
"created_at": "2026-02-06T10:08:54.000Z"
}
}
{
"status": "error",
"message": "This quote does not exist.",
"data": null
}
Fetching Trade Status
All newly initiated trades start with a status of NEW. To fetch the latest status of a trade, send a GET request to the check trade status endpoint using the trade_id.
curl --location 'https://api.flutterwave.com/v3/trade/96086092-fca3-4f9b-9ce1-90633a9d300a' \
--header 'Authorization: Bearer {{secret_key}}'
During its lifecycle, a trade can transition into one of the following statuses:
-
PENDING– The trade is still being executed. -
FAILED– The trade could not be completed. -
SETTLED– The trade completed successfully, and funds have been exchanged.
{
"status": "success",
"message": "Trade item fetched.",
"data": {
"id": "30b85c03-9124-4418-b30b-0495ffbdc633",
"quantity": 1617759,
"price": 1000.203181,
"instrument": "NGN/USD",
"status": "SETTLED",
"response_message": "Transfer was Successful",
"recipient": {
"id": "200004751",
"name": "MythCo."
},
"narration": "sample exchange",
"reference": "TRD-86ade016977958e2",
"callback_url": "https://www.helloajadi.com/api/flw/dev",
"created_at": "2026-02-06T10:08:54.000Z"
}
}
Handling Webhooks
Whenever a quote or trade status changes, you will receive a webhook notification with details of these changes. Below are examples of expected webhook structures.
Quote events
{
"event": "quote.completed",
"event.type": "QUOTE",
"data": {
"id": "342cd6e7-9015-48c7-92a6-7a81e70bb6f4",
"reference": "5bddd594b3025ce9",
"quantity": 100000,
"instrument": "GHS/USD",
"quote": null,
"status": "PROCESSING",
"created_at": "2026-02-03T11:35:55.000Z",
"complete_message": "Your RFQ is being processed"
}
}
{
"event": "quote.completed",
"event.type": "QUOTE",
"data": {
"id": "c085aab5-5938-46a7-90a9-22420c2a8d6e",
"reference": "c9f6539747ef11f1",
"quantity": 1617759,
"instrument": "NGN/USD",
"quote": {
"best_available_rate": 0.000659,
"approved_quantity": 1517759,
"total_value": 1000.203181,
"expiry": "2026-02-06T10:12:02.990Z"
},
"status": "READY",
"created_at": "2026-02-06T10:03:22.000Z",
"complete_message": "Your RFQ is ready"
}
}
{
"event": "quote.completed",
"event.type": "QUOTE",
"data": {
"id": "c085aab5-5938-46a7-90a9-22420c2a8d6e",
"reference": "c9f6539747ef11f1",
"quantity": 1617759,
"instrument": "NGN/USD",
"quote": {
"best_available_rate": "0.000659",
"approved_quantity": 1517759,
"total_value": 1000.203181,
"expiry": "2026-02-06T10:12:02.990Z"
},
"status": "EXPIRED",
"created_at": "2026-02-06T10:03:22.000Z",
"complete_message": "Your RFQ is expired"
}
}
{
"event": "quote.completed",
"event.type": "QUOTE",
"data": {
"id": "0498f583-b536-4d70-a842-634e155c58b8",
"reference": "eeca16e015cf423b",
"quantity": 15000,
"instrument": "NGN/USD",
"quote": null,
"status": "FAILED",
"created_at": "2026-02-03T14:40:34.000Z",
"complete_message": "Minimum amount is NGN 1517759.30. Please increase the amount or use the regular flow."
}
}
Trade events
{
"event": "transfer.completed",
"event.type": "Transfer",
"data": {
"id": 255169,
"account_number": "42769239",
"bank_name": "wallet",
"bank_code": "wallet",
"fullname": "uzoma kenkwo",
"created_at": "2026-02-03T12:03:59.000Z",
"currency": "USD",
"debit_currency": "NGN",
"amount": 629.705485,
"fee": 0,
"status": "SUCCESSFUL",
"reference": "TRD-7fbcf39210578e4d",
"meta": {
"quote_id": "526366c8-93e7-7dcc-0dd8-00006d4d54f1",
"AccountId": 1668495,
"merchant_id": "300408963"
},
"narration": "sample exchange",
"approver": null,
"complete_message": "Transfer was successful",
"requires_approval": 0,
"is_approved": 1
}
}
{
"event": "transfer.completed",
"event.type": "Transfer",
"data": {
"id": 255169,
"account_number": "42769239",
"bank_name": "wallet",
"bank_code": "wallet",
"fullname": "uzoma kenkwo",
"created_at": "2026-02-03T12:03:59.000Z",
"currency": "USD",
"debit_currency": "NGN",
"amount": 629.705485,
"fee": 0,
"status": "FAILED",
"reference": "TRD-7fbcf39210578e4d",
"meta": {
"quote_id": "342cd6e7-9015-48c7-92a6-7a81e70bb6f4",
"AccountId": 1668495,
"merchant_id": "300408963"
},
"narration": "sample exchange",
"approver": null,
"complete_message": "DISBURSE FAILED: Insufficient balance in customer's wallet",
"requires_approval": 0,
"is_approved": 1
}
}
After completing a successful trade, converted funds are instantly settled into your target currency's payout wallet.
Testing your integration
scenarioshould not be used in production requests for successful RFQ processing.
You can simulate several RFQ outcomes to test different paths in your FX workflow using the scenario field.
curl --location 'https://api.flutterwave.com/v3/rfq' \
--header 'Authorization: Bearer {{YOUR_SECRET_KEY}}' \
--header 'Content-Type: application/json' \
--data '{
"base_currency": "NGN",
"target_currency": "USD",
"quantity": 1500000,
"reference": "{{YOUR_UNIQUE_REFERENCE}}",
"scenario": "ready"
}'
The following scenarios can be mocked in test mode:
ready- mocks a valid quote ready for trading.expired- mocks an expired quote.invalid_amount- mocks the invalid amount error in the quote request process.invalid_currency_pair- mocks the invalid currency pair in the quote request.exceeded_limit- simulates when the limit of an account is reached.
Updated about 4 hours ago
