An order is created from a quote and is placed at a source that quote named: its own source or, when that source cannot take the order, one of its fallback sources. Your user then sends the deposit to an address that belongs to the order’s source, and you read the order to follow it until it is final.
Creating an order
Section titled “Creating an order”POST /orders creates the order at the source of a quote and answers
201 with the order. When that source cannot take the order, it is placed at the next
best source of the quote instead (see Next best source). It needs
your API key and has a weight of 50. The body carries the quote_id and where the
funds go; the pair, the amount and the sources all come from the quote. Before you
call it, your user must have seen Kotova X as the provider and the notices of the
quote’s source and fallback_sources: see
Display requirements.
Send an Idempotency-Key header with every request, and give the request a generous
timeout: the order is created at the source while you wait, so the answer can take
several seconds, and up to 90 seconds when other sources are tried.
A sandbox is not available yet. Every order created through the API, including one created from a console on this page, is a real order at a source.
curl https://api.kotova.io/x/v1/orders \ -H "Authorization: Bearer $KOTOVA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 3f1c9a52-7d0e-4b8a-9c61-2e5f4a7b8d90" \ -d '{ "quote_id": "q_5mJc2Vq9XkT3nR8bYw1LdP", "to_address": "REPLACE_WITH_RECIPIENT_TRX_ADDRESS", "refund_address": "REPLACE_WITH_REFUND_BTC_ADDRESS", "partner_order_id": "shop-order-1042", "lang": "en" }'import { randomUUID } from "node:crypto";
// One key per order attempt. Store it, and send it again if you retry this request.const idempotencyKey = randomUUID();
const response = await fetch("https://api.kotova.io/x/v1/orders", { method: "POST", headers: { Authorization: `Bearer ${process.env.KOTOVA_API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": idempotencyKey, }, body: JSON.stringify({ quote_id: quote.quote_id, to_address: "REPLACE_WITH_RECIPIENT_TRX_ADDRESS", refund_address: "REPLACE_WITH_REFUND_BTC_ADDRESS", partner_order_id: "shop-order-1042", lang: "en", }), signal: AbortSignal.timeout(100_000),});const order = await response.json();if (!response.ok) throw new Error(`${order.error.code}: ${order.error.message}`);// Show order.deposit.address, order.deposit.amount and, when set, order.deposit.memo.import osimport uuidimport requests
# One key per order attempt. Store it, and send it again if you retry this request.idempotency_key = str(uuid.uuid4())
response = requests.post( "https://api.kotova.io/x/v1/orders", headers={ "Authorization": "Bearer " + os.environ["KOTOVA_API_KEY"], "Idempotency-Key": idempotency_key, }, json={ "quote_id": quote["quote_id"], "to_address": "REPLACE_WITH_RECIPIENT_TRX_ADDRESS", "refund_address": "REPLACE_WITH_REFUND_BTC_ADDRESS", "partner_order_id": "shop-order-1042", "lang": "en", }, timeout=100,)order = response.json()if not response.ok: raise RuntimeError(f"{order['error']['code']}: {order['error']['message']}")# Show order["deposit"]["address"], order["deposit"]["amount"] and, when set, order["deposit"]["memo"].Request
Section titled “Request”The body is JSON (Content-Type: application/json). The endpoint takes no query
parameters.
| Field | Required | Rules |
|---|---|---|
quote_id |
Yes | The quote_id of a quote from POST /quote, used before its expires_at. Single use. |
to_address |
Yes | Where your user receives the funds, on the quote’s to network. |
to_memo |
No | The memo or destination tag of the recipient. Only on networks that support one (the quote’s requirements.to_memo is optional). |
refund_address |
Recommended; required when the quote says so | Where a refund goes, on the quote’s from network. Required when the quote’s requirements.refund_address is required, and recommended for every order: where the source takes it with the order, it goes to the source then, so a refund needs no extra step, and the quote’s fallback sources that need one stay eligible. See Refund addresses. |
refund_memo |
No | The memo or destination tag of the refund address, when that address needs one. Needs a refund_address. |
partner_order_id |
No | Your own reference for the order: 1 to 64 characters of A–Z, a–z, 0–9, ., _, : and -. It is returned on the order, and you can filter your order list by it. It does not have to be unique. |
lang |
No | The language of the source notice in the response: en (the default), de, it, es, fr or ru. Any other value falls back to en with an unsupported_lang warning. |
retry_next_best_source |
No | true (the default): when the quote’s source cannot take the order, it is placed at the next of the quote’s fallback_sources that can. false: only the quote’s source is tried. A JSON boolean: omit the field for the default. null or any other value is a 400 invalid_request with details.field: retry_next_best_source and details.reason: invalid_type. See Next best source. |
The quote fixes the pair, the amount, the rate type and the source filters. Sending any
of from, to, amount, amount_type, rate_type, exclude_sources,
freeze_protection, can_provide_refund_address or end_user_ip is a 400
invalid_request with details.reason: not_allowed_with_quote_id (or, without a
quote_id, a 400 saying that quote_id is required). Any other field the API does not
know is a 400 with details.reason: unknown_field.
Response
Section titled “Response”An order that was created answers 201, with a Location header (/x/v1/orders/X-7KQ2ZD)
and the order object. GET /orders/{id} returns the same object.
Shortened here: provider and source carry the full blocks described below.
{ "id": "X-7KQ2ZD", "status": "waiting_for_deposit", "is_final": false, "status_detail": null, "rate_type": "variable", "created_at": "2026-11-10T10:15:39Z", "updated_at": "2026-11-10T10:15:39Z", "expires_at": "2026-11-10T11:15:39Z", "completed_at": null, "from": { "currency": "BTC", "network": "btc", "amount": "0.05", "amount_received": null }, "to": { "currency": "USDT", "network": "trx", "amount": "3389.8", "amount_sent": null, "address": "REPLACE_WITH_RECIPIENT_TRX_ADDRESS", "memo": null }, "deposit": { "address": "bc1q9h7garjv2gx0s4kd3mzt6v8wcl5ul3xzq0p2ne", "memo": null, "memo_label": null, "amount": "0.05" }, "refund": { "address": "REPLACE_WITH_REFUND_BTC_ADDRESS", "memo": null, "forwarded": true }, "transactions": { "deposit": null, "payout": null, "refund": null }, "order_url": "https://app.kotova.io/x/X-7KQ2ZD", "estimated_duration_seconds": 1500, "quote_id": "q_5mJc2Vq9XkT3nR8bYw1LdP", "partner_order_id": "shop-order-1042", "provider": { "name": "Kotova X", "website_url": "https://app.kotova.io" }, "source": { "slug": "sideshift", "name": "SideShift", "order_id": "7f3c1e9a2b5d4c8e", "tracking_url": "https://sideshift.ai/orders/7f3c1e9a2b5d4c8e" }, "warnings": []}The order object, field by field. Amounts follow the rounding rules of quotes: what your user sends is rounded up, what they receive is rounded down. Ignore fields you do not know: new ones can be added at any time.
| Field | What it is |
|---|---|
id |
The public order id: X- followed by 6 characters of A–Z and 0–9. Quote it to support. |
status |
One of nine statuses. See Order statuses. |
is_final |
true once nothing will change any more. Read the order until it is true. |
status_detail |
Why an order needs attention: set for action_required, and for refunding when the reason is known. Otherwise null. See Status detail. |
rate_type |
fixed or variable. |
created_at, updated_at |
When the order was created and when it last changed. |
expires_at |
The deposit deadline, the one the order page counts down. null when unknown. See Expiry and late deposits. |
completed_at |
Set only once the status is completed. |
from.currency, from.network |
What your user sends. |
from.amount |
The order amount: binding for a fixed rate, indicative for a variable one. |
from.amount_received |
The deposit as the source saw it, once there is one. |
to.currency, to.network |
What your user receives. |
to.amount |
The amount to receive: binding for a fixed rate, indicative for a variable one. |
to.amount_sent |
The payout as sent, once there is one. |
to.address, to.memo |
Where the payout goes, as you sent them. |
deposit.address |
Where your user sends the deposit: an address of the source. |
deposit.amount |
How much to send. |
deposit.memo |
When set, your user must send it with the deposit. Show it prominently. |
deposit.memo_label |
What to call that memo, such as Memo or Destination Tag. Set together with deposit.memo. |
refund.address, refund.memo |
Where a refund goes, on the from network. null when there is none, and after the source refused the address (status_detail.reason: refund_address_rejected). |
refund.forwarded |
true when the refund address went to the source with the order. false when there is none, or when it is kept with the order only. See Refund addresses. |
transactions.deposit |
null until the deposit is seen. Then hash, explorer_url, confirmations and required_confirmations: how many confirmations the source needs before it treats the deposit as final, null when unknown. |
transactions.payout |
null until the payout is sent. Then hash, explorer_url and confirmations. |
transactions.refund |
The same for a refund. |
order_url |
The order page for your user; optional to show. See The order page. |
estimated_duration_seconds |
A typical time from deposit to payout, or null when there is no estimate. |
quote_id |
The quote the order was created from. |
partner_order_id |
Your reference, as you sent it, or null. |
provider |
Kotova X, the provider of the service: name, icon, icon_png and website_url. |
source |
The source that executes the order: slug, name, kind, icon, terms_url and notice, as in a quote, plus order_id (the source’s own id for the order, for support with the source) and tracking_url (the source’s tracking page for the order, null when there is none Kotova can link to). After a fallback, it is not the quote’s source. |
warnings |
Non-fatal notes, usually empty. source_fallback when the order was placed at one of the quote’s fallback sources, and refund_address_on_file when the refund address is kept with the order only: in the 201 and its Idempotency-Key replays, not from GET /orders/{id}. |
In a transaction, explorer_url is null when no block explorer is known for the
network, and confirmations is null when the count is unknown.
Next best source
Section titled “Next best source”A source can be unable to take an order at the moment it is created: it is
unavailable, it refuses the order, or it cannot create it right now. By default
(retry_next_best_source omitted or true), the order is then placed at the first of
the quote’s fallback_sources that can take it, in
their order, and the answer is that order alone.
- What is tried. The quote’s source first, then the quote’s
fallback_sourcesin order: at most three sources, all named in the quote. A fallback source whoserequirements.refund_addressisrequiredis skipped when the order has norefund_address. No further source is asked once the request has run for 40 seconds, and a fallback source is skipped earlier when its create could not finish in time; both aredeadlineindetails.sources_tried. - What is never retried elsewhere. An address or memo a source refuses (422
invalid_addresswithdetails.reason: rejected_by_source), also when the source says so only in its message (it names the field, or says the address or memo is invalid, missing or similar), and an unknown outcome (502source_errorwithdetails.reason: outcome_unknown) end the request at that source: the address is the problem, or the order may exist there. A 422invalid_addressfrom one of the quote’s fallback sources carriesdetails.sources_tried, as does a 500internal_errorfrom one of them; a 502outcome_unknowncarries it wheneverretry_next_best_sourceis on. - What changes. The order’s
sourcenames the source that created it, and its amounts are that source’s: they can differ from the quote’s. Show your user the order’s source and amounts. - How you know. The 201 carries a
source_fallbackwarning, with the quote’s source inquoted_sourceand why it did not take the order inreason:disabled,pausedorbusy(it was not available), orrejected,unavailableorrate_limited(it was asked and created nothing). AnIdempotency-Keyreplay of that 201 carries it again.GET /orders/{id}does not: there, compare the order’ssource.slugwith the quote’s. - What it costs. The request has a weight of 50 once, however many sources are tried.
"warnings": [ { "code": "source_fallback", "message": "The quote's source (sideshift) could not take the order, so it was placed with the next best source of the quote.", "quoted_source": "sideshift", "reason": "unavailable" }]Because the order can be placed at a fallback source, your user must see the notices
of the quote’s fallback_sources before confirming, as well as the notice of its
source, by the same rules. The fallback notices may follow the quote’s notice,
including in an expandable section placed directly next to it. If you cannot show
them, send "retry_next_best_source": false: only the quote’s source is tried, and
when it cannot take the order the request fails as described in
When no source creates the order.
See Display requirements.
Retrying with an idempotency key
Section titled “Retrying with an idempotency key”A request that times out, or whose connection drops, may still have created the order.
With an Idempotency-Key you can simply send the same request again: you get the order
the first request created, never a second one.
- The key is 8 to 64 characters of A–Z, a–z, 0–9,
_,-,:and.. A UUID fits. A malformed key is a 400invalid_requestwithdetails.field: Idempotency-Key. - Make a new key for every new order, and store it with your record of the attempt. Keys are per account, and are kept for 24 hours after the order is created.
- Same key, same body, after the order was created: 201 with the order as it is now,
not as it was when created, with the
Locationheader andIdempotent-Replayed: true, and with thesource_fallbackandrefund_address_on_filewarnings when the first answer had them. The bodies are compared as JSON, so field order and whitespace do not matter. A replay costs 1 and does not count against the order caps. - Same key, different body: 409
idempotency_conflict. - Same key while the first request is still running: 409
idempotency_in_progress, withRetry-After: 2. Wait, then send it again. - The first request failed and nothing was created: the key is released, and a retry with it runs as a new request.
- The source may have created the order without confirming it (502
source_errorwithdetails.reason: outcome_unknown): the key stays blocked for 24 hours. A retry with it answers 502source_errorwithdetails.reason: idempotency_outcome_unknowninstead of risking a second order at the source. Write to api@kotova.io with the first request’sX-Request-Id.
When you send a key and it cannot be checked, the request is refused with a 503
service_unavailable rather than run without the
protection you asked for.
Even without a key, a quote creates at most one order: a second POST /orders with the
same quote_id answers 409 quote_already_used,
with the order’s id in details.order_id once the order exists. The key is what gets
you the order itself back.
Addresses, memos and refund addresses
Section titled “Addresses, memos and refund addresses”to_addressis checked against the address format of thetonetwork. An address that does not fit is a 422invalid_addresswithdetails.reason: format. A network without a format rule accepts any address, and the source checks it when the order is created. To check an address before your user confirms, usePOST /addresses/validate.to_memo: when your user’s receiving address needs a memo or destination tag (an account at a centralised service, for example), send it here. The API cannot tell whether an address needs one. A memo on a network without memos is a 422memo_not_supported.refund_addressis an address on thefromnetwork, the one the deposit comes from, and is checked against that network’s format. When the quote’srequirements.refund_addressisrequired, an order without one is a 422refund_address_required, with the network and the source indetails. Otherwise it is optional, and we recommend sending it: see Refund addresses.refund_memo: when your user’s refund address needs a memo or destination tag, send it here. It needs arefund_address(otherwise a 400 withdetails.reason: requires_refund_address) and afromnetwork that supports memos.- Addresses are trimmed and may be up to 4,096 characters, with no spaces or control characters inside. Memos may be up to 255 characters, with no control characters. An empty memo counts as none.
- A source can refuse an address or a memo when the order is created. That is a
422
invalid_addresswithdetails.reason: rejected_by_source, anddetails.fieldnames what it refused:to_address,to_memo,refund_addressorrefund_memo. When the source did not say which,details.fieldis the best guess anddetails.fieldslists every candidate: check them all. Nothing was created, and the order is not tried at another source, whether or notretry_next_best_sourceis on.
Refund addresses
Section titled “Refund addresses”We recommend sending a refund_address with every order, and a refund_memo when that
address needs one. The refund address is never required unless the quote’s
requirements.refund_address is required. Where the source takes a refund address
with the order, it goes to the source then, so a refund needs no extra step; and a
fallback source that needs one is skipped without it, so sending one keeps more of the
quote’s fallback_sources eligible. With a refund address on file, an order that needs
attention can also be refunded automatically when nobody decides in time (see
auto_resolution in Status detail).
What the refund address is used for depends on the order’s source. The quote says it
for each of its sources in source.refund_address_forwarding, and
GET /sources for every source:
refund_address_forwarding |
What happens to the refund address | On the order |
|---|---|---|
at_creation |
It goes to the source with the order, and the source checks it then. A refusal is a 422 invalid_address with details.field: refund_address or refund_memo, and nothing is created. |
refund.forwarded: true |
when_needed |
The source takes none when the order is created. It is kept with the order and given to the source only if the order needs a refund. If the source refuses it then, the order shows status_detail.reason: refund_address_rejected. Today: FixedFloat (ffio). |
refund.forwarded: false, and the warning refund_address_on_file |
none |
The source never takes one and handles refunds itself. It is kept with the order for support only. Today: CCE Cash (ccecash). |
refund.forwarded: false, and the warning refund_address_on_file |
Every other source takes the refund address at_creation today. New values may be
added: read the field rather than a list of sources, and tolerate values you do not
know.
refund_address_on_file comes with the 201 and its Idempotency-Key replays, with the
order’s source in source and its refund_address_forwarding in intake.
GET /orders/{id} does not carry it: there, read refund.forwarded.
"warnings": [ { "code": "refund_address_on_file", "message": "This source does not take a refund address when the order is created: it is kept with the order and given to the source only if the order needs a refund.", "source": "ffio", "intake": "when_needed" }]What is checked, and in which order
Section titled “What is checked, and in which order”- The body. A malformed body is a 400
invalid_request. - The
Idempotency-Key. A replay, the two 409s and a blocked key (see above) are answered here. - Your account’s order caps. See Weight and order caps.
- The quote. 404
quote_not_foundfor an unknown quote, one that is gone, or one of another account; 409quote_already_used; 410quote_expired. A usable quote is then claimed for this request. - The addresses and memos, as described above.
- The sources. The quote’s source and then, with
retry_next_best_source, its fallback sources, in order (see Next best source). A source that cannot take an order right now is passed over. When none of them can, the answer is a 503source_unavailablewithdetails.requote: trueandRetry-After: 30. Request a new quote. - A source creates the order. Its answer decides the response, or, when it created nothing, the next source is tried.
Every answer from steps 1 to 6 comes before a source is contacted: nothing was created at any source, and the request costs 1.
When no source creates the order
Section titled “When no source creates the order”Once a source has been asked, its answer is typed so that you know what exists. When
no source took the order, the answer is that of the last source asked, and
details.source names it (the quote’s source when none was asked).
| Answer | What it means | What to do |
|---|---|---|
422 invalid_address, details.reason: rejected_by_source |
The source refused what details.field names: to_address, to_memo, refund_address or refund_memo (with details.fields when the source did not say which). Nothing was created. No other source is tried, with or without retry_next_best_source. |
Ask your user for another address or memo. The quote is released and can be used again until it expires. |
502 source_error, details.retryable: true |
The source could not create the order right now. Nothing was created. | Request a new quote and try again. |
502 source_error, details.retryable: false |
The source refused this order. Nothing was created. | Request a new quote. |
502 source_error, details.reason: outcome_unknown |
The source did not confirm the order, and it may exist there. The quote and the Idempotency-Key stay used. No other source is tried. |
Do not retry on your own. Write to api@kotova.io with the X-Request-Id. |
503 source_unavailable, details.requote: true |
No source could take an order right now. Nothing was created. | Request a new quote. |
500 internal_error with details.order_id |
The order was created but could not be read back. | Read it with GET /orders/{id}, or send the request again with the same Idempotency-Key. |
After any other 500, send the same request again with the same Idempotency-Key. It
cannot create a second order: if one was created, the retry answers it, or a 409 that
points to it.
With retry_next_best_source (the default), a 502 source_error and a 503
source_unavailable also carry details.sources_tried: the quote’s source and its
fallback sources, in the order they were tried, each with the reason it did not take the
order. So do a 422 invalid_address and a 500 internal_error when one of the
quote’s fallback sources gave them. Here the quote’s source and the second fallback
source failed, and the first fallback source was not available:
{ "error": { "code": "source_error", "message": "The source could not create the order right now; nothing was created. Request a new quote and retry.", "request_id": "req_8fK2mQ9xLp3vTz7nWc1bYd4h", "details": { "source": "changelly", "retryable": true, "sources_tried": [ { "source": "sideshift", "reason": "unavailable" }, { "source": "changenow", "reason": "busy" }, { "source": "changelly", "reason": "unavailable" } ] }, "doc_url": "https://docs.kotova.io/x/api/errors#source_error" }}The reasons are listed in Sources tried. Unless the
last entry is outcome_unknown, nothing was created at any of these sources, and the
quote and the Idempotency-Key are released.
Weight and order caps
Section titled “Weight and order caps”POST /orders has a weight of 50 against your account’s budget. It costs only 1 when it
is a replay, and when it is answered before any source was contacted (steps 1 to 6
above). Once a source has been contacted, the full 50 counts, once, whether or not the
order was created and however many sources were tried. A request refused by the rate limit (429
rate_limited) costs nothing. Weights change only with at
least 30 days’ notice.
Orders are also capped per account, across all of its keys:
- orders still waiting for a deposit (
waiting_for_deposit): 20 × limit / 250; - orders created in the last 24 hours: 100 × limit / 250.
limit is your account’s rate limit (250 by default, so 20 and 100). Each cap is
rounded down and is at least 1. Over either cap, the answer is a 429
order_cap_reached with details.cap
(open_unfunded or created_24h), details.limit, details.current and a
Retry-After header: 60 seconds for open_unfunded, and for created_24h the time
until the oldest order of the last 24 hours drops out of the window. See
Rate limits.
The console below fills its request body with example addresses. Replace to_address
and refund_address with addresses you control before you send it: every order it
creates is real, and a refund goes to the refund_address you sent.
Try itPOST/ordersAPI key · weight 50
The deposit address belongs to the source
Section titled “The deposit address belongs to the source”deposit.address is an address of the source the order was placed at, the one in the
order’s source block. Your user sends deposit.amount of from.currency on
from.network to it, and the source pays out to to.address. The funds go directly to
the executing source: Kotova never holds them.
- Show the address, the amount and the network together, and make clear that the
deposit must be sent on
from.network. - When
deposit.memois set, your user must send it with the deposit. Show it prominently, under the name indeposit.memo_label. - Show the deadline in
expires_at. source.order_idis the source’s own reference for the order, andsource.tracking_urlits tracking page, when there is one.
Reading an order
Section titled “Reading an order”GET /orders/{id} returns one order of your account: any order created with any of its
keys, current or revoked. It has a weight of 1. The id is not case-sensitive. A malformed
id is a 400 invalid_request (details.field: id), and an unknown order, one created in
the Kotova X app, or one of another account is a 404
order_not_found. The optional lang query parameter
sets the language of the source notice.
An order’s state lives at its source, so a read may first ask the source for the latest state:
- only while the order can still change;
- at most once every 10 seconds per order while it is in progress, and once a minute while it is only being followed up (a refund under way, an expired order that a late deposit could still revive, a payout whose transaction is not recorded yet);
- the interval is shared by everything that reads the order, the order page and Kotova’s own scheduled checks included, so reading more often does not refresh it more often;
- the read waits up to about 6 seconds for the source. If the source takes longer, the read returns the stored state and a later read sees the result;
- a read never fails because a refresh did.
The response header X-Kotova-Status-Refreshed is true when the order was refreshed
for this read, and false when it was served as stored.
curl -i https://api.kotova.io/x/v1/orders/X-7KQ2ZD \ -H "Authorization: Bearer $KOTOVA_API_KEY"const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function followOrder(orderId, onUpdate) { for (;;) { const response = await fetch(`https://api.kotova.io/x/v1/orders/${orderId}`, { headers: { Authorization: `Bearer ${process.env.KOTOVA_API_KEY}` }, }); if (response.status === 429 || response.status >= 500) { await sleep(Number(response.headers.get("Retry-After") ?? 30) * 1000); continue; } const order = await response.json(); if (!response.ok) throw new Error(`${order.error.code}: ${order.error.message}`); onUpdate(order); // tell your user about order.status if (order.is_final) return order; await sleep(20_000); }}import osimport timeimport requests
def follow_order(order_id, on_update): while True: response = requests.get( f"https://api.kotova.io/x/v1/orders/{order_id}", headers={"Authorization": "Bearer " + os.environ["KOTOVA_API_KEY"]}, timeout=30, ) if response.status_code == 429 or response.status_code >= 500: time.sleep(int(response.headers.get("Retry-After", "30"))) continue order = response.json() if not response.ok: raise RuntimeError(f"{order['error']['code']}: {order['error']['message']}") on_update(order) # tell your user about order["status"] if order["is_final"]: return order time.sleep(20)A completed order, shortened:
{ "id": "X-7KQ2ZD", "status": "completed", "is_final": true, "status_detail": null, "completed_at": "2026-11-10T10:41:12Z", "from": { "currency": "BTC", "network": "btc", "amount": "0.05", "amount_received": "0.05" }, "to": { "currency": "USDT", "network": "trx", "amount": "3389.8", "amount_sent": "3390.15", "address": "REPLACE_WITH_RECIPIENT_TRX_ADDRESS", "memo": null }, "transactions": { "deposit": { "hash": "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b", "explorer_url": "https://mempool.space/tx/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b", "confirmations": 3, "required_confirmations": 2 }, "payout": { "hash": "c3a2d6e9b1f04e5a8d7c6b5a49382716f5e4d3c2b1a09f8e7d6c5b4a39281706", "explorer_url": "https://tronscan.org/#/transaction/c3a2d6e9b1f04e5a8d7c6b5a49382716f5e4d3c2b1a09f8e7d6c5b4a39281706", "confirmations": null }, "refund": null }}Try itGET/orders/{id}API key · weight 1
Listing your orders
Section titled “Listing your orders”GET /orders lists your account’s API orders, from any of its keys, current or
revoked, newest first. It has a weight of 1. Each entry is a summary; read one order with
GET /orders/{id} for the full object. Orders created in the
Kotova X app are not listed.
The list never asks the sources: each order is shown as last stored. To follow one order, read it.
| Query parameter | Rules |
|---|---|
status |
Comma-separated statuses, such as waiting_for_deposit,confirming_deposit. An unknown status is a 400 invalid_request with details.reason: invalid_value and the valid statuses in details.allowed. |
partner_order_id |
Only orders with this partner_order_id. |
created_after |
Only orders created at or after this time: ISO-8601 with a time zone, such as 2026-11-01T00:00:00Z. |
created_before |
Only orders created before this time (exclusive). |
limit |
Page size, 1 to 100. Default 20. |
cursor |
The next_cursor of the previous page. Keep the other parameters the same while you page. |
An unknown or repeated query parameter is a 400 invalid_request
(details.reason: unknown_parameter or duplicate_parameter).
The answer is data, has_more and next_cursor. next_cursor is null on the last
page. Each entry has id, status, is_final, rate_type, created_at,
updated_at, from and to (each with currency, network and amount), source
(its slug), partner_order_id and order_url.
{ "data": [ { "id": "X-7KQ2ZD", "status": "completed", "is_final": true, "rate_type": "variable", "created_at": "2026-11-10T10:15:39Z", "updated_at": "2026-11-10T10:41:12Z", "from": { "currency": "BTC", "network": "btc", "amount": "0.05" }, "to": { "currency": "USDT", "network": "trx", "amount": "3389.8" }, "source": { "slug": "sideshift" }, "partner_order_id": "shop-order-1042", "order_url": "https://app.kotova.io/x/X-7KQ2ZD" } ], "has_more": true, "next_cursor": "eyJjIjoiMjAyNi0xMS0xMFQxMDoxNTozOS4xMjM0NTZaIiwiaSI6IlgtN0tRMlpEIn0"}curl -G https://api.kotova.io/x/v1/orders \ -H "Authorization: Bearer $KOTOVA_API_KEY" \ --data-urlencode "status=action_required" \ --data-urlencode "limit=100"let cursor = null;do { const url = new URL("https://api.kotova.io/x/v1/orders"); url.searchParams.set("status", "action_required"); url.searchParams.set("limit", "100"); if (cursor) url.searchParams.set("cursor", cursor);
const response = await fetch(url, { headers: { Authorization: `Bearer ${process.env.KOTOVA_API_KEY}` }, }); const page = await response.json(); if (!response.ok) throw new Error(`${page.error.code}: ${page.error.message}`);
for (const order of page.data) console.log(order.id, order.status, order.partner_order_id); cursor = page.next_cursor;} while (cursor);import osimport requests
params = {"status": "action_required", "limit": 100}while True: response = requests.get( "https://api.kotova.io/x/v1/orders", headers={"Authorization": "Bearer " + os.environ["KOTOVA_API_KEY"]}, params=params, timeout=30, ) page = response.json() if not response.ok: raise RuntimeError(f"{page['error']['code']}: {page['error']['message']}") for order in page["data"]: print(order["id"], order["status"], order["partner_order_id"]) if not page["has_more"]: break params["cursor"] = page["next_cursor"]Try itGET/ordersAPI key · weight 1
Polling politely
Section titled “Polling politely”Webhooks are not available yet, so you follow an order by reading it.
- While your user is waiting, read the order every 15 to 30 seconds until
is_finalistrue. Reading more often does not refresh it more often, and every read costs 1. - Stop when
is_finalistrue, not when the status looks final: anexpiredorder can still change for a while. - On a 429 or a 503, wait for the number of seconds in
Retry-After. - To find orders you lost track of, list them with a
statusfilter, then read each one.
Kotova also checks orders on its own schedule, so most orders reach their final state even when nobody reads them.
Expiry and late deposits
Section titled “Expiry and late deposits”expires_at is the deposit deadline: the source’s own deadline, capped at 60 minutes
after creation at most sources. Some sources can extend their deadline, so the status,
not the clock, says whether the order is still open.
An order without a deposit becomes expired. If your user sent nothing, nothing is owed:
request a new quote for a new order. A deposit that arrives late can still revive an
expired order. Kotova keeps checking with the source for a period counted from the
order’s creation, usually 24 hours and shorter at some sources for some rate types, and
is_final stays false until that period is over. Whether a late deposit is honoured
is the source’s decision, and it can also make the order action_required with the
reason late_deposit. See
The deposit window and
Late deposits.
When an order needs attention
Section titled “When an order needs attention”An order whose source reports a problem, such as a deposit of the wrong amount, has the
status action_required, and its status_detail says why, since when, and what happens
if nobody acts.
- When
status_detail.resolve_urlis set, your user can choose there, on the order page, between the options the source offers. Endpoints for these choices, and for giving a new refund address, are not available yet in the API. auto_resolutionandauto_resolution_atsay what happens, and when, if nobody chooses first.- With the reason
payout_on_hold, nobody can choose anything: write to api@kotova.io with the order id.
Every reason, and what to show your user, is on Order statuses. How sources handle these cases is described in Emergency states.
The order page
Section titled “The order page”order_url is https://app.kotova.io/x/ followed by the order id. Your user opens it by
entering the address the order pays out to (to.address). It shows the order and its
status and, where the source allows it, the choices for an order that needs attention.
Giving your user order_url is optional; to keep them in your app, show the order from
GET /orders/{id} and send them to status_detail.resolve_url only when an order needs
a decision.
The order page of an API order has no box for e-mail updates and cannot be saved to a Kotova account. It tells your user that the app they used keeps them updated, so telling your users about status changes is up to you. E-mail notifications to the end users of API orders are not available yet.
When you write to api@kotova.io about an order, include its id and the X-Request-Id
of the response in question.