# Currencies, networks and icons

> How currencies and networks are identified, how to list them and the pairs between them, and how to display the icons the API returns.

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

The catalogue endpoints list every currency Kotova X can swap, the networks each one
can be sent and received on, and the icons to show next to them. They need no key.

## Identifiers

A currency on one network is called a leg, and every request names a leg with two
identifiers. Send both, always: the same currency on two networks is two different legs.

- **`currency`** is the ticker, such as `BTC` or `USDT`. Requests match it in any case;
  answers always use upper case.
- **`network`** is the network code, such as `btc`, `trx` or `eth`. Requests match it in
  any case; answers always use lower case.

So USDT on TRON is `{"currency": "USDT", "network": "trx"}`, and USDT on Ethereum is
`{"currency": "USDT", "network": "eth"}`. Numeric ids never appear in the API. A leg also
has a `ticker` field, which is only for display (on some networks a currency trades under
another symbol); identifiers always use `currency`.

A `currency` must match `^[A-Za-z0-9.]{1,12}$` and a `network` must match
`^[A-Za-z0-9_-]{1,24}$`. Anything else is a 400
[`invalid_request`](https://docs.kotova.io/x/api/errors#invalid_request) with `details.reason` set to
`invalid_format`. A well-formed pair of identifiers that is not in the catalogue is a 422
[`unknown_currency`](https://docs.kotova.io/x/api/errors#unknown_currency).

### Aliases

When the catalogue renames or merges a currency, the old spelling keeps working as an
alias. `GET /currencies` lists a currency's old spellings in `aliases`; USDT, for
example, lists `USDT0`. A request that uses an alias is answered with the canonical
identifiers, plus a `deprecated_alias` warning that names both:

```json
{
  "code": "deprecated_alias",
  "message": "from.currency \"USDT0\" is a deprecated alias of \"USDT\"; use \"USDT\".",
  "field": "from.currency",
  "given": "USDT0",
  "canonical": "USDT"
}
```

The same mechanism applies to network codes. When you see this warning, switch to the
`canonical` value. Warnings never make a request fail, so log them where you will
notice them.

## How the catalogue is served

`GET /currencies`, `GET /networks` and `GET /pairs` (and `GET /sources`, see
[Sources](https://docs.kotova.io/x/api/sources)) share the same rules.

- **No key and no weight.** Send no `Authorization` header: it is ignored, and leaving it
  out lets the CDN answer. The calls do not count against your account's
  [rate limit](https://docs.kotova.io/x/api/rate-limits).
- **Browsers welcome.** Answers carry `Access-Control-Allow-Origin: *`, so a web page can
  read the catalogue directly. `HEAD` works on every one of these endpoints.
- **One page.** A list answer has `data`, `has_more`, `next_cursor`, `catalog_version`
  and `updated_at`. `has_more` is always `false` and `next_cursor` always `null`: the
  whole list comes in one answer. `catalog_version` changes whenever the catalogue is
  rebuilt, and `updated_at` is when that happened.
- **Strict query strings.** An unknown or repeated query parameter, or an empty segment
  such as `?&`, is a 400 [`invalid_request`](https://docs.kotova.io/x/api/errors#invalid_request)
  (`unknown_parameter` or `duplicate_parameter`). `GET /currencies` and `GET /networks`
  take no parameters at all, so a cache-busting `?t=…` is refused rather than ignored.
- **Per-IP limit.** Calls that reach the server, rather than the CDN, are limited to 120
  per minute per IP address. Over that, the answer is a 429
  [`rate_limited`](https://docs.kotova.io/x/api/errors#rate_limited) with `details.reason` set to `ip` and a
  `Retry-After` header.
- **Unavailable catalogue.** If the catalogue cannot be read, the answer is a 503
  [`service_unavailable`](https://docs.kotova.io/x/api/errors#service_unavailable) with `details.reason` set to
  `catalog_missing` and a `Retry-After` header.

### Caching and ETags

Successful answers are cached by the CDN with
`Cache-Control: public, s-maxage=60, stale-while-revalidate=86400` and carry a weak
`ETag` such as `W/"394da0f044c4-5be2a7d1c09e"`. Send the ETag back in `If-None-Match` and
an unchanged answer comes back as a `304 Not Modified` with no body. Error answers are
never cached (`Cache-Control: no-store`).

The catalogue changes rarely, but it does change: currencies are added, and a source can
be switched on or off at any time, which changes `send`, `receive` and the source lists
without a new `catalog_version`. The ETag covers both, so compare ETags rather than
versions. A good pattern is to keep a copy on your server and revalidate it every few
minutes with `If-None-Match`.

**curl**

```bash
# First call: note the ETag header.
curl -si https://api.kotova.io/x/v1/currencies | grep -i '^etag'

# Later: revalidate. 304 means your copy is current.
curl -si https://api.kotova.io/x/v1/currencies \
  -H 'If-None-Match: W/"394da0f044c4-5be2a7d1c09e"' | head -n 1
```

**JavaScript**

```js
const BASE = 'https://api.kotova.io/x/v1';
let cached = null; // { etag, body }

async function getCurrencies() {
  const headers = cached ? { 'If-None-Match': cached.etag } : {};
  const res = await fetch(`${BASE}/currencies`, { headers });
  if (res.status === 304) return cached.body;
  if (!res.ok) throw new Error(`currencies: HTTP ${res.status}`);
  cached = { etag: res.headers.get('ETag'), body: await res.json() };
  return cached.body;
}
```

**Python**

```python
import requests

BASE = "https://api.kotova.io/x/v1"
cached = None  # {"etag": ..., "body": ...}

def get_currencies():
    global cached
    headers = {"If-None-Match": cached["etag"]} if cached else {}
    res = requests.get(f"{BASE}/currencies", headers=headers, timeout=10)
    if res.status_code == 304:
        return cached["body"]
    res.raise_for_status()
    cached = {"etag": res.headers["ETag"], "body": res.json()}
    return cached["body"]
```

## List currencies

`GET /currencies` returns every currency that can be sent or received through the API
right now, each with the networks it can be used on. It takes no parameters.

**curl**

```bash
curl -s https://api.kotova.io/x/v1/currencies
```

**JavaScript**

```js
const res = await fetch('https://api.kotova.io/x/v1/currencies');
const { data, catalog_version } = await res.json();
```

**Python**

```python
import requests

res = requests.get("https://api.kotova.io/x/v1/currencies", timeout=10)
res.raise_for_status()
data = res.json()["data"]
```

An answer, shortened to one currency:

```json
{
  "data": [
    {
      "currency": "BTC",
      "name": "Bitcoin",
      "type": "cryptocurrency",
      "icon": {
        "url": "https://cdn.kotova.io/images/currencies/BTC.svg",
        "fallback_url": "https://assets.kotova.io/images/currencies/BTC.svg"
      },
      "aliases": [],
      "networks": [
        {
          "network": "btc",
          "network_name": "Bitcoin",
          "name": "Bitcoin",
          "ticker": "BTC",
          "contract_address": null,
          "token_standard": null,
          "decimals": 8,
          "memo": { "supported": false, "label": null },
          "address_format": "standard",
          "icon": {
            "url": "https://cdn.kotova.io/images/currencies/BTC.svg",
            "fallback_url": "https://assets.kotova.io/images/currencies/BTC.svg"
          },
          "send": true,
          "receive": true,
          "send_sources": ["sideshift", "chainflip", "changenow", "ffio"],
          "receive_sources": ["sideshift", "chainflip", "changenow", "ffio"]
        }
      ]
    }
  ],
  "has_more": false,
  "next_cursor": null,
  "catalog_version": "394da0f044c4",
  "updated_at": "2026-11-10T07:00:52Z"
}
```

Each entry in `data` is a currency:

| Field | Type | Meaning |
|---|---|---|
| `currency` | string | The identifier: the ticker, in upper case. |
| `name` | string | Display name, such as `Tether`. |
| `type` | string | Extensible: `cryptocurrency`, `stablecoin`, `utility_token`, `meme` and others. Accept values you do not know. |
| `icon` | icon or `null` | The currency's icon (see [Icons](https://docs.kotova.io/x/api/currencies#icons)). |
| `aliases` | array of strings | Old spellings still accepted for this currency. |
| `networks` | array | One entry per network the currency can be used on through the API. |

Each entry in `networks` is a leg:

| Field | Type | Meaning |
|---|---|---|
| `network` | string | The identifier: the network code, in lower case. |
| `network_name` | string | The network's display name, such as `TRON`. |
| `name` | string | The currency's display name on this network, such as `Tether (TRC20)`. |
| `ticker` | string | Display ticker on this network. Never use it as an identifier. |
| `contract_address` | string or `null` | The token contract; `null` for a network's native coin. |
| `token_standard` | string or `null` | `TRC20`, `ERC20` and so on; `null` for a native coin. |
| `decimals` | integer | On-chain decimals of this leg. Amounts carry at most `min(decimals, 12)` fractional digits. |
| `memo` | object | Whether the network takes a memo, and what it calls it (see [Memos](https://docs.kotova.io/x/api/currencies#memos)). |
| `address_format` | string | Extensible: `standard`, or `lightning_invoice` on the Lightning network. |
| `icon` | icon or `null` | The icon for this currency on this network. |
| `send` | boolean | Usable as `from` through the API right now. |
| `receive` | boolean | Usable as `to` through the API right now. |
| `send_sources` | array of slugs | Sources that accept this leg as `from`, in display order. Empty when `send` is `false`. |
| `receive_sources` | array of slugs | Sources that pay out this leg, in display order. Empty when `receive` is `false`. |

A currency is listed when at least one of its legs can be used in at least one
direction. A leg that cannot be used in either direction is left out.

### Send, receive and source lists

`send` and `receive` say whether a leg can be used as `from` or as `to` right now. A
quote with a leg in a direction it does not support fails with a 422
[`currency_unavailable`](https://docs.kotova.io/x/api/errors#currency_unavailable). Lightning legs always have
`receive: false` in v1: receiving on Lightning is not available yet.

The per-leg source lists let you apply your own source exclusions without an extra
request. A pair is served by exactly the sources that appear in the `from` leg's
`send_sources` and in the `to` leg's `receive_sources`. Remove the sources you exclude;
if nothing is left, do not offer the pair, because a quote would fail. You can narrow it
further with the fields from [`GET /sources`](https://docs.kotova.io/x/api/sources), such as `rate_types` or
`freeze_protection`.

```js
// Sources that could serve from → to, minus the slugs you exclude.
function servingSources(currencies, from, to, excluded = []) {
  const leg = (id) =>
    currencies
      .find((c) => c.currency === id.currency)
      ?.networks.find((n) => n.network === id.network);
  const a = leg(from);
  const b = leg(to);
  if (!a?.send || !b?.receive) return [];
  return a.send_sources.filter(
    (slug) => b.receive_sources.includes(slug) && !excluded.includes(slug)
  );
}

servingSources(data, { currency: 'BTC', network: 'btc' },
  { currency: 'USDT', network: 'trx' }, ['changenow']);
```

These lists say which sources serve a leg. Whether a source returns a quote for a given
amount, at a given moment, is only known when you [request a quote](https://docs.kotova.io/x/api/quotes).

### Memos

Some networks need a memo (also called a destination tag) next to the address. `memo`
says whether a network takes one:

- `supported` is `true` when the network takes a memo;
- `label` is what the network calls it, such as `Memo` or `Destination Tag`, and `null`
  when `supported` is `false`.

When `supported` is `true`, show a memo field labelled with `label` wherever your user
enters an address, and send it as `to_memo` or `refund_memo` when you
[create the order](https://docs.kotova.io/x/api/orders). A memo on a network that does not take one is
refused with a 422 [`memo_not_supported`](https://docs.kotova.io/x/api/errors#memo_not_supported).

**Endpoint:** `GET https://api.kotova.io/x/v1/currencies` (List currencies and networks)

- Authentication: none (public endpoint, send no key)
- Weight: none

Example request:

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

Example response (`200`):

```json
{
  "data": [
    {
      "currency": "BTC",
      "name": "Bitcoin",
      "type": "cryptocurrency",
      "icon": {
        "url": "https://cdn.kotova.io/images/currencies/BTC.svg",
        "fallback_url": "https://assets.kotova.io/images/currencies/BTC.svg"
      },
      "aliases": [],
      "networks": [
        {
          "network": "btc",
          "network_name": "Bitcoin",
          "name": "Bitcoin",
          "ticker": "BTC",
          "contract_address": null,
          "token_standard": null,
          "decimals": 8,
          "memo": {
            "supported": false,
            "label": null
          },
          "address_format": "standard",
          "icon": {
            "url": "https://cdn.kotova.io/images/currencies/BTC.svg",
            "fallback_url": "https://assets.kotova.io/images/currencies/BTC.svg"
          },
          "send": true,
          "receive": true,
          "send_sources": [
            "sideshift",
            "chainflip",
            "changenow",
            "ffio"
          ],
          "receive_sources": [
            "sideshift",
            "chainflip",
            "changenow",
            "ffio"
          ]
        }
      ]
    },
    {
      "currency": "USDT",
      "name": "Tether",
      "type": "stablecoin",
      "icon": {
        "url": "https://cdn.kotova.io/images/currencies/USDT.svg",
        "fallback_url": "https://assets.kotova.io/images/currencies/USDT.svg"
      },
      "aliases": [
        "USDT0"
      ],
      "networks": [
        {
          "network": "trx",
          "network_name": "TRON",
          "name": "Tether (TRC20)",
          "ticker": "USDT",
          "contract_address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
          "token_standard": "TRC20",
          "decimals": 6,
          "memo": {
            "supported": false,
            "label": null
          },
          "address_format": "standard",
          "icon": {
            "url": "https://cdn.kotova.io/images/tokens/USDT-TRX.svg",
            "fallback_url": "https://assets.kotova.io/images/tokens/USDT-TRX.svg"
          },
          "send": true,
          "receive": true,
          "send_sources": [
            "sideshift",
            "changenow",
            "ffio"
          ],
          "receive_sources": [
            "sideshift",
            "changenow",
            "ffio"
          ]
        }
      ]
    }
  ],
  "has_more": false,
  "next_cursor": null,
  "catalog_version": "394da0f044c4",
  "updated_at": "2026-11-10T07:00:52Z"
}
```

## List networks

`GET /networks` returns every network that has at least one currency usable through the
API. It takes no parameters.

**curl**

```bash
curl -s https://api.kotova.io/x/v1/networks
```

**JavaScript**

```js
const res = await fetch('https://api.kotova.io/x/v1/networks');
const { data } = await res.json();
```

**Python**

```python
import requests

res = requests.get("https://api.kotova.io/x/v1/networks", timeout=10)
res.raise_for_status()
data = res.json()["data"]
```

One entry from the answer:

```json
{
  "network": "xrp",
  "name": "XRP Ledger",
  "symbol": "XRP",
  "family": "other",
  "icon": {
    "url": "https://cdn.kotova.io/images/chains/xrp.svg",
    "fallback_url": "https://assets.kotova.io/images/chains/xrp.svg"
  },
  "memo": { "supported": true, "label": "Destination Tag" },
  "address_format": "standard",
  "evm_chain_id": null,
  "explorer_tx_url_template": "https://xrpscan.com/tx/{hash}"
}
```

| Field | Type | Meaning |
|---|---|---|
| `network` | string | The identifier: the network code, in lower case. |
| `name` | string | Display name, such as `XRP Ledger`. |
| `symbol` | string | Symbol of the network's native coin. |
| `family` | string | Extensible, lower case: `utxo`, `evm`, `solana`, `tron`, `cosmos`, `near` or `other`. Accept values you do not know. |
| `icon` | icon or `null` | The network's icon. |
| `memo` | object | `supported` and `label`, as on a leg. |
| `address_format` | string | Extensible: `standard`, or `lightning_invoice` on the Lightning network. |
| `evm_chain_id` | integer or `null` | The EVM chain id; `null` outside the `evm` family. |
| `explorer_tx_url_template` | string or `null` | A block explorer link for a transaction. Replace `{hash}` with the transaction hash. |

The list has the same envelope as `GET /currencies`: `has_more`, `next_cursor`,
`catalog_version` and `updated_at`.

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

- Authentication: none (public endpoint, send no key)
- Weight: none

Example request:

```sh
curl 'https://api.kotova.io/x/v1/networks'
```

Example response (`200`):

```json
{
  "data": [
    {
      "network": "btc",
      "name": "Bitcoin",
      "symbol": "BTC",
      "family": "utxo",
      "icon": {
        "url": "https://cdn.kotova.io/images/chains/btc.svg",
        "fallback_url": "https://assets.kotova.io/images/chains/btc.svg"
      },
      "memo": {
        "supported": false,
        "label": null
      },
      "address_format": "standard",
      "evm_chain_id": null,
      "explorer_tx_url_template": "https://mempool.space/tx/{hash}"
    },
    {
      "network": "eth",
      "name": "Ethereum",
      "symbol": "ETH",
      "family": "evm",
      "icon": {
        "url": "https://cdn.kotova.io/images/chains/eth.svg",
        "fallback_url": "https://assets.kotova.io/images/chains/eth.svg"
      },
      "memo": {
        "supported": false,
        "label": null
      },
      "address_format": "standard",
      "evm_chain_id": 1,
      "explorer_tx_url_template": "https://etherscan.io/tx/{hash}"
    },
    {
      "network": "xrp",
      "name": "XRP Ledger",
      "symbol": "XRP",
      "family": "other",
      "icon": {
        "url": "https://cdn.kotova.io/images/chains/xrp.svg",
        "fallback_url": "https://assets.kotova.io/images/chains/xrp.svg"
      },
      "memo": {
        "supported": true,
        "label": "Destination Tag"
      },
      "address_format": "standard",
      "evm_chain_id": null,
      "explorer_tx_url_template": "https://xrpscan.com/tx/{hash}"
    }
  ],
  "has_more": false,
  "next_cursor": null,
  "catalog_version": "394da0f044c4",
  "updated_at": "2026-11-10T07:00:52Z"
}
```

## Find pairs

`GET /pairs` tells you what a leg can be swapped into, what can be swapped into it, or
whether one particular pair is served. There is no list of every pair: give at least one
side.

| Parameter | Meaning |
|---|---|
| `from` | Currency sent. Needs `from_network`. |
| `from_network` | Network of `from`. Needs `from`. |
| `to` | Currency received. Needs `to_network`. |
| `to_network` | Network of `to`. Needs `to`. |

What you get depends on the sides you give:

- **Both sides:** that pair, when at least one source serves it, or an empty `data` list
  when none does.
- **`from` and `from_network` only:** every leg the `from` leg can be swapped into.
- **`to` and `to_network` only:** every leg that can be swapped into the `to` leg.

**curl**

```bash
curl -s 'https://api.kotova.io/x/v1/pairs?from=BTC&from_network=btc&to=USDT&to_network=trx'
```

**JavaScript**

```js
const params = new URLSearchParams({
  from: 'BTC', from_network: 'btc', to: 'USDT', to_network: 'trx',
});
const res = await fetch(`https://api.kotova.io/x/v1/pairs?${params}`);
const { data, warnings } = await res.json();
```

**Python**

```python
import requests

res = requests.get(
    "https://api.kotova.io/x/v1/pairs",
    params={"from": "BTC", "from_network": "btc", "to": "USDT", "to_network": "trx"},
    timeout=10,
)
res.raise_for_status()
pairs = res.json()["data"]
```

```json
{
  "data": [
    {
      "from": { "currency": "BTC", "network": "btc" },
      "to": { "currency": "USDT", "network": "trx" },
      "rate_types": ["fixed", "variable"],
      "sources": ["sideshift", "changenow", "ffio"]
    }
  ],
  "has_more": false,
  "next_cursor": null,
  "catalog_version": "394da0f044c4",
  "updated_at": "2026-11-10T07:00:52Z",
  "warnings": []
}
```

Each pair has:

| Field | Type | Meaning |
|---|---|---|
| `from` | object | `currency` and `network` of the leg sent. |
| `to` | object | `currency` and `network` of the leg received. |
| `rate_types` | array | The rate types (`fixed`, `variable`) that at least one serving source offers. |
| `sources` | array of slugs | The sources that serve this pair, in display order. |

A pair carries no amounts and no limits: those come only from a
[quote](https://docs.kotova.io/x/api/quotes). `warnings` holds a `deprecated_alias` warning when you used an
old spelling.

### Errors from pairs

| Status | Code | When |
|---|---|---|
| 400 | [`invalid_request`](https://docs.kotova.io/x/api/errors#invalid_request) | No side at all (`details.reason` is `from_or_to_required`), half a side (`required`, with `details.field` naming the missing parameter), an identifier in the wrong format, or an unknown parameter. |
| 422 | [`unknown_currency`](https://docs.kotova.io/x/api/errors#unknown_currency) | A leg is not in the catalogue. `details.side` says which. |
| 422 | [`currency_unavailable`](https://docs.kotova.io/x/api/errors#currency_unavailable) | A leg exists but cannot be used in that direction right now. |
| 422 | [`same_currency`](https://docs.kotova.io/x/api/errors#same_currency) | Both sides name the same leg. |

**Endpoint:** `GET https://api.kotova.io/x/v1/pairs` (What a currency can be swapped into (or from))

- Authentication: none (public endpoint, send no key)
- Weight: none
- Parameters:
  - `from` (query, optional): Currency sent.
  - `from_network` (query, optional): Network of `from`.
  - `to` (query, optional): Currency received.
  - `to_network` (query, optional): Network of `to`.

Example request:

```sh
curl 'https://api.kotova.io/x/v1/pairs'
```

Example response (`200`):

```json
{
  "data": [
    {
      "from": {
        "currency": "BTC",
        "network": "btc"
      },
      "to": {
        "currency": "USDT",
        "network": "trx"
      },
      "rate_types": [
        "fixed",
        "variable"
      ],
      "sources": [
        "sideshift",
        "changenow",
        "ffio"
      ]
    }
  ],
  "has_more": false,
  "next_cursor": null,
  "catalog_version": "394da0f044c4",
  "updated_at": "2026-11-10T07:00:52Z",
  "warnings": []
}
```

## Icons

Currencies, legs, networks, sources and Kotova X itself come with an icon. Every icon is
an object with two URLs:

```json
{
  "url": "https://cdn.kotova.io/images/currencies/BTC.svg",
  "fallback_url": "https://assets.kotova.io/images/currencies/BTC.svg"
}
```

- **`url`** is the file on Kotova's CDN, `https://cdn.kotova.io`.
- **`fallback_url`** is the same path on Kotova's mirror, `https://assets.kotova.io`.
  Some networks block the CDN; if `url` fails to load, retry once with `fallback_url`.
  It is `null` when an icon is hosted elsewhere, in which case `url` is that address as
  stored.
- **The icon itself can be `null`.** Show a placeholder, such as the ticker's first
  letters, rather than a broken image.

A leg's `icon` is the currency's icon on that network: the leg's own artwork where there
is one, otherwise the currency's icon, otherwise the network's.

Use the URLs exactly as returned. They are absolute and already percent-encoded, paths
are case-sensitive, and they do not follow a naming pattern, so never build one from a
ticker or a network code.

```html
<img src="https://cdn.kotova.io/images/currencies/BTC.svg"
     data-fallback="https://assets.kotova.io/images/currencies/BTC.svg"
     onerror="this.onerror = null; if (this.dataset.fallback) this.src = this.dataset.fallback;"
     alt="BTC" width="24" height="24">
```

```js
// Builds an <img> for an icon object from the API (or null).
function iconImg(icon, alt, size = 24) {
  const img = document.createElement('img');
  img.alt = alt;
  img.width = img.height = size;
  if (!icon) return img; // or render your own placeholder
  img.src = icon.url;
  img.addEventListener('error', function retry() {
    img.removeEventListener('error', retry);
    if (icon.fallback_url) img.src = icon.fallback_url;
  });
  return img;
}
```

### Caching icons

Icon files are served with `Access-Control-Allow-Origin: *`, and browsers and proxies
may cache them. You may also cache or self-host unmodified copies of
Kotova-hosted icon files to display Kotova data. A file can change under the same URL, so
refresh any copy you keep, for example once a week.

> [!WARNING]
> **Third-party marks**
>
> Most currency, network and source icons are their owners' trademarks. Kotova grants no
> licence to them. Use them only to identify the asset or source, unmodified, and at your
> own risk.

The Kotova X icon, which you must show wherever you present a Kotova X quote or order, is
covered in [Display requirements](https://docs.kotova.io/x/api/display-requirements).
