# Quotes

> How to request a quote, its fallback sources, what it fixes and what it does not, fixed and variable rates, quotes for an exact amount received, expiry, and what happens when no source can quote.

- Source: https://docs.kotova.io/x/api/quotes
- API base URL: https://api.kotova.io/x/v1
- OpenAPI document: https://api.kotova.io/x/v1/openapi.json

A quote asks every eligible source what it would give for your user's trade and
returns the best answer, with the source that offered it and the next best sources.
Requesting a quote has no side effects: nothing is created at any source until you
create an order from it.

## Requesting a quote

`POST /quote` takes the pair, the amount and the rate type, and answers with one
quote: the best source for the request under your filters, priced by the same engine
and ranked by the same rule as the Kotova X app. It needs your API key and has a weight
of 1. The answer carries a single-use `quote_id`, which you send to
[`POST /orders`](https://docs.kotova.io/x/api/orders#creating-an-order) to create the order at that source,
or at one of the quote's [fallback sources](https://docs.kotova.io/x/api/quotes#fallback-sources) when that source cannot
take it.

**curl**

```sh
curl https://api.kotova.io/x/v1/quote \
  -H "Authorization: Bearer $KOTOVA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": {"currency": "BTC", "network": "btc"},
    "to": {"currency": "USDT", "network": "trx"},
    "amount": "0.05",
    "amount_type": "from",
    "rate_type": "variable",
    "end_user_ip": "93.184.216.34",
    "lang": "en"
  }'
```

**JavaScript**

```js
const response = await fetch("https://api.kotova.io/x/v1/quote", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.KOTOVA_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    from: { currency: "BTC", network: "btc" },
    to: { currency: "USDT", network: "trx" },
    amount: "0.05",
    amount_type: "from",
    rate_type: "variable",
    end_user_ip: "93.184.216.34", // your user's public IP address
    lang: "en",
  }),
});
const quote = await response.json();
if (!response.ok) throw new Error(`${quote.error.code}: ${quote.error.message}`);
console.log(quote.quote_id, quote.to.amount, quote.source.name);
```

**Python**

```python
import os
import requests

response = requests.post(
    "https://api.kotova.io/x/v1/quote",
    headers={"Authorization": "Bearer " + os.environ["KOTOVA_API_KEY"]},
    json={
        "from": {"currency": "BTC", "network": "btc"},
        "to": {"currency": "USDT", "network": "trx"},
        "amount": "0.05",
        "amount_type": "from",
        "rate_type": "variable",
        "end_user_ip": "93.184.216.34",  # your user's public IP address
        "lang": "en",
    },
    timeout=30,
)
quote = response.json()
if not response.ok:
    raise RuntimeError(f"{quote['error']['code']}: {quote['error']['message']}")
print(quote["quote_id"], quote["to"]["amount"], quote["source"]["name"])
```

`93.184.216.34` stands in for your user's address. Send the real one, or leave the
field out (see [Your user's IP address](https://docs.kotova.io/x/api/quotes#your-users-ip-address)).

### Request

The body is JSON (`Content-Type: application/json`). Fields the API does not know are
refused with a 400 `invalid_request` (`details.reason: unknown_field`) rather than
ignored, so a misspelt field never goes unnoticed. The endpoint takes no query
parameters.

| Field | Required | Rules |
| --- | --- | --- |
| `from` | Yes | What your user sends: an object with exactly `currency` and `network`, as [`GET /currencies`](https://docs.kotova.io/x/api/currencies) lists them. Case does not matter. |
| `to` | Yes | What your user receives, in the same form. It must differ from `from` (422 `same_currency`). |
| `amount` | Yes | A decimal string greater than zero, such as `"0.05"`: no exponent, no sign, at most 16 integer digits, and at most min(network decimals, 12) fractional digits on the side it refers to. Trailing zeros do not count. More fractional digits are a 400 with `details.reason: too_many_decimals` and `details.max_fraction_digits`. Send a string: a JSON number is accepted only when it prints as a plain decimal. |
| `amount_type` | No | Which side `amount` refers to: `from` (the amount sent, the default) or `to` (the amount to receive). See [Quoting an exact amount received](https://docs.kotova.io/x/api/quotes#quoting-an-exact-amount-received). |
| `rate_type` | Yes | `fixed` or `variable`. There is no default. See [Fixed and variable rates](https://docs.kotova.io/x/api/quotes#fixed-and-variable-rates). |
| `exclude_sources` | No | Source slugs not to use, from [`GET /sources`](https://docs.kotova.io/x/api/sources): at most 20, case-insensitive, duplicates removed. Slugs the API does not know are ignored with an `unknown_source` warning. |
| `freeze_protection` | No | `true` asks only the sources that cannot freeze funds (`freeze_protection: true` in `GET /sources`). Default `false`. |
| `can_provide_refund_address` | No | `false` skips the sources that need a refund address when the order is created. Default `true`. |
| `end_user_ip` | No | Your user's public IPv4 or IPv6 address, never your server's. See [Your user's IP address](https://docs.kotova.io/x/api/quotes#your-users-ip-address). |
| `lang` | No | The language of the source notice: `en` (the default), `de`, `it`, `es`, `fr` or `ru`. A region suffix such as `de-AT` is accepted. Any other value falls back to `en` with an `unsupported_lang` warning instead of an error. |

`freeze_protection` is a filter on this one request. DEX-only mode in the Kotova X app
is a separate setting, stored on the device of whoever uses the app, and has no effect
on the API. See [DEX-only mode](https://docs.kotova.io/x/security/dex-only-mode).

### Response

A quote answers 200 with the quote object. The source block, with its notice, is the
one your user must see before you create an order from this quote, together with the
notices in `fallback_sources`: see
[Display requirements](https://docs.kotova.io/x/api/display-requirements).

```json
{
  "quote_id": "q_5mJc2Vq9XkT3nR8bYw1LdP",
  "created_at": "2026-11-10T10:15:04Z",
  "expires_at": "2026-11-10T10:17:03Z",
  "rate_type": "variable",
  "amount_type": "from",
  "from": { "currency": "BTC", "network": "btc", "amount": "0.05" },
  "to": { "currency": "USDT", "network": "trx", "amount": "3389.8" },
  "rate": "67796",
  "limits": { "min_from_amount": "0.00029412", "max_from_amount": "0.73529411" },
  "estimated_duration_seconds": 1500,
  "requirements": {
    "refund_address": "optional",
    "to_memo": "not_supported",
    "refund_memo": "not_supported"
  },
  "filters": {
    "exclude_sources": [],
    "freeze_protection": false,
    "can_provide_refund_address": true,
    "end_user_ip_provided": true
  },
  "provider": {
    "name": "Kotova X",
    "icon": {
      "url": "https://cdn.kotova.io/images/brand/kotova-x/icon.svg",
      "fallback_url": "https://assets.kotova.io/images/brand/kotova-x/icon.svg"
    },
    "icon_png": {
      "url": "https://cdn.kotova.io/images/brand/kotova-x/icon-256.png",
      "fallback_url": "https://assets.kotova.io/images/brand/kotova-x/icon-256.png"
    },
    "website_url": "https://app.kotova.io"
  },
  "source": {
    "slug": "sideshift",
    "name": "SideShift",
    "kind": "centralized",
    "icon": {
      "url": "https://cdn.kotova.io/images/counterparties/cp_icon_sideshift.svg",
      "fallback_url": "https://assets.kotova.io/images/counterparties/cp_icon_sideshift.svg"
    },
    "terms_url": "https://sideshift.ai/legal",
    "notice": {
      "text": "The exchange service is provided by SideShift. Creating an order confirms your agreement with the SideShift rules.",
      "link_text": "SideShift rules",
      "link_url": "https://sideshift.ai/legal",
      "lang": "en"
    },
    "refund_address_forwarding": "at_creation"
  },
  "fallback_sources": [
    {
      "source": {
        "slug": "changenow",
        "name": "ChangeNOW",
        "kind": "centralized",
        "icon": {
          "url": "https://cdn.kotova.io/images/counterparties/cp_icon_changenow.svg",
          "fallback_url": "https://assets.kotova.io/images/counterparties/cp_icon_changenow.svg"
        },
        "terms_url": "https://changenow.io/terms-of-use",
        "notice": {
          "text": "The exchange service is provided by ChangeNOW. Creating an order confirms your agreement with the ChangeNOW rules.",
          "link_text": "ChangeNOW rules",
          "link_url": "https://changenow.io/terms-of-use",
          "lang": "en"
        },
        "refund_address_forwarding": "at_creation"
      },
      "from": { "amount": "0.05" },
      "to": { "amount": "3376.2" },
      "requirements": { "refund_address": "optional" }
    },
    {
      "source": {
        "slug": "changelly",
        "name": "Changelly",
        "kind": "centralized",
        "icon": {
          "url": "https://cdn.kotova.io/images/counterparties/cp_icon_changelly.svg",
          "fallback_url": "https://assets.kotova.io/images/counterparties/cp_icon_changelly.svg"
        },
        "terms_url": "https://changelly.com/terms-of-use",
        "notice": {
          "text": "The exchange service is provided by Changelly. Creating an order confirms your agreement with the Changelly rules.",
          "link_text": "Changelly rules",
          "link_url": "https://changelly.com/terms-of-use",
          "lang": "en"
        },
        "refund_address_forwarding": "at_creation"
      },
      "from": { "amount": "0.05" },
      "to": { "amount": "3371.45" },
      "requirements": { "refund_address": "optional" }
    }
  ],
  "warnings": []
}
```

The response also carries `X-Kotova-Quote-Cache` (see
[Caching and the fan-out allowance](https://docs.kotova.io/x/api/quotes#caching-and-the-fan-out-allowance)) and the rate
limit headers described in [Rate limits](https://docs.kotova.io/x/api/rate-limits).

| Field | What it is |
| --- | --- |
| `quote_id` | The quote's id: `q_` followed by 22 letters and digits, case-sensitive. Single use: send it to `POST /orders` before `expires_at`. |
| `created_at` | When the quote was made. |
| `expires_at` | When the quote stops being usable: 60 seconds (`fixed`) or 120 seconds (`variable`) after the source answered. See [Freshness and expiry](https://docs.kotova.io/x/api/quotes#freshness-and-expiry). |
| `rate_type`, `amount_type` | As requested. |
| `from` | `currency`, `network` and `amount`: what your user sends, rounded up. |
| `to` | `currency`, `network` and `amount`: what your user receives, rounded down. |
| `rate` | `to.amount` divided by `from.amount`, to 12 significant digits, truncated. Kotova computes it from the two amounts; it is never a source's own rate figure. |
| `limits` | `min_from_amount` and `max_from_amount`: the quoted source's bounds, in units of `from`. Either is `null` when the source reports none; a `null` maximum means no upper limit. |
| `estimated_duration_seconds` | A typical time from deposit to payout for this source and route, or `null` when there is no estimate. It is an estimate, not a commitment. |
| `requirements.refund_address` | `required`: `POST /orders` must send a `refund_address` on the `from` network. `optional`: it may, and we recommend it: where the source takes it with the order, it goes to the source then, so a refund needs no extra step, and fallback sources that need one stay eligible (see [Refund addresses](https://docs.kotova.io/x/api/orders#refund-addresses)). This is the rule of the quote's source; each fallback source states its own. |
| `requirements.to_memo` | `optional` when the `to` network supports a memo or destination tag, `not_supported` when it does not. |
| `requirements.refund_memo` | The same for the `from` network, where a refund would go. When it is `optional`, send `refund_memo` with the refund address if that address needs one. |
| `filters` | The filters as applied: `exclude_sources` (lower-cased, duplicates removed), `freeze_protection`, `can_provide_refund_address` and `end_user_ip_provided`. |
| `provider` | Kotova X, the provider of the service: `name` (always `Kotova X`), `icon`, `icon_png` and `website_url`. |
| `source` | The quote's source, the one every other top-level field describes and an order from this quote is placed at first (see `fallback_sources`): `slug`, `name`, `kind` (`centralized`, `decentralized` or `internal`, extensible), `icon` (`null` when its logo may not be shown), `terms_url`, `refund_address_forwarding` (what an order's refund address is used for at this source: `at_creation`, `when_needed` or `none`, extensible; see [Refund addresses](https://docs.kotova.io/x/api/orders#refund-addresses)) and `notice` (`text`, `link_text`, `link_url`, `lang`). |
| `fallback_sources` | The next best sources, up to two, best first: where the order is placed when `source` cannot take it. Each has `source` (the same block, with its `refund_address_forwarding` and notice), `from.amount`, `to.amount` and `requirements.refund_address`. An empty array when there are none. See [Fallback sources](https://docs.kotova.io/x/api/quotes#fallback-sources). |
| `warnings` | Non-fatal notes, usually empty. See below. |

Ignore response fields you do not know: new fields can be added at any time.

A quote can carry these warnings. Their `code` is an extensible list, so tolerate codes
you do not know.

| `code` | Extra fields | Meaning |
| --- | --- | --- |
| `unknown_source` | `field`, `sources` | `exclude_sources` named slugs the API does not know. They were ignored. |
| `deprecated_alias` | `field`, `given`, `canonical` | An old spelling of a currency or network was used. The quote uses `canonical`; switch to it. |
| `unsupported_lang` | `field`, `supported` | `lang` was not one of the six languages, so the notice is in English. |
| `end_user_ip_missing` | `sources` | These sources need `end_user_ip` and were skipped because it was not sent. Sending it may give a better rate. |
| `sources_temporarily_skipped` | `sources` | These sources could not be asked right now: a source is paused, or your account used up its share of that source's quotes for this minute. A later quote may find a better rate. |

**Endpoint:** `POST https://api.kotova.io/x/v1/quote` (Get a quote)

- Authentication: API key, sent as `Authorization: Bearer $KOTOVA_API_KEY`
- Weight: 1
- Request body: `application/json` (required)

Example request:

```sh
curl -X POST 'https://api.kotova.io/x/v1/quote' \
  -H "Authorization: Bearer $KOTOVA_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
  "from": {
    "currency": "BTC",
    "network": "btc"
  },
  "to": {
    "currency": "USDT",
    "network": "trx"
  },
  "amount": "0.05",
  "amount_type": "from",
  "rate_type": "variable",
  "end_user_ip": "93.184.216.34",
  "lang": "en"
}'
```

Example response (`200`):

```json
{
  "quote_id": "q_5mJc2Vq9XkT3nR8bYw1LdP",
  "created_at": "2026-11-10T10:15:04Z",
  "expires_at": "2026-11-10T10:17:03Z",
  "rate_type": "variable",
  "amount_type": "from",
  "from": {
    "currency": "BTC",
    "network": "btc",
    "amount": "0.05"
  },
  "to": {
    "currency": "USDT",
    "network": "trx",
    "amount": "3389.8"
  },
  "rate": "67796",
  "limits": {
    "min_from_amount": "0.00029412",
    "max_from_amount": "0.73529411"
  },
  "estimated_duration_seconds": 1500,
  "requirements": {
    "refund_address": "optional",
    "to_memo": "not_supported",
    "refund_memo": "not_supported"
  },
  "filters": {
    "exclude_sources": [],
    "freeze_protection": false,
    "can_provide_refund_address": true,
    "end_user_ip_provided": true
  },
  "provider": {
    "name": "Kotova X",
    "icon": {
      "url": "https://cdn.kotova.io/images/brand/kotova-x/icon.svg",
      "fallback_url": "https://assets.kotova.io/images/brand/kotova-x/icon.svg"
    },
    "icon_png": {
      "url": "https://cdn.kotova.io/images/brand/kotova-x/icon-256.png",
      "fallback_url": "https://assets.kotova.io/images/brand/kotova-x/icon-256.png"
    },
    "website_url": "https://app.kotova.io"
  },
  "source": {
    "slug": "sideshift",
    "name": "SideShift",
    "kind": "centralized",
    "icon": {
      "url": "https://cdn.kotova.io/images/counterparties/cp_icon_sideshift.svg",
      "fallback_url": "https://assets.kotova.io/images/counterparties/cp_icon_sideshift.svg"
    },
    "terms_url": "https://sideshift.ai/legal",
    "notice": {
      "text": "The exchange service is provided by SideShift. Creating an order confirms your agreement with the SideShift rules.",
      "link_text": "SideShift rules",
      "link_url": "https://sideshift.ai/legal",
      "lang": "en"
    },
    "refund_address_forwarding": "at_creation"
  },
  "fallback_sources": [
    {
      "source": {
        "slug": "changenow",
        "name": "ChangeNOW",
        "kind": "centralized",
        "icon": {
          "url": "https://cdn.kotova.io/images/counterparties/cp_icon_changenow.svg",
          "fallback_url": "https://assets.kotova.io/images/counterparties/cp_icon_changenow.svg"
        },
        "terms_url": "https://changenow.io/terms-of-use",
        "notice": {
          "text": "The exchange service is provided by ChangeNOW. Creating an order confirms your agreement with the ChangeNOW rules.",
          "link_text": "ChangeNOW rules",
          "link_url": "https://changenow.io/terms-of-use",
          "lang": "en"
        },
        "refund_address_forwarding": "at_creation"
      },
      "from": {
        "amount": "0.05"
      },
      "to": {
        "amount": "3376.2"
      },
      "requirements": {
        "refund_address": "optional"
      }
    },
    {
      "source": {
        "slug": "changelly",
        "name": "Changelly",
        "kind": "centralized",
        "icon": {
          "url": "https://cdn.kotova.io/images/counterparties/cp_icon_changelly.svg",
          "fallback_url": "https://assets.kotova.io/images/counterparties/cp_icon_changelly.svg"
        },
        "terms_url": "https://changelly.com/terms-of-use",
        "notice": {
          "text": "The exchange service is provided by Changelly. Creating an order confirms your agreement with the Changelly rules.",
          "link_text": "Changelly rules",
          "link_url": "https://changelly.com/terms-of-use",
          "lang": "en"
        },
        "refund_address_forwarding": "at_creation"
      },
      "from": {
        "amount": "0.05"
      },
      "to": {
        "amount": "3371.45"
      },
      "requirements": {
        "refund_address": "optional"
      }
    }
  ],
  "warnings": []
}
```

## Fixed and variable rates

`rate_type` is required, so the choice is always explicit.

- **`fixed`**: the source fixes the rate when the order is created, for its deposit
  window. The order's `from.amount` and `to.amount` are binding as long as your user
  sends `deposit.amount` before the order's `expires_at`. A deposit of a different
  amount, or one that arrives late, can make the order
  [`action_required`](https://docs.kotova.io/x/api/order-statuses) instead.
- **`variable`**: the rate is set when the deposit arrives. The amounts in the
  quote and in the order are indicative, and the order's `to.amount_sent` says what was
  actually paid out.

Not every source offers both rate types (`rate_types` in
[`GET /sources`](https://docs.kotova.io/x/api/sources)). A source that does not offer the requested one is
skipped. A fixed-rate quote is valid for 60 seconds, a variable-rate quote for 120.

## Quoting an exact amount received

With `"amount_type": "to"`, `amount` is what your user wants to receive, and its
fractional digits are checked against the `to` network. Each source is asked what your
user must send for it, and the best quote is the one with the smallest `from.amount`.

- The quote's `to.amount` is the source's answer, rounded down, and can differ slightly
  from the amount you asked for. Show the quote's amounts, not your request.
- `limits` stay in units of `from`.
- When the order is created, a source that accepts a receive amount gets the receive
  amount; the others get the quote's `from.amount`. Either way, the order's own amounts
  are the ones that count.
- A guard that refuses an order below a minimum amount received is not available yet.

## What a quote fixes, and what it does not

A quote fixes the sources and the pair. An order from it is placed at the quote's
source or, when that source cannot take it, at one of its `fallback_sources` (unless
the order is sent with `retry_next_best_source: false`), and never at a source the
quote does not name. It is placed for the quote's currencies, networks, amount,
`amount_type` and `rate_type`, under the quote's filters. `POST /orders` refuses those
fields next to a `quote_id`.

A quote does not lock a rate, and nothing is reserved at the source. Sources price the
order again when it is created, fixed rates included. The amounts in the
[201 response of `POST /orders`](https://docs.kotova.io/x/api/orders#response) are the ones that count, and
they can differ from the quote's. Show your user the order's amounts and its source
before they send anything. If the amounts are not acceptable, your user can simply not
send the deposit: the order then expires, and nothing is owed.

## Your user's IP address

Some sources price each trade for the person making it, and need that person's IP
address to quote at all. `requires_end_user_ip` in [`GET /sources`](https://docs.kotova.io/x/api/sources) says
which. Your key lives on your server, so the API cannot see your user's address: pass it
in `end_user_ip`.

- Send your user's public address, taken from their connection to your service or from
  a header your own proxy sets. Never send your server's address.
- Private, shared (CGNAT), loopback, documentation and other special ranges are refused
  with a 400 `invalid_request`, `details.reason: not_public_unicast`. The documentation
  ranges are refused too, so test with a real public address or leave the field out.
- Without `end_user_ip`, the sources that need it are skipped. The quote then carries an
  `end_user_ip_missing` warning naming them, and `filters.end_user_ip_provided` is
  `false`. If only such sources could serve the request, the answer is 422
  `no_source_available` with the reason `end_user_ip_required`.
- The address is passed to the sources that need it, for the quote and for an order
  created from it.

## How the source is chosen

Every source that serves the pair and passes your filters is asked. Only answers inside
the answering source's own limits compete.

- With `"amount_type": "from"`, the best quote is the one with the largest `to.amount`.
- With `"amount_type": "to"`, it is the one with the smallest `from.amount`.
- Amounts are compared as exact decimals. A tie goes to the source that comes first in
  `display_order` (in `GET /sources`), then to the name in alphabetical order.
- What Kotova earns on an order never weighs.

The best source is returned as `source`, and the next ones as `fallback_sources`. The
rule is the one the Kotova X app uses: see
[Ranking parameters and weighting](https://docs.kotova.io/x/how-it-works/quoting-and-routing#ranking-parameters-and-weighting).

## Fallback sources

`fallback_sources` lists the next best sources of the same ranking after `source`: up
to two, best first. They passed the same filters and answered inside their own limits.
A source whose amounts round to zero is left out, and the list is empty when no other
source qualifies.

| Field | What it is |
| --- | --- |
| `source` | The fallback source: the same block as the quote's `source`, with its `refund_address_forwarding` and its notice. |
| `from.amount` | What your user sends if this source takes the order, rounded up. |
| `to.amount` | What your user receives if this source takes the order, rounded down. |
| `requirements.refund_address` | `required`: this source is tried only when the order sends a `refund_address`, and skipped without one. `optional`: it is tried either way. Sending a refund address keeps every fallback source eligible. |

By default, `POST /orders` tries these sources in this order when the quote's source
cannot take the order, and answers with the order of the first one that takes it. That
order's source and amounts are the fallback source's, not the quote's. Show the
notices of the fallback sources before your user confirms as well, by the same rules
as the quote's notice; they may follow it, also in an expandable section directly next
to it. Or send `retry_next_best_source: false` with the order to use the quote's
source only. See
[Next best source](https://docs.kotova.io/x/api/orders#next-best-source) and
[Display requirements](https://docs.kotova.io/x/api/display-requirements#fallback-sources).

The top-level fields of the quote, such as `rate`, `limits`, `requirements` and
`estimated_duration_seconds`, describe the quote's source only.

## Rounding

Amounts are decimal strings with no exponent and no trailing zeros, and at most
min(network decimals, 12) fractional digits.

- What your user sends is rounded up: `from.amount` and `limits.min_from_amount`.
- What your user receives is rounded down: `to.amount`. `limits.max_from_amount` is
  rounded down too.
- `rate` is computed from the rounded amounts and truncated to 12 significant digits.
  Show the amounts themselves rather than recomputing them from `rate`.

## Freshness and expiry

A quote is valid until its `expires_at`: 60 seconds for a fixed rate and 120 seconds
for a variable one, counted from the moment the source answered. A quote served from
the cache was answered earlier, so it has less time left. Always read `expires_at`
rather than assuming the full period.

A quote is single use. The first `POST /orders` with its `quote_id` claims it, and every
later one answers 409 [`quote_already_used`](https://docs.kotova.io/x/api/errors#quote_already_used), with the
order's id in `details.order_id` once the order exists.

- After `expires_at`, `POST /orders` answers 410
  [`quote_expired`](https://docs.kotova.io/x/api/errors#quote_expired). Shortly afterwards the quote is
  deleted, and its id then answers 404
  [`quote_not_found`](https://docs.kotova.io/x/api/errors#quote_not_found), as does a quote of another
  account.
- When an order could not be created and nothing exists at any source (a validation
  error, an address error, an order cap, sources that refused the order), the quote is
  released and can be used again until it expires.
- When a source may have created the order without confirming it, the quote stays
  used. See [When no source creates the order](https://docs.kotova.io/x/api/orders#when-no-source-creates-the-order).

If your user takes longer than the quote's validity to decide, request a new quote
just before they confirm, and show its amounts and its source notices.

### Caching and the fan-out allowance

The API shares the Kotova X app's quote cache. Two requests for the same currencies and
networks, the exact same amount, the same `amount_type` and the same `rate_type` within
the cache lifetime (30 seconds for a fixed rate, 60 seconds for a variable one) are
answered from one round of source answers. The response header `X-Kotova-Quote-Cache`
is `hit` when the cache answered and `miss` when the sources were asked for this
request. Some sources quote per end user and are not served from the cache.

A quote that has to ask a source live counts against your account's fan-out allowance
as well as its budget: max(30, limit / 8) such quotes per minute, per account, where
*limit* is your account's rate limit. That is a cache miss, and also a cached quote
that still asks a source live, such as a source that prices per end user when you send
`end_user_ip`. A round in which every source that would be asked live is over your
account's share of that source's quotes for the minute is skipped and not charged. Past it, the answer is 429
[`rate_limited`](https://docs.kotova.io/x/api/errors#rate_limited) with `details.reason: fanout` and a
`Retry-After` header. A quote refused this way costs nothing. Quote the amounts your
user actually chose: varying an amount slightly on every request misses the cache each
time. See [Rate limits](https://docs.kotova.io/x/api/rate-limits).

## When no source can quote

The request is checked in this order, and the first problem found is the answer.

1. **The request itself.** A malformed body is a 400
   [`invalid_request`](https://docs.kotova.io/x/api/errors#invalid_request), with `details.field` and
   `details.reason`.
2. **The currencies.** A currency and network that is not in the catalogue is a 422
   [`unknown_currency`](https://docs.kotova.io/x/api/errors#unknown_currency); one that cannot be used in that
   direction right now is a 422
   [`currency_unavailable`](https://docs.kotova.io/x/api/errors#currency_unavailable). Both name the side in
   `details.side`. Lightning is not available yet as a receive network. The same
   currency and network on both sides is a 422
   [`same_currency`](https://docs.kotova.io/x/api/errors#same_currency). Only then is the number of decimal
   places in `amount` checked against its network (400, `too_many_decimals`).
3. **The pair.** When no source offers the pair at all, the answer is a 422
   [`pair_unavailable`](https://docs.kotova.io/x/api/errors#pair_unavailable), before any source is asked.
4. **The sources.** Every eligible source is asked. A source that times out, fails, is
   rate limited, paused or over its budget is skipped, and a request never fails while
   another source answers. When no source gives a usable quote, the answer is the first
   of these that applies:
   1. [`amount_below_min`](https://docs.kotova.io/x/api/errors#amount_below_min) (422): the amount is below
      the minimum of the sources that could otherwise serve it. This is also the answer
      when the best quote rounds to zero at the currency's precision.
   2. [`amount_above_max`](https://docs.kotova.io/x/api/errors#amount_above_max) (422): the amount is above
      their maximum.
   3. [`sources_unavailable`](https://docs.kotova.io/x/api/errors#sources_unavailable) (503): sources could not
      be reached right now. Retry after the `Retry-After` header's 10 seconds
      (`details.retry_after_seconds`).
   4. [`pair_unavailable`](https://docs.kotova.io/x/api/errors#pair_unavailable) (422): every source said it
      does not serve the pair.
   5. [`no_source_available`](https://docs.kotova.io/x/api/errors#no_source_available) (422): no source is
      left for this request.

`amount_below_min` and `amount_above_max` carry the bounds in units of `from`, so you
can show your user what is possible:

```json
{
  "error": {
    "code": "amount_below_min",
    "message": "The amount is below the minimum of every available source.",
    "request_id": "req_8fK2mQ9xLp3vTz7nWc1bYd4h",
    "details": {
      "min_from_amount": "0.00029412",
      "max_from_amount": "0.73529411",
      "currency": "BTC",
      "network": "btc"
    },
    "doc_url": "https://docs.kotova.io/x/api/errors#amount_below_min"
  }
}
```

`min_from_amount` is the smallest minimum, rounded up. `max_from_amount` is the largest
maximum, rounded down, and `null` when a source has no maximum.

`no_source_available` lists, in `details.reasons`, the sources your request's filters
left out, grouped by reason. The list can be empty when the remaining sources declined
for reasons of their own.

| `reason` | The listed sources were left out because |
| --- | --- |
| `excluded_by_request` | they are in `exclude_sources`. |
| `freeze_protection` | `freeze_protection` is `true` and they can freeze funds. |
| `rate_type_unsupported` | they do not offer the requested `rate_type`. |
| `end_user_ip_required` | they need `end_user_ip`, and the request had none. |
| `refund_address_required` | they need a refund address, and `can_provide_refund_address` is `false`. |
| `create_budget_exhausted` | they cannot take more orders through the API at the moment. |

A quote can also be refused by your account's limits: 429
[`rate_limited`](https://docs.kotova.io/x/api/errors#rate_limited), for the per-account budget or the fan-out
allowance above. A 503 [`service_unavailable`](https://docs.kotova.io/x/api/errors#service_unavailable) means
the API itself cannot answer right now. Every error, with what to do about it, is on
the [Errors](https://docs.kotova.io/x/api/errors) page. When you write to api@kotova.io about a quote, include
the response's `X-Request-Id`.
