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

# FX conversion

> Quote a rate, show it, then execute on it.

Converting between two currencies you hold is a two-call flow. You **preview** to get a rate and a
`previewId`, then you **execute** against that id. The rate you were shown is the rate you get.

This exists because a quote you cannot act on is not useful. Splitting the call lets you show a
number to a customer, let them think about it, and settle on that number rather than on whatever
the market has moved to by the time they answer.

## Preview

Specify **either** `fromAmount` or `toAmount`, not both. `fromAmount` sells a fixed amount;
`toAmount` buys a fixed amount and tells you what it will cost.

```bash theme={null}
curl -X POST https://api.stablemint.net/v1/fx/preview \
  -H "ApiKey: $STABLEMINT_API_KEY" \
  -H "Timestamp: $TIMESTAMP" \
  -H "Nonce: $NONCE" \
  -H "Signature: $SIGNATURE" \
  -H "Content-Type: application/json" \
  -d '{ "fromCurrency": "USD", "toCurrency": "EUR", "fromAmount": 100000 }'
```

```json theme={null}
{
  "previewId": "3f1b9c44-7e2a-4c81-9f0d-2b6a5e8d1c33",
  "fxRate": 0.9132,
  "fromCurrency": "USD",
  "fromAmount": 100000,
  "toCurrency": "EUR",
  "toAmount": 91320,
  "fromCurrencyPreviousBalance": 284500.12,
  "expiryTime": "2026-09-14T15:32:10Z"
}
```

| Field                         | Meaning                                                                                                    |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `previewId`                   | The handle you execute against.                                                                            |
| `fxRate`                      | All-in rate. Any spread is already in this number — there is no separate fee to add.                       |
| `toAmount`                    | What you receive. This is the figure to show a customer.                                                   |
| `fromCurrencyPreviousBalance` | Your balance in the sell currency before the conversion, so you can show the effect without a second call. |
| `expiryTime`                  | After this instant the `previewId` is dead.                                                                |

Previewing moves no money and has no side effects. Quote as often as you need.

## When a preview is refused

A preview StableMint will not book comes back as `400` — including when the only thing wrong is
that you cannot afford it. The body is **not** a problem document: it is the quote you asked for,
plus a `failureReason` saying why it will not be booked, and no `previewId`.

```json theme={null}
{
  "fromCurrency": "USD",
  "fromAmount": 500000,
  "toCurrency": "EUR",
  "toAmount": 456600,
  "fxRate": 0.9132,
  "fromCurrencyPreviousBalance": 284500.12,
  "expiryTime": "2026-09-14T15:32:10Z",
  "notes": "Insufficient balance: 500000 USD -> 456600 EUR",
  "failureReason": "NoBalance",
  "minFromAmount": 1.00,
  "maxFromAmount": 1000000.00,
  "minToAmount": 1.00,
  "maxToAmount": 1000000.00
}
```

| `failureReason`               | Meaning                                                                                                                                         |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `NoBalance`                   | Your balance in `fromCurrency` is below `fromAmount`. Compare it against `fromCurrencyPreviousBalance`, which is returned on the same body.     |
| `AmountOutOfRange`            | The amount falls outside the limits, which come back on the response so you can correct it in one step. Read them rather than hard-coding them. |
| `InvalidCurrency`             | The pair is not convertible.                                                                                                                    |
| `InsufficientMasterLiquidity` | StableMint cannot fund a conversion of that size right now.                                                                                     |

<Warning>
  **A `400` here does not mean your request was malformed.** `NoBalance` and
  `InsufficientMasterLiquidity` are both worth retrying — the first once the account is funded, the
  second after a wait — even though [Errors](/guides/errors) treats `400` as "retrying unchanged
  will not help" everywhere else. Read `failureReason` before deciding.
</Warning>

Check `failureReason` before reaching for `previewId`: a refused preview has no id to execute
against.

## Execute

```bash theme={null}
curl -X POST https://api.stablemint.net/v1/fx/execute \
  -H "ApiKey: $STABLEMINT_API_KEY" \
  -H "Timestamp: $TIMESTAMP" \
  -H "Nonce: $NONCE" \
  -H "Signature: $SIGNATURE" \
  -H "Content-Type: application/json" \
  -d '{ "previewId": "3f1b9c44-7e2a-4c81-9f0d-2b6a5e8d1c33" }'
```

<Warning>
  **A `previewId` is single-use.** It is consumed on the first successful execute. Replaying the
  same id does not book a second conversion — but it is not an idempotent read either, so treat a
  non-success response as an unknown outcome and check
  [Transactions](/guides/transactions) for a `Conversion` before retrying with a fresh preview.
</Warning>

The response restates the rate and amounts that were booked. It may also carry `scaOperationId` and
`maxAttemptCount`, which indicate the conversion requires a strong customer authentication step
before it settles; handle those fields if your flow can produce them rather than assuming every
execute completes inline.

## Handling expiry

<Steps>
  <Step title="Preview, and record expiryTime">
    Treat it as a hard deadline, not a guideline.
  </Step>

  <Step title="Show the rate for less time than you have">
    Leave yourself enough margin to make the execute call and handle a retry.
  </Step>

  <Step title="Re-preview rather than chase">
    If the quote lapses, get a new one and show the new number. There is no extension mechanism,
    and executing an expired id fails.
  </Step>
</Steps>

<Note>
  **A conversion is two transactions, not one.** It appears in
  [Transactions](/guides/transactions) as a pair sharing `category: "Conversion"` — an `Outward`
  leg debiting the sell currency and an `Inward` leg crediting the buy currency. Nothing leaves
  StableMint, but do not expect a single row, and do not filter on a `transactionDirection` of
  `Internal`: conversions never carry it.
</Note>

## Currencies

EUR and USD today, in either direction, between accounts you already hold. You cannot convert into
a currency you have no account in.
