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

Errors

Открыть как Markdown

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

Every error response has the same shape and carries a stable code. Branch on the code, not on the message: the code is part of the contract, the wording of the message is not.

Every error the API answers (HTTP status 400 or above) has this body:

{
"error": {
"code": "quote_expired",
"message": "This quote has expired. Request a new quote.",
"request_id": "req_8fK2mQ9xLp3vTz7nWc1bYd4h",
"details": {
"quote_id": "q_5mJc2Vq9XkT3nR8bYw1LdP",
"expires_at": "2026-11-10T10:17:03Z"
},
"doc_url": "https://docs.kotova.io/x/api/errors#quote_expired"
}
}
Field What it holds
code The error code, one of those listed on this page. Stable within v1.
message An explanation for people. It may change at any time.
request_id The request’s id, also sent as the X-Request-Id header. Quote it to support.
details Machine-readable specifics, for the codes that have any. Absent otherwise.
doc_url The section of this page that documents the code.

New codes may be added within v1. When you meet a code you do not know, act on the HTTP status. A successful (2xx) answer can carry non-fatal notes instead, in warnings; see Warnings.

Status Codes
400 invalid_request
401 unauthorized, key_revoked
403 affiliate_inactive
404 not_found, order_not_found, quote_not_found
405 method_not_allowed
409 quote_already_used, idempotency_conflict, idempotency_in_progress
410 quote_expired
413 payload_too_large
415 unsupported_media_type
422 unknown_currency, currency_unavailable, same_currency, pair_unavailable, no_source_available, amount_below_min, amount_above_max, refund_address_required, invalid_address, memo_not_supported
429 rate_limited, order_cap_reached
500 internal_error
502 source_error
503 source_unavailable, sources_unavailable, service_unavailable

Most 4xx errors mean the request itself has to change: sending it again unchanged gives the same answer. Retry only these, and only as described:

Code Retry
rate_limited, order_cap_reached After the seconds in Retry-After.
idempotency_in_progress After Retry-After (2 seconds), with the same Idempotency-Key and body.
sources_unavailable After Retry-After (10 seconds).
service_unavailable After Retry-After when it is sent, otherwise after a growing pause.
source_unavailable Request a new quote, then create the order from the new quote_id.
source_error with details.retryable: true Request a new quote, then create the order from it.
internal_error See internal_error.

POST /orders is the one request where a retry could do harm, by creating a second order. Send an Idempotency-Key header with every order request (8 to 64 characters of A-Z a-z 0-9 _ - : .; a UUID fits) and keep it with your own order. After a timeout, a dropped connection or a 500 answer, send the request again with the same key and the same body:

  • if the first request created the order, the retry answers 201 with that order and Idempotent-Replayed: true, and costs 1 unit;
  • if the first request is still running, the retry answers 409 idempotency_in_progress;
  • if the first request failed without creating anything, the retry runs as a new request.

Keys are kept for 24 hours, per account. Use a new key for every new order, and for an order you send with a new quote_id after requesting a new quote: its body is no longer the same.

HTTP 400. The request is malformed. details.field names the problem: a body field as a dot path (from.currency), a query parameter, or a header such as Idempotency-Key. details.reason says what is wrong:

details.reason Meaning
required The field is missing, or the body is empty.
invalid_type The value has the wrong JSON type, or the body is not a JSON object.
invalid_format The value does not have the expected form, such as a quote id, an order id or an Idempotency-Key.
invalid_value The value is not one of those allowed; details.allowed lists them when the list is short.
unknown_field The body has a field the endpoint does not take. Check the spelling.
unknown_parameter A query parameter the endpoint does not take, or an empty query segment such as ?& on a keyless endpoint.
duplicate_parameter A query parameter is given more than once.
too_long An address or memo is longer than allowed.
too_many exclude_sources lists more than 20 sources.
too_many_decimals The amount has more decimal places than the currency allows on that network; details.max_fraction_digits gives the limit.
too_many_integer_digits The amount has too many digits before the decimal point.
not_positive The amount is zero or negative.
not_public_unicast end_user_ip is not a public IPv4 or IPv6 address. Private, loopback, CGNAT, documentation and other special ranges are refused.
from_or_to_required GET /pairs was called without from and from_network, or to and to_network.
not_allowed_with_quote_id POST /orders was sent a field that belongs to the quote, such as amount. Send quote_id with the recipient fields only.
requires_refund_address refund_memo was sent without refund_address.
invalid_json The body is not valid JSON.
invalid_encoding The body is not UTF-8.

