# Quickstart

> From an account to a first order: create a key, request a quote, show it with the required notices, create the order and follow it until it is final.

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

This page takes you from a Kotova account to a first order, one step at a time. It
assumes your integration runs on a server: the API key is a secret, and it belongs
there rather than in a browser or a mobile app.

The examples swap 0.05 BTC on the Bitcoin network for USDT on TRON. Every path is
relative to the base URL `https://api.kotova.io/x/v1`.

## From an account to a first order

1. **Prepare your account.** Sign in at [app.kotova.io](https://app.kotova.io) with an
   account whose e-mail address is confirmed. A disposable address cannot be used for
   API access. Then:

   - turn on two-factor authentication with an authenticator app, under **Personal
     data** in your account;
   - open the [account API page](https://app.kotova.io/account/api) and choose
     **Generate referral code**. This creates your affiliate profile and accepts the
     [affiliate terms](https://kotova.io/terms/affiliate). Your API key belongs to
     this profile, and the orders you create with it are attributed to it.

2. **Create your API key.** On the [account API page](https://app.kotova.io/account/api),
   accept the [API Terms of Use](https://kotova.io/terms/api), choose **Create API key**
   and enter the 6-digit code from your authenticator app. Backup codes cannot be used
   here.

   The page then shows your secret key, which starts with `ktv_sk_`. It is shown once
   and never again, so copy it straight into your server's secret store. The examples
   on these pages read it from an environment variable named `KOTOVA_API_KEY`. See
   [Authentication](https://docs.kotova.io/x/api/authentication).

3. **Check that the API answers.** `GET /health` needs no key. The JavaScript and
   Python snippets also set up what the later steps use: the base URL, the
   `Authorization` header, and placeholders for your user's IP address, your user's
   receiving and refund addresses, and your own status display.

**curl**

```sh
curl -sS https://api.kotova.io/x/v1/health
```

**JavaScript**

```js
const BASE = 'https://api.kotova.io/x/v1';
const auth = { Authorization: `Bearer ${process.env.KOTOVA_API_KEY}` };

// Placeholders: replace them with your user's values and your own UI.
const endUserIp = '93.184.216.34'; // your user's public IP address
const userTronAddress = 'REPLACE_WITH_YOUR_USERS_TRON_ADDRESS'; // your user's own address
const userBtcAddress = 'REPLACE_WITH_YOUR_USERS_BTC_ADDRESS'; // your user's own address, for a refund
const showStatus = (order) => console.log(order.id, order.status);

// Keyless endpoints: send no key.
const health = await (await fetch(`${BASE}/health`)).json();
console.log(health.status); // "ok"
```

**Python**

```python
import os
import requests

BASE = "https://api.kotova.io/x/v1"
API_KEY = os.environ["KOTOVA_API_KEY"]
AUTH = {"Authorization": f"Bearer {API_KEY}"}

# Placeholders: replace them with your user's values and your own UI.
end_user_ip = "93.184.216.34"  # your user's public IP address
user_tron_address = "REPLACE_WITH_YOUR_USERS_TRON_ADDRESS"  # your user's own address
user_btc_address = "REPLACE_WITH_YOUR_USERS_BTC_ADDRESS"  # your user's own address, for a refund

def show_status(order):
    print(order["id"], order["status"])

# Keyless endpoints: send no key.
health = requests.get(f"{BASE}/health", timeout=10).json()
print(health["status"])  # "ok"
```

   An excerpt of the response:

   ```json
   {
     "status": "ok",
     "api_version": "v1",
     "time": "2026-11-10T10:15:04Z",
     "catalog_version": "394da0f044c4"
   }
   ```

   To make this call from your browser, use the console in
   [Check availability](https://docs.kotova.io/x/api/utilities#check-availability).

4. **Find the currency and network codes.** `GET /currencies` lists every currency
   with the networks it can be sent on (`send`) and received on (`receive`). A quote
   names each side by `currency` and `network`. Both endpoints on this step need no
   key.

**curl**

```sh
curl -sS https://api.kotova.io/x/v1/currencies
```

**JavaScript**

```js
const { data } = await (await fetch(`${BASE}/currencies`)).json();
for (const currency of data) {
  for (const leg of currency.networks) {
    if (leg.send) console.log(currency.currency, leg.network, leg.name);
  }
}
```

**Python**

```python
data = requests.get(f"{BASE}/currencies", timeout=10).json()["data"]
for currency in data:
    for leg in currency["networks"]:
        if leg["send"]:
            print(currency["currency"], leg["network"], leg["name"])
```

   To see what one currency can be swapped into, ask `GET /pairs` with `from` and
   `from_network`:

**curl**

```sh
curl -sS "https://api.kotova.io/x/v1/pairs?from=BTC&from_network=btc"
```

**JavaScript**

```js
const pairs = await (await fetch(`${BASE}/pairs?from=BTC&from_network=btc`)).json();
for (const pair of pairs.data) {
  console.log(pair.to.currency, pair.to.network, pair.rate_types.join(', '));
}
```

**Python**

```python
pairs = requests.get(
    f"{BASE}/pairs", params={"from": "BTC", "from_network": "btc"}, timeout=10
).json()["data"]
for pair in pairs:
    print(pair["to"]["currency"], pair["to"]["network"], ", ".join(pair["rate_types"]))
```

   See [Currencies and icons](https://docs.kotova.io/x/api/currencies).

5. **Request a quote.** `POST /quote` asks every eligible source and returns the best
   offer. `rate_type` is required (`variable` or `fixed`). Send your user's public IP
   address as `end_user_ip`: some sources only quote when they have it, and the quote
   says which ones were skipped without it. Use `lang` for the language of the source
   notice.

**curl**

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

**JavaScript**

```js
const quoteRes = await fetch(`${BASE}/quote`, {
  method: 'POST',
  headers: { ...auth, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    from: { currency: 'BTC', network: 'btc' },
    to: { currency: 'USDT', network: 'trx' },
    amount: '0.05',
    amount_type: 'from',
    rate_type: 'variable',
    end_user_ip: endUserIp, // your user's public IP, such as '93.184.216.34'
    lang: 'en',
  }),
});
const quote = await quoteRes.json();
if (!quoteRes.ok) throw new Error(`${quote.error.code}: ${quote.error.message}`);
```

**Python**

```python
res = requests.post(f"{BASE}/quote", headers=AUTH, timeout=30, json={
    "from": {"currency": "BTC", "network": "btc"},
    "to": {"currency": "USDT", "network": "trx"},
    "amount": "0.05",
    "amount_type": "from",
    "rate_type": "variable",
    "end_user_ip": end_user_ip,  # your user's public IP, such as "93.184.216.34"
    "lang": "en",
})
quote = res.json()
if not res.ok:
    raise RuntimeError(f"{quote['error']['code']}: {quote['error']['message']}")
```

   An excerpt of the response:

   ```json
   {
     "quote_id": "q_5mJc2Vq9XkT3nR8bYw1LdP",
     "expires_at": "2026-11-10T10:17:03Z",
     "rate_type": "variable",
     "from": { "currency": "BTC", "network": "btc", "amount": "0.05" },
     "to": { "currency": "USDT", "network": "trx", "amount": "3389.8" },
     "requirements": {
       "refund_address": "optional",
       "to_memo": "not_supported",
       "refund_memo": "not_supported"
     },
     "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"
       }
     },
     "source": {
       "slug": "sideshift",
       "name": "SideShift",
       "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"
       }
     },
     "fallback_sources": [
       {
         "source": {
           "slug": "changenow",
           "name": "ChangeNOW",
           "notice": {
             "text": "The exchange service is provided by ChangeNOW. Creating an order confirms your agreement with the ChangeNOW rules.",
             "link_text": "ChangeNOW rules",
             "link_url": "https://changenow.io/terms-of-use",
             "lang": "en"
           }
         },
         "from": { "amount": "0.05" },
         "to": { "amount": "3376.2" },
         "requirements": { "refund_address": "optional" }
       }
     ],
     "warnings": []
   }
   ```

   The `quote_id` can be used for one order, until `expires_at`: 120 seconds after the
   source answered for a `variable` quote, 60 seconds for a `fixed` one. See
   [Quotes](https://docs.kotova.io/x/api/quotes).

6. **Show the quote with the required notices.** Show your user the amounts, with
   Kotova X named as the provider, by name and icon (`provider.name`,
   `provider.icon`). Before your user confirms, show the source notice:
   `source.notice.text`, with the words in `link_text` linked to `link_url`. Show the
   notice of each of the quote's `fallback_sources` as well, by the same rules: when the
   quote's source cannot take the order, it is placed with the first of them that can.
   The fallback notices may follow the quote's notice, including in an expandable
   section placed directly next to it. All of this is mandatory. See
   [Display requirements](https://docs.kotova.io/x/api/display-requirements).

7. **Create the order.** When your user confirms, send `POST /orders` with the
   `quote_id`, the address your user receives on (`to_address`) and, if you like, your
   own reference (`partner_order_id`).

   We recommend sending `refund_address` too: your user's address on the network they
   send from, with `refund_memo` when that address needs a memo. 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. It is required only when
   the quote says `requirements.refund_address: "required"`. Ask for it in the same form
   as the address your user receives on (see
   [Asking for the addresses](https://docs.kotova.io/x/api/display-requirements#asking-for-the-addresses)).

   Send an `Idempotency-Key` header and store it with your own order. If the request
   times out, send it again with the same key and the same body: you get the order
   that was created, never a second one. Creating an order costs 50 units of your
   [rate limit](https://docs.kotova.io/x/api/rate-limits).

   If you do not show the notices of the fallback sources, add
   `"retry_next_best_source": false` to the body: the order is then placed only with
   the quote's source. See [Next best source](https://docs.kotova.io/x/api/orders#next-best-source).

**curl**

```sh
curl -sS 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_YOUR_USERS_TRON_ADDRESS",
    "refund_address": "REPLACE_WITH_YOUR_USERS_BTC_ADDRESS",
    "partner_order_id": "shop-order-1042"
  }'
```

**JavaScript**

```js
import { randomUUID } from 'node:crypto';

const idempotencyKey = randomUUID(); // store it with your order; reuse it on a retry
const orderRes = await fetch(`${BASE}/orders`, {
  method: 'POST',
  headers: { ...auth, 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey },
  body: JSON.stringify({
    quote_id: quote.quote_id,
    to_address: userTronAddress, // your user's own address
    refund_address: userBtcAddress, // recommended: where a refund goes
    partner_order_id: 'shop-order-1042',
  }),
  signal: AbortSignal.timeout(100_000),
});
const order = await orderRes.json();
if (!orderRes.ok) throw new Error(`${order.error.code}: ${order.error.message}`);
```

**Python**

```python
import uuid

idempotency_key = str(uuid.uuid4())  # store it with your order; reuse it on a retry
res = requests.post(
    f"{BASE}/orders",
    headers={**AUTH, "Idempotency-Key": idempotency_key},
    timeout=100,
    json={
        "quote_id": quote["quote_id"],
        "to_address": user_tron_address,  # your user's own address
        "refund_address": user_btc_address,  # recommended: where a refund goes
        "partner_order_id": "shop-order-1042",
    },
)
order = res.json()
if not res.ok:
    raise RuntimeError(f"{order['error']['code']}: {order['error']['message']}")
```

> [!WARNING]
> **Addresses are placeholders**
>
> The `quote_id`, the key and every address on this page are placeholders. Replace
> them with your own values and your user's own addresses, and never send funds to an
> address copied from these docs.

8. **Show the deposit instructions.** The API answers `201 Created` with the order. An
   excerpt, with the addresses shortened:

   ```json
   {
     "id": "X-7KQ2ZD",
     "status": "waiting_for_deposit",
     "is_final": false,
     "expires_at": "2026-11-10T11:15:39Z",
     "deposit": {
       "address": "bc1q9h7g…zq0p2ne",
       "memo": null,
       "memo_label": null,
       "amount": "0.05"
     },
     "order_url": "https://app.kotova.io/x/X-7KQ2ZD"
   }
   ```

   Show your user `deposit.amount` of `from.currency` on `from.network`, the
   `deposit.address` it goes to and, when it is set, `deposit.memo` under the label in
   `deposit.memo_label`. A deposit without its memo may not be credited. The deposit
   address belongs to the order's source, and the deposit is due before `expires_at`.
   When the order was placed with a fallback source, its `source` names it, its
   amounts are that source's, and `warnings` has `source_fallback`: show the order's
   source and amounts, not the quote's.

   You may give your user the `order_url` if you are happy for them to follow and
   resolve the order on Kotova: it opens the order page on app.kotova.io, which they
   unlock with the address they receive on. To keep them in your app, show the order
   yourself from `GET /orders/{id}`, and, until the API has endpoints for an order's
   choices, send them to `status_detail.resolve_url` only when an order needs a
   decision.

9. **Follow the order until it is final.** Read the order with `GET /orders/{id}` every
   15 to 30 seconds and show your user its `status`, until `is_final` is `true`. Each
   read costs 1 unit; the API refreshes the order from its source at most once every
   10 seconds while it is in progress, and once a minute while it is only being
   followed up (see [Reading an order](https://docs.kotova.io/x/api/orders#reading-an-order)).

**curl**

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

**JavaScript**

```js
const sleep = (s) => new Promise((resolve) => setTimeout(resolve, s * 1000));

async function followOrder(id) {
  for (;;) {
    const res = await fetch(`${BASE}/orders/${id}`, { headers: auth });
    const body = await res.json();
    if (res.ok) {
      showStatus(body); // your own UI
      if (body.is_final) return body;
    } else if (res.status !== 429 && res.status < 500) {
      throw new Error(`${body.error.code}: ${body.error.message}`);
    }
    await sleep(Number(res.headers.get('Retry-After')) || 20);
  }
}
```

**Python**

```python
import time

def follow_order(order_id):
    while True:
        res = requests.get(f"{BASE}/orders/{order_id}", headers=AUTH, timeout=30)
        body = res.json()
        if res.ok:
            show_status(body)  # your own UI
            if body["is_final"]:
                return body
        elif res.status_code != 429 and res.status_code < 500:
            raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
        time.sleep(int(res.headers.get("Retry-After", 20)))
```

   An order in `action_required` needs attention: `status_detail` says why and whether
   your user can act on the order page. When the reason is `payout_on_hold`, write to
   [api@kotova.io](mailto:api@kotova.io) with the order id. Every status, and what to
   show for it, is in [Order statuses](https://docs.kotova.io/x/api/order-statuses).

## Testing before you go live

A sandbox is not available yet. Requesting a quote has no side effects, so you can
build and test everything up to the order without creating anything. An order you create is a
real order at its source:

- If nobody sends the deposit, the order expires and nothing is owed (creating it still
  used 50 units of your rate limit). Until it expires it
  counts towards your account's limit of open orders waiting for a deposit (see
  [Order caps](https://docs.kotova.io/x/api/rate-limits#order-caps)).
- To test the whole flow, create an order for a small amount, send the deposit from a
  wallet you control and receive the payout at an address you control.
- The **Try it** consoles on these pages call the production API with your key. The
  order console asks you to confirm every time before it creates an order.

## Next steps

- [Errors](https://docs.kotova.io/x/api/errors): Every error code, and which requests to retry.
- [Rate limits](https://docs.kotova.io/x/api/rate-limits): Weights, headers and the per-account budget.
- [Orders](https://docs.kotova.io/x/api/orders): Creating, reading and listing orders in detail.
