Перейти к содержимому
KOTOVADOCS
Запустить App

Quotes

Открыть как Markdown

Это содержимое пока не доступно на вашем языке.

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.

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 to create the order at that source, or at one of the quote’s fallback sources when that source cannot take it.

Terminal window
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"
}'

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).

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 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.
rate_type Yes fixed or variable. There is no default. See Fixed and variable rates.
exclude_sources No Source slugs not to use, from GET /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.
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.

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.

{
"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) and the rate limit headers described in 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.
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). 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) 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.
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.
Try itPOST/quoteAPI key · weight 1
Connection

Your secret key stays in this page and is sent only to the base URL above. “Remember in this tab” keeps it in this tab’s session storage until the tab is closed; it is never stored anywhere else. If you are unsure, delete the key on your account page after testing and create a new one.

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 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). 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.

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.

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 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.

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 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.

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.

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 and Display requirements.

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

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.

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, with the order’s id in details.order_id once the order exists.

  • After expires_at, POST /orders answers 410 quote_expired. Shortly afterwards the quote is deleted, and its id then answers 404 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.

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.

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 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.

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, with details.field and details.reason.
  2. The currencies. A currency and network that is not in the catalogue is a 422 unknown_currency; one that cannot be used in that direction right now is a 422 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. 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, 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 (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 (422): the amount is above their maximum.
    3. 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 (422): every source said it does not serve the pair.
    5. 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:

{
"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, for the per-account budget or the fan-out allowance above. A 503 service_unavailable means the API itself cannot answer right now. Every error, with what to do about it, is on the Errors page. When you write to api@kotova.io about a quote, include the response’s X-Request-Id.