Fix the request before you send it again. An unknown or repeated query parameter is refused before your key is checked and costs nothing; on POST /orders, a 400 costs 1 unit.

HTTP 401. The request to a keyed endpoint has no Authorization header, a value that is not a well-formed Bearer ktv_sk_… key, or a key that does not exist. The response carries WWW-Authenticate: Bearer realm="kotova-x". Check that your server sends the secret key, not the key ID (ktv_pk_…), and that it has not been cut short. See Authentication.

HTTP 401. The key was regenerated or deleted, or revoked by Kotova. Put the current secret key on your servers, or create a new key on the account API page. The response carries WWW-Authenticate: Bearer realm="kotova-x".

HTTP 403. The key exists, but the affiliate profile it belongs to is not active: it was deactivated or has expired, or the account that created the key no longer exists or no longer owns the profile. The account API page says that API access is paused. Write to api@kotova.io with your key ID.

HTTP 404. There is no endpoint at this path. Check that the path starts with /x/v1 and that you call api.kotova.io. The rates feed also answers not_found for a file other than variable.xml and fixed.xml, and while the feed is switched off.

HTTP 404. GET /orders/{id} found no order with this id for your account. details.id repeats the id. Only orders created through the API with one of your account’s keys, current or earlier, are visible.

HTTP 404. POST /orders found no quote with this quote_id for your account: the id is wrong, the quote belongs to another account, or it is no longer stored because it expired a while ago. details.quote_id repeats the id. Request a new quote.

HTTP 405. The endpoint does not support this HTTP method. The Allow header and details.allowed list the methods it does support. HEAD works on the keyless GET endpoints only.

HTTP 409. A quote can be used for one order, and this one was already used. details.quote_id is the quote and details.order_id the order created from it, or null while that order’s outcome is unknown. If you did not mean to create a second order, read the one in details.order_id. For a new order, request a new quote.

HTTP 409. This Idempotency-Key was already used, within the last 24 hours, with a different request body. details.header is Idempotency-Key. A retry must repeat the original body exactly; a new order needs a new key.

HTTP 409. A request with this Idempotency-Key is still being processed. Wait for Retry-After (2 seconds) and send the same request again with the same key.

HTTP 410. The quote’s expires_at has passed: 60 seconds after the source answered for a fixed quote, 120 seconds for a variable one. details.quote_id and details.expires_at identify it. Request a new quote and show it to your user again, with its source notices, since the amounts or the sources may have changed.

HTTP 413. The request body is larger than 16 KB. details.max_bytes gives the limit (16384).

HTTP 415. A POST body must be JSON, sent with Content-Type: application/json (with charset=utf-8 or no charset).

HTTP 422. This currency on this network is not in the catalogue. details.currency and details.network repeat what you sent, and details.side (from or to) says which side of a quote or pair it was; it is absent on POST /addresses/validate. Check the codes against GET /currencies.

HTTP 422. The currency exists on this network, but cannot be used in this direction right now: it cannot be sent (details.side: "from") or received (details.side: "to"). details.currency and details.network name it. The send and receive flags in GET /currencies show what is usable.

HTTP 422. The currency and network to send and to receive are the same. details.currency and details.network name them.

HTTP 422. No source offers this pair right now. details.from and details.to name the two sides. GET /pairs shows what a currency can be swapped into.

HTTP 422. Sources serve this pair, but none is left that could quote this request, usually because your request’s filters excluded them. details.reasons lists each reason with the sources it excluded:

"details": {
"reasons": [
{ "reason": "freeze_protection", "sources": ["sideshift", "changenow", "ffio"] },
{ "reason": "rate_type_unsupported", "sources": ["chainflip"] }
]
}
reason What excluded the sources
excluded_by_request You listed them in exclude_sources.
freeze_protection You asked for freeze_protection, and they can freeze funds.
rate_type_unsupported They do not offer the rate_type you asked for.
end_user_ip_required They need end_user_ip, and the request had none.
refund_address_required They need a refund address when the order is created, and you sent can_provide_refund_address: false.
create_budget_exhausted They cannot take more orders through the API for the moment.

New reasons may be added. Relax the filter that excluded the most sources, send end_user_ip, or try the other rate type.

The list can be empty when the remaining sources declined the request for reasons of their own. Then try another amount, or ask again later.

