# Orders

> How to create an order from a quote, what happens when the quote's source cannot take it, where your user sends the deposit, how to read and list your orders, and how often to check them.

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

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

`POST /orders` creates the order at the source of a [quote](https://docs.kotova.io/x/api/quotes) 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](https://docs.kotova.io/x/api/orders#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](https://docs.kotova.io/x/api/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.

> [!WARNING]
> **Every address on this page is a placeholder**
>
> Replace `to_address` and `refund_address` with your user's own addresses. The deposit
> address in the examples is an example too: your user sends funds only to the
> `deposit.address` of the order you created.

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

```sh
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"
  }'
```

**JavaScript**

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

**Python**

```python
import os
import uuid
import 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

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`](https://docs.kotova.io/x/api/quotes), 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](https://docs.kotova.io/x/api/orders#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](https://docs.kotova.io/x/api/orders#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

An order that was created answers 201, with a `Location` header (`/x/v1/orders/X-7KQ2ZD`)
and the order object. [`GET /orders/{id}`](https://docs.kotova.io/x/api/orders#reading-an-order) returns the same object.
Shortened here: `provider` and `source` carry the full blocks described below.

```json
{
  "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](https://docs.kotova.io/x/api/quotes#rounding): 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](https://docs.kotova.io/x/api/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](https://docs.kotova.io/x/api/order-statuses#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](https://docs.kotova.io/x/api/orders#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](https://docs.kotova.io/x/api/orders#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](https://docs.kotova.io/x/api/orders#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

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`](https://docs.kotova.io/x/api/quotes#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_sources` in
  order: at most three sources, all named in the quote. A fallback source whose
  `requirements.refund_address` is `required` is skipped when the order has no
  `refund_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 are `deadline` in `details.sources_tried`.
- **What is never retried elsewhere.** An address or memo a source refuses (422
  [`invalid_address`](https://docs.kotova.io/x/api/errors#invalid_address) with `details.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 (502
  [`source_error`](https://docs.kotova.io/x/api/errors#source_error) with `details.reason: outcome_unknown`)
  end the request at that source: the address is the problem, or the order may exist
  there. A 422 `invalid_address` from one of the quote's fallback sources carries
  `details.sources_tried`, as does a 500 `internal_error` from one of them; a 502
  `outcome_unknown` carries it whenever `retry_next_best_source` is on.
- **What changes.** The order's `source` names 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_fallback` warning, with the quote's
  source in `quoted_source` and why it did not take the order in `reason`: `disabled`,
  `paused` or `busy` (it was not available), or `rejected`, `unavailable` or
  `rate_limited` (it was asked and created nothing). An `Idempotency-Key` replay of
  that 201 carries it again. `GET /orders/{id}` does not: there, compare the order's
  `source.slug` with the quote's.
- **What it costs.** The request has a weight of 50 once, however many sources are
  tried.

```json
"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](https://docs.kotova.io/x/api/orders#when-no-source-creates-the-order).
See [Display requirements](https://docs.kotova.io/x/api/display-requirements#fallback-sources).

### 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 400 `invalid_request` with `details.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 `Location` header and `Idempotent-Replayed: true`,
  and with the `source_fallback` and `refund_address_on_file` warnings 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`](https://docs.kotova.io/x/api/errors#idempotency_conflict).
- **Same key while the first request is still running:** 409
  [`idempotency_in_progress`](https://docs.kotova.io/x/api/errors#idempotency_in_progress), with
  `Retry-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_error`
  with `details.reason: outcome_unknown`): the key stays blocked for 24 hours. A retry
  with it answers 502 [`source_error`](https://docs.kotova.io/x/api/errors#source_error) with
  `details.reason: idempotency_outcome_unknown` instead of risking a second order at the
  source. Write to api@kotova.io with the first request's `X-Request-Id`.

When you send a key and it cannot be checked, the request is refused with a 503
[`service_unavailable`](https://docs.kotova.io/x/api/errors#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`](https://docs.kotova.io/x/api/errors#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

- **`to_address`** is checked against the address format of the `to` network. An
  address that does not fit is a 422 [`invalid_address`](https://docs.kotova.io/x/api/errors#invalid_address)
  with `details.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, use [`POST /addresses/validate`](https://docs.kotova.io/x/api/utilities).
- **`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 422
  [`memo_not_supported`](https://docs.kotova.io/x/api/errors#memo_not_supported).
- **`refund_address`** is an address on the `from` network, the one the deposit comes
  from, and is checked against that network's format. When the quote's
  `requirements.refund_address` is `required`, an order without one is a 422
  [`refund_address_required`](https://docs.kotova.io/x/api/errors#refund_address_required), with the network
  and the source in `details`. Otherwise it is optional, and we recommend sending it:
  see [Refund addresses](https://docs.kotova.io/x/api/orders#refund-addresses).
- **`refund_memo`**: when your user's refund address needs a memo or destination tag,
  send it here. It needs a `refund_address` (otherwise a 400 with
  `details.reason: requires_refund_address`) and a `from` network 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_address`](https://docs.kotova.io/x/api/errors#invalid_address) with `details.reason:
  rejected_by_source`, and `details.field` names what it refused: `to_address`,
  `to_memo`, `refund_address` or `refund_memo`. When the source did not say which,
  `details.field` is the best guess and `details.fields` lists every candidate: check
  them all. Nothing was created, and the order is not tried at another source, whether
  or not `retry_next_best_source` is on.

#### 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](https://docs.kotova.io/x/api/order-statuses#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`](https://docs.kotova.io/x/api/sources#source-fields) 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`.

```json
"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

1. **The body.** A malformed body is a 400 `invalid_request`.
2. **The `Idempotency-Key`.** A replay, the two 409s and a blocked key (see above) are
   answered here.
3. **Your account's order caps.** See [Weight and order caps](https://docs.kotova.io/x/api/orders#weight-and-order-caps).
4. **The quote.** 404 [`quote_not_found`](https://docs.kotova.io/x/api/errors#quote_not_found) for an unknown
   quote, one that is gone, or one of another account; 409
   [`quote_already_used`](https://docs.kotova.io/x/api/errors#quote_already_used); 410
   [`quote_expired`](https://docs.kotova.io/x/api/errors#quote_expired). A usable quote is then claimed for
   this request.
5. **The addresses and memos**, as described above.
6. **The sources.** The quote's source and then, with `retry_next_best_source`, its
   fallback sources, in order (see [Next best source](https://docs.kotova.io/x/api/orders#next-best-source)). A source
   that cannot take an order right now is passed over. When none of them can, the
   answer is a 503 [`source_unavailable`](https://docs.kotova.io/x/api/errors#source_unavailable) with
   `details.requote: true` and `Retry-After: 30`. Request a new quote.
7. **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

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}`](https://docs.kotova.io/x/api/orders#reading-an-order), 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:

```json
{
  "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](https://docs.kotova.io/x/api/errors#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

`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`](https://docs.kotova.io/x/api/errors#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`](https://docs.kotova.io/x/api/errors#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](https://docs.kotova.io/x/api/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.

**Endpoint:** `POST https://api.kotova.io/x/v1/orders` (Create an order from a quote)

- Authentication: API key, sent as `Authorization: Bearer $KOTOVA_API_KEY`
- Weight: 50
- Parameters:
  - `Idempotency-Key` (header, optional): Recommended: 8-64 characters of A-Z a-z 0-9 _ - : . (a UUID fits). A retry with the same key and the same body answers the same order (201, `Idempotent-Replayed: true`) instead of creating a second one; the same key with a different body is 409 `idempotency_conflict`. Keys are kept 24 hours, per account. While the first request with a key is still running, a retry with it is 409 `idempotency_in_progress`: repeat it after `Retry-After`. Once that request has contacted a source it may run up to about 100 s after it arrived. If it was cut off there before its outcome was recorded, the source may hold the order: from then on the key answers 502 `source_error` with `reason: idempotency_outcome_unknown` (retryable false) for 24 hours. Do not place the order again under a new key; contact api@kotova.io with the request id.
- Request body: `application/json` (required)

Example request:

```sh
curl -X POST '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": "TXk8rQ7cKz3qWv5m2NbP9yLh4Gd6Sa1EjF",
  "refund_address": "REPLACE_WITH_YOUR_BTC_REFUND_ADDRESS",
  "partner_order_id": "shop-order-1042",
  "lang": "en",
  "retry_next_best_source": true
}'
```

Example response (`201`):

```json
{
  "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": "TXk8rQ7cKz3qWv5m2NbP9yLh4Gd6Sa1EjF",
    "memo": null
  },
  "deposit": {
    "address": "bc1q9h7garjv2gx0s4kd3mzt6v8wcl5ul3xzq0p2ne",
    "memo": null,
    "memo_label": null,
    "amount": "0.05"
  },
  "refund": {
    "address": "REPLACE_WITH_YOUR_BTC_REFUND_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",
    "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"
    },
    "order_id": "7f3c1e9a2b5d4c8e",
    "tracking_url": "https://sideshift.ai/orders/7f3c1e9a2b5d4c8e"
  },
  "warnings": []
}
```

## 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.memo` is set, your user must send it with the deposit. Show it
  prominently, under the name in `deposit.memo_label`.
- Show the deadline in `expires_at`.
- `source.order_id` is the source's own reference for the order, and `source.tracking_url`
  its tracking page, when there is one.

## 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`](https://docs.kotova.io/x/api/errors#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**

```sh
curl -i https://api.kotova.io/x/v1/orders/X-7KQ2ZD \
  -H "Authorization: Bearer $KOTOVA_API_KEY"
```

**JavaScript**

```js
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);
  }
}
```

**Python**

```python
import os
import time
import 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:

```json
{
  "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
  }
}
```

**Endpoint:** `GET https://api.kotova.io/x/v1/orders/{id}` (Get an order)

- Authentication: API key, sent as `Authorization: Bearer $KOTOVA_API_KEY`
- Weight: 1
- Parameters:
  - `id` (path, required): The order id (`X-XXXXXX`, case-insensitive).
  - `lang` (query, optional): Language of the source notice (en default). One of: `en`, `de`, `it`, `es`, `fr`, `ru`.

Example request:

```sh
curl 'https://api.kotova.io/x/v1/orders/X-7KQ2ZD' \
  -H "Authorization: Bearer $KOTOVA_API_KEY"
```

Example response (`200`):

```json
{
  "id": "X-7KQ2ZD",
  "status": "completed",
  "is_final": true,
  "status_detail": null,
  "rate_type": "variable",
  "created_at": "2026-11-10T10:15:39Z",
  "updated_at": "2026-11-10T10:41:12Z",
  "expires_at": "2026-11-10T11:15:39Z",
  "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": "TXk8rQ7cKz3qWv5m2NbP9yLh4Gd6Sa1EjF",
    "memo": null
  },
  "deposit": {
    "address": "bc1q9h7garjv2gx0s4kd3mzt6v8wcl5ul3xzq0p2ne",
    "memo": null,
    "memo_label": null,
    "amount": "0.05"
  },
  "refund": {
    "address": "REPLACE_WITH_YOUR_BTC_REFUND_ADDRESS",
    "memo": null,
    "forwarded": true
  },
  "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
  },
  "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",
    "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"
    },
    "order_id": "7f3c1e9a2b5d4c8e",
    "tracking_url": "https://sideshift.ai/orders/7f3c1e9a2b5d4c8e"
  },
  "warnings": []
}
```

## 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}`](https://docs.kotova.io/x/api/orders#reading-an-order) 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`.

```json
{
  "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**

```sh
curl -G https://api.kotova.io/x/v1/orders \
  -H "Authorization: Bearer $KOTOVA_API_KEY" \
  --data-urlencode "status=action_required" \
  --data-urlencode "limit=100"
```

**JavaScript**

```js
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);
```

**Python**

```python
import os
import 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"]
```

**Endpoint:** `GET https://api.kotova.io/x/v1/orders` (List your orders)

- Authentication: API key, sent as `Authorization: Bearer $KOTOVA_API_KEY`
- Weight: 1
- Parameters:
  - `status` (query, optional): Comma-separated order statuses: waiting_for_deposit, confirming_deposit, exchanging, sending, completed, action_required, refunding, refunded, expired.
  - `partner_order_id` (query, optional): Only orders with this partner_order_id.
  - `created_after` (query, optional): Created at or after (ISO-8601 with a zone).
  - `created_before` (query, optional): Created before (exclusive).
  - `limit` (query, optional): Page size, 1-100.
  - `cursor` (query, optional): `next_cursor` of the previous page.

Example request:

```sh
curl 'https://api.kotova.io/x/v1/orders' \
  -H "Authorization: Bearer $KOTOVA_API_KEY"
```

Example response (`200`):

```json
{
  "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"
}
```

## 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_final` is
  `true`. Reading more often does not refresh it more often, and every read costs 1.
- Stop when `is_final` is `true`, not when the status looks final: an `expired` order
  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 `status` filter, 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

`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](https://docs.kotova.io/x/how-it-works/order-lifecycle#the-deposit-window) and
[Late deposits](https://docs.kotova.io/x/how-it-works/order-lifecycle#late-deposits).

## 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_url` is 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_resolution` and `auto_resolution_at` say 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](https://docs.kotova.io/x/api/order-statuses#status-detail). How sources handle these cases is
described in [Emergency states](https://docs.kotova.io/x/how-it-works/order-lifecycle#emergency-states).

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