> ## Documentation Index
> Fetch the complete documentation index at: https://doc.raliopay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Overview

> Understand how FX Swaps work in RalioPay: quoting, execution, lifecycle, rates, fees, and errors.

## What an FX Swap is

An **FX Swap** converts funds between two accounts of the same user that hold different currencies. It is an internal wallet-to-wallet operation: the origin account is debited in its currency and the destination account is credited in its currency at an exchange rate supplied by an external liquidity provider (e.g. `EUR/USD`).

<Info>
  The exchange rate is always set by the external provider — RalioPay never quotes its own rates. RalioPay's margin is applied as an explicit **fee over the origin amount** (see [Fees](#fees)), never as a hidden spread on the rate.
</Info>

## Two ways to execute

<Steps>
  <Step title="Quote first (guaranteed rate)">
    Call [Create FX quote](/api-reference/accounts/create-fx-quote) to lock a rate. The quote returns a `quoteId` valid for **60 seconds** (`quote.validUntil`). Then call [Execute FX swap](/api-reference/accounts/execute-fx-swap) with that `quoteId` — the locked rate and amounts are used. A quote can be used only once.
  </Step>

  <Step title="Direct execution (market rate)">
    Skip the quote and call [Execute FX swap](/api-reference/accounts/execute-fx-swap) directly with `recipient` and an amount. The swap executes at the provider's market rate at that moment.
  </Step>
</Steps>

When executing with a `quoteId`, do not send `fromAmount`, `toAmount`, or `recipient` — they are already fixed by the quote. When executing without a `quoteId`, `recipient` is required along with exactly one amount field. `ownReference` and `description` are always required.

## Amount direction: fromAmount vs toAmount

Both the quote and the direct execution accept **exactly one** of two amount fields, which determines the conversion direction:

| Field        | Direction | Meaning                                                                                                             |
| :----------- | :-------- | :------------------------------------------------------------------------------------------------------------------ |
| `fromAmount` | SELL      | You fix the amount debited from the origin account; the credited amount is derived from the provider's sell rate.   |
| `toAmount`   | BUY       | You fix the amount credited to the destination account; the debited amount is derived from the provider's buy rate. |

The response always carries a single `rate` field (the applicable provider rate for the chosen direction) together with the currency `pair` (e.g. `"EUR/USD"`).

## Lifecycle

Execution is **asynchronous**: the creation response returns the order in `PENDING` state while the origin funds are placed on hold and the swap is delegated to the provider.

```
PENDING → SETTLED
        ↘ FAILED
```

| State     | Description                                                                                                       |
| :-------- | :---------------------------------------------------------------------------------------------------------------- |
| `PENDING` | The swap was accepted, origin funds are on hold, and the provider is executing the conversion.                    |
| `SETTLED` | The conversion finished and the destination account was credited. `settledAt` is set.                             |
| `FAILED`  | The provider rejected or could not execute the swap. The hold on the origin funds is released. `failedAt` is set. |

Track progress by polling [Get FX swap order details](/api-reference/accounts/get-fx-swap-order-details) (`detailUrl` in the creation response) or, preferably, by subscribing to the [FX swap webhook events](/webhooks-events/fx/overview).

<Note>
  The REST `state` field uses `PENDING` / `SETTLED` / `FAILED`, while webhook payloads report progress in `data.status` as `PROCESSING` / `COMPLETED` / `FAILED`. They describe the same lifecycle with different vocabularies — do not mix them.
</Note>

## Fees

The total fee is charged in the **origin currency** over the origin amount and is returned in the `fee` object with its breakdown:

```json theme={null}
"fee": {
  "amount": 1500,
  "currency": "EUR",
  "breakdown": {
    "systemFee": 1000,
    "merchantFee": 500
  }
}
```

For a SELL swap the fee is deducted from `fromAmount` before conversion; for a BUY swap it is added on top of the derived origin amount. The provider's rate is passed through unchanged.

## Amounts

All monetary amounts are expressed in **minor units** (e.g. cents): `100000` with currency `EUR` means `1000.00 EUR`. Request amounts (`fromAmount`, `toAmount`) and every `{ "amount", "currency" }` object in responses follow this rule.

## Available endpoints

* [Create FX quote](/api-reference/accounts/create-fx-quote): locks a provider rate for 60 seconds and returns a `quoteId`.
* [Execute FX swap](/api-reference/accounts/execute-fx-swap): executes the conversion, with or without a `quoteId`.
* [Get FX swap order details](/api-reference/accounts/get-fx-swap-order-details): retrieves the current state of an FX swap order.

## Errors

Error responses follow the standard shape:

```json theme={null}
{
  "error": {
    "type": "UNPROCESSABLE_ERROR",
    "code": "QUOTE_EXPIRED",
    "message": "FX quote has expired"
  }
}
```

| HTTP    | Code              | Message                                                                  | When                                                                                            |
| :------ | :---------------- | :----------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------- |
| 422     | `QUOTE_EXPIRED`   | FX quote has expired                                                     | The `quoteId` is older than its 60-second validity window.                                      |
| 404     | `NOT_FOUND`       | FX quote not found                                                       | Unknown `quoteId`, or the quote belongs to a different account.                                 |
| 422     | `INVALID_REQUEST` | FX quote has already been used                                           | A `quoteId` can execute only one swap.                                                          |
| 400     | `INVALID_REQUEST` | Insufficient balance                                                     | The origin account cannot cover the debited amount plus fees.                                   |
| 400/422 | `INVALID_REQUEST` | This FX quote cannot be processed at the moment. Please contact support. | FX is not enabled for the merchant, the currency pair is not configured, or pricing is missing. |
| 400     | `INVALID_REQUEST` | origin and destination accounts must have different currencies           | Both accounts hold the same currency.                                                           |
| 400     | `INVALID_REQUEST` | recipient.accountId not found / not active                               | The destination account does not exist or is not active.                                        |

<Warning>
  `QUOTE_EXPIRED` is the only dedicated machine-readable error code for FX swaps. For every other case, key your handling off the HTTP status and the `message` field rather than the generic `code`.
</Warning>