HTTP 422. The amount is below the minimum of every source that could quote it. details.min_from_amount is the smallest minimum, details.max_from_amount the largest maximum (null when there is none), and details.currency and details.network the currency they are counted in. The bounds are always in the currency your user sends, even for a quote with amount_type: "to". Ask your user for a larger amount.

HTTP 422. The amount is above the maximum of every source that could quote it. details has the same fields as for amount_below_min. Ask your user for a smaller amount, or split the swap.

HTTP 422. The source of this quote needs a refund address when the order is created, and POST /orders had none. The quote announced it with requirements.refund_address: "required". details.field is refund_address, details.network the network the refund goes to (the one your user sends from) and details.source the source. Send the order again with a refund_address on that network.

HTTP 422. An address or a memo in POST /orders was refused. details.field says which one, and details.reason why:

  • format: the address in details.field (to_address or refund_address) does not match the address format of the network in details.network. A recipient address belongs to the network your user receives on, a refund address to the network they send from. POST /addresses/validate runs the same check before you create the order.
  • rejected_by_source: the source in details.source refused what details.field names: to_address, to_memo, refund_address or refund_memo. This also counts when the source says so only in its message: it names the field, or says the address or memo is invalid, missing or similar. A message that only mentions an address does not count. Nothing was created, and the order is not tried at another source, whether or not retry_next_best_source is on. When the source did not say which value it refused, details.field is the best guess and details.fields lists every candidate: check them all. When the refusing source was one of the quote’s fallback_sources, details.sources_tried lists every source tried, ending with the one that refused (see Sources tried).
"details": {
"field": "to_address",
"fields": ["to_address", "refund_address"],
"reason": "rejected_by_source",
"source": "changenow"
}

Ask your user for a correct address or memo, then create the order again; while the quote has not expired, you can use the same quote_id.

HTTP 422. A memo was sent for a network that has no memos. details.field is the memo field (to_memo or refund_memo) and details.network the network. Leave the memo out. GET /currencies shows, per network, whether a memo is supported.

HTTP 429. A request limit was reached. Retry-After gives the seconds to wait, and a refused request costs nothing. details.reason says which limit:

details.reason Limit Other details
absent Your account’s budget of weight units per 60-second window. limit, remaining, reset_seconds, weight
fanout Quotes per minute that asked a source live, at least 30. limit, remaining, reset_seconds
ip Calls to keyless endpoints that reach the server: 120 per minute per IP address. limit, remaining, reset_seconds
auth_failures Failed authentications: 30 per minute per IP address. limit, remaining, reset_seconds

See Rate limits.

HTTP 429. Your account has reached one of its order caps. details.cap says which: open_unfunded (orders still waiting for a deposit) or created_24h (orders created in the last 24 hours). details.limit is the cap and details.current your count. Wait for Retry-After; open orders stop counting once they are funded or expire. See Order caps.

HTTP 500. Something failed on Kotova’s side. If details.order_id is present, POST /orders created that order but could not read it back: read it with GET /orders/{id}, or send the same request again with the same Idempotency-Key. If details.sources_tried is present, POST /orders failed on Kotova’s side while creating the order at one of the quote’s fallback_sources: nothing was created, and the quote and the Idempotency-Key are released (see Sources tried). Otherwise retry later, and on POST /orders only with the same Idempotency-Key. If it persists, write to api@kotova.io with the request id.

HTTP 502. No source created the order. details.source names the last source asked, and details.retryable says what to do:

  • true: nothing was created. Request a new quote and create the order from it.
  • false without details.reason: the source refused the order. Do not send it again unchanged; request a new quote before you try again.
  • false with details.reason: "outcome_unknown": the source did not confirm the order, and it may exist there. To prevent a duplicate, the quote and the Idempotency-Key stay used. Write to api@kotova.io with the request id before you create another order for the same swap.
  • false with details.reason: "idempotency_outcome_unknown": an earlier request with this Idempotency-Key had an unknown outcome, so the key stays blocked for 24 hours. details.source is absent. Write to api@kotova.io with the request id.

With retry_next_best_source (the default), a source_error from a create attempt also carries details.sources_tried, which lists every source the order was tried at: see Sources tried. The idempotency_outcome_unknown answer does not.

By default, POST /orders tries the quote’s source and then the quote’s fallback_sources (see Next best source). When none of them takes the order, the 502 source_error or 503 source_unavailable carries details.sources_tried: one entry per source, in the order they were tried, with source (the slug) and reason. So does a 422 invalid_address or a 500 internal_error from one of the quote’s fallback_sources; the same answers from the quote’s own source have no sources_tried. The 502 with details.reason: idempotency_outcome_unknown, which answers a retry with a blocked Idempotency-Key, has none either.

reason What happened
disabled, paused, busy The source was not available, and was not asked.
rejected The source was asked and refused the order. Nothing was created.
unavailable The source was asked and could not create the order right now. Nothing was created.
rate_limited The source was asked and answered that it is rate limited. Nothing was created.
invalid_address The source was asked and refused an address or a memo. No other source is asked: always the last entry, with a 422 invalid_address.
internal The source was asked, and Kotova’s side failed before anything was created. Always the last entry, with a 500 internal_error.
outcome_unknown The source was asked and did not confirm the order, which may exist there. Always the last entry.
refund_address_required A fallback source that needs a refund address was skipped, because the order had none.
deadline A fallback source was skipped, because the request had already run too long to ask another source, or its create could not have finished in time.
"details": {
"source": "changelly",
"retryable": true,
"sources_tried": [
{ "source": "sideshift", "reason": "unavailable" },
{ "source": "changenow", "reason": "busy" },
{ "source": "changelly", "reason": "unavailable" }
]
}

The other details describe the last source asked. New reasons may be added. Without retry_next_best_source, only the quote’s source is tried, and sources_tried is absent.

HTTP 503. No source of your quote could take an order right now: the quote’s source and, with retry_next_best_source (the default), its fallback sources were not available, or the last one asked was rate limited. Nothing was created. details.source names the last source asked, or the quote’s source when none was asked; details.reason says why (disabled, paused, busy or rate_limited), and details.requote is true. With retry_next_best_source, details.sources_tried lists every source (see Sources tried). Request a new quote, which may come from other sources, and create the order from it. Retry-After is sent as well.

HTTP 503. POST /quote got no usable quote from any source, and at least one source could not be asked right now, for a temporary reason such as a timeout, an error, its own rate limit or a regional block. Other sources may have answered, for example that they do not serve the pair. details.retry_after_seconds and the Retry-After header give the wait (10 seconds). Then ask for the quote again.

HTTP 503. The API cannot serve the request right now. details.reason says why:

details.reason Meaning
disabled The API is not enabled where the request was sent: it is switched off, or the host does not serve it. Use https://api.kotova.io/x/v1.
maintenance The API is under maintenance. Retry later.
catalog_missing The currency catalogue is temporarily unavailable.
storage_unavailable A storage service the API needs is temporarily unavailable. Retry shortly.

New reasons may be added. Wait for Retry-After when it is sent, otherwise retry after a growing pause. GET /health also answers 503 while the API cannot serve requests.

A successful answer may carry non-fatal notes in warnings. Each has a code, a message for people, and fields of its own:

"warnings": [
{
"code": "end_user_ip_missing",
"message": "Sources that quote per end user were skipped because end_user_ip was not sent; sending it may improve the rate.",
"sources": ["sideshift"]
}
]
code 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 code was used. Switch to canonical.
unsupported_lang field, supported lang is not one of the supported languages, so the source notice is in English.
end_user_ip_missing sources These sources need end_user_ip and were skipped. Sending it may give a better quote.
sources_temporarily_skipped sources These sources could not be asked right now. A later quote may be better.
source_fallback quoted_source, reason POST /orders only. The quote’s source could not take the order, so it was placed at the next of the quote’s fallback_sources that could. The order’s source names that source, and its amounts are that source’s. reason is why the quote’s source did not take it: disabled, paused or busy (not available), or rejected, unavailable or rate_limited (asked, nothing created). It comes with the 201 and with its Idempotency-Key replays, never with GET /orders/{id}: there, compare the order’s source.slug with the quote’s.
refund_address_on_file source, intake POST /orders only. The order’s source takes no refund address when the order is created, so the refund_address is kept with the order and refund.forwarded is false. intake is the source’s refund_address_forwarding: when_needed (given to the source only if the order needs a refund) or none (kept for support; the source handles refunds itself). It comes with the 201 and with its Idempotency-Key replays, never with GET /orders/{id}. See Refund addresses.

New warning codes may be added: ignore those you do not know.

Write to api@kotova.io. Include the request_id from the error body (or the X-Request-Id header of any response) and, for an order, its id (X-XXXXXX). Never send your secret key: support never needs it.