# Utility endpoints

> Checking an address before you submit it, reading your own key, limits and usage, checking whether the API is available, and fetching the OpenAPI document.

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

Four small endpoints help you build and run an integration. One checks an address
before you submit it, one reports your own key, limits and usage, one tells you
whether the API is available, and one serves the OpenAPI document that describes the
whole API.

## Check an address

`POST /addresses/validate` checks an address, and optionally a memo, against the format
rule of its network. It is the same check `POST /orders` applies, so you can catch a
mistyped address while your user is still on the form. It needs your key and weighs 1.
No source is asked.

| Field | Required | Meaning |
|---|---|---|
| `currency` | Yes | The currency, as in [Identifiers](https://docs.kotova.io/x/api/currencies#identifiers). |
| `network` | Yes | The network code. |
| `address` | Yes | The address to check, 1 to 4096 characters, without spaces or control characters. Surrounding whitespace is removed. |
| `memo` | No | A memo or destination tag, at most 255 characters, or `null`. An empty string counts as no memo. |

Any other field is a 400 [`invalid_request`](https://docs.kotova.io/x/api/errors#invalid_request).

> [!WARNING]
> **Example addresses are placeholders**
>
> The address below is a placeholder that only looks like a TRON address. Replace it with
> your user's own address, and never send funds to an address copied from this
> documentation.

**curl**

```bash
curl -s https://api.kotova.io/x/v1/addresses/validate \
  -H "Authorization: Bearer $KOTOVA_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"currency": "USDT", "network": "trx", "address": "TXk8rQ7cKz3qWv5m2NbP9yLh4Gd6Sa1EjF"}'
```

**JavaScript**

```js
const res = await fetch('https://api.kotova.io/x/v1/addresses/validate', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.KOTOVA_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    currency: 'USDT',
    network: 'trx',
    address: 'TXk8rQ7cKz3qWv5m2NbP9yLh4Gd6Sa1EjF', // placeholder: use your user's address
  }),
});
const check = await res.json();
```

**Python**

```python
import os
import requests

key = os.environ["KOTOVA_API_KEY"]
res = requests.post(
    "https://api.kotova.io/x/v1/addresses/validate",
    headers={"Authorization": f"Bearer {key}"},
    json={
        "currency": "USDT",
        "network": "trx",
        "address": "TXk8rQ7cKz3qWv5m2NbP9yLh4Gd6Sa1EjF",  # placeholder
    },
    timeout=10,
)
check = res.json()
```

```json
{
  "valid": true,
  "checked": "format",
  "currency": "USDT",
  "network": "trx",
  "memo": { "supported": false, "valid": null },
  "warnings": []
}
```

| Field | Type | Meaning |
|---|---|---|
| `valid` | boolean or `null` | The verdict of the network's address format rule (see below). |
| `checked` | string | What was checked. Today always `format`; extensible. |
| `currency` | string | The currency, in its canonical spelling. |
| `network` | string | The network code, in its canonical spelling. |
| `memo.supported` | boolean | Whether the network takes a memo. |
| `memo.valid` | boolean or `null` | `false` when you sent a memo for a network that takes none; otherwise `null`, because memos have no format rule. |
| `warnings` | array | Usually empty; a `deprecated_alias` warning when you used an old spelling. |

The check is advisory. A source checks each address it receives with the order for
itself when the order is created, and its verdict is the one that counts. What each answer means for
[`POST /orders`](https://docs.kotova.io/x/api/orders):

| `valid` | Meaning | What `POST /orders` does |
|---|---|---|
| `true` | The address matches the network's format. | Accepts it. The recipient address goes to the source with the order, and so does a refund address where the source takes one at creation; FixedFloat and CCE Cash keep it on file instead (see [`refund_address_forwarding`](https://docs.kotova.io/x/api/orders#refund-addresses)). The source can still refuse an address it receives: a 422 [`invalid_address`](https://docs.kotova.io/x/api/errors#invalid_address) with `details.reason` set to `rejected_by_source`. |
| `false` | The address does not match the network's format. | Refuses it with a 422 [`invalid_address`](https://docs.kotova.io/x/api/errors#invalid_address) and `details.reason` set to `format`. |
| `null` | There is no format rule for this network. | Accepts it and leaves the check to the source. |

A memo for a network without memos (`memo.valid: false`) is refused by `POST /orders`
with a 422 [`memo_not_supported`](https://docs.kotova.io/x/api/errors#memo_not_supported). If an address you
know to be correct gets `valid: false`, write to api@kotova.io with the request's
`X-Request-Id`.

The currency must exist on that network in the catalogue, otherwise the answer is a 422
[`unknown_currency`](https://docs.kotova.io/x/api/errors#unknown_currency). Whether the leg can be sent or
received right now does not matter here. The usual errors for a keyed call apply as
well: 401, 403, 413 for a body over 16 KB, 415 without `Content-Type: application/json`,
and 429 when your account's [rate limit](https://docs.kotova.io/x/api/rate-limits) is spent.

**Endpoint:** `POST https://api.kotova.io/x/v1/addresses/validate` (Check an address)

- Authentication: API key, sent as `Authorization: Bearer $KOTOVA_API_KEY`
- Weight: 1
- Request body: `application/json` (required)

Example request:

```sh
curl -X POST 'https://api.kotova.io/x/v1/addresses/validate' \
  -H "Authorization: Bearer $KOTOVA_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
  "currency": "USDT",
  "network": "trx",
  "address": "TXk8rQ7cKz3qWv5m2NbP9yLh4Gd6Sa1EjF"
}'
```

Example response (`200`):

```json
{
  "valid": true,
  "checked": "format",
  "currency": "USDT",
  "network": "trx",
  "memo": {
    "supported": false,
    "valid": null
  },
  "warnings": []
}
```

## Read your key and limits

`GET /me` describes the key you call it with, its account, the account's rate limit
and today's usage. It needs your key, weighs 1 and takes no parameters. Calling it at
start-up is a simple way to check that a key works.

**curl**

```bash
curl -s https://api.kotova.io/x/v1/me \
  -H "Authorization: Bearer $KOTOVA_API_KEY"
```

**JavaScript**

```js
const res = await fetch('https://api.kotova.io/x/v1/me', {
  headers: { Authorization: `Bearer ${process.env.KOTOVA_API_KEY}` },
});
const me = await res.json();
```

**Python**

```python
import os
import requests

key = os.environ["KOTOVA_API_KEY"]
res = requests.get(
    "https://api.kotova.io/x/v1/me",
    headers={"Authorization": f"Bearer {key}"},
    timeout=10,
)
me = res.json()
```

```json
{
  "key": {
    "public_key": "ktv_pk_3f9a7c2kd8q1m4x6b2n5r7t9",
    "created_at": "2026-11-03T09:12:44Z",
    "last_used_at": "2026-11-10T10:12:01Z"
  },
  "affiliate": {
    "ref_code": "KX7M2Q",
    "status": "active"
  },
  "rate_limit": {
    "limit": 250,
    "remaining": 243,
    "window_seconds": 60,
    "resets_at": "2026-11-10T10:16:00Z",
    "weights": { "create_order": 50, "default": 1 }
  },
  "usage_today": {
    "quotes": 12,
    "orders": 1,
    "reads": 40,
    "validations": 0,
    "limited": 0
  },
  "mock": false
}
```

| Field | Type | Meaning |
|---|---|---|
| `key.public_key` | string | The key's public id, `ktv_pk_` followed by 24 letters and digits. It is not a secret: quote it, with the `X-Request-Id` of the request in question, when you write to api@kotova.io. |
| `key.created_at` | timestamp or `null` | When the key was created. |
| `key.last_used_at` | timestamp or `null` | When the key was last used. It is recorded at most every five minutes, so it can lag behind. |
| `affiliate.ref_code` | string or `null` | Your account's referral code. |
| `affiliate.status` | string | Always `active`: a key whose account is not active gets a 403 [`affiliate_inactive`](https://docs.kotova.io/x/api/errors#affiliate_inactive) instead. |
| `rate_limit.limit` | integer | Your account's budget, in weight units per window. |
| `rate_limit.remaining` | integer or `null` | Units left in the current window after this request; `null` when it could not be read. |
| `rate_limit.window_seconds` | integer | The window length: 60 seconds. |
| `rate_limit.resets_at` | timestamp or `null` | When the current window ends. Windows start on the full minute. |
| `rate_limit.weights.create_order` | integer | The weight of `POST /orders`: 50. |
| `rate_limit.weights.default` | integer | The weight of every other keyed call: 1. |
| `usage_today` | object or `null` | Your account's calls so far this UTC day, across all keys; `null` when it could not be read. |
| `usage_today.quotes` | integer | Calls to `POST /quote`. |
| `usage_today.orders` | integer | Calls to `POST /orders`. |
| `usage_today.reads` | integer | Calls to `GET /orders`, `GET /orders/{id}` and `GET /me`. |
| `usage_today.validations` | integer | Calls to `POST /addresses/validate`. |
| `usage_today.limited` | integer | Calls refused because the account's budget was spent. |
| `mock` | boolean | Always `false` on `api.kotova.io`. |

Limits and usage belong to the account, not to the key, so a new key starts with the
same budget and the same counters. The weights and the headers that report the budget
on every keyed answer are described in [Rate limits](https://docs.kotova.io/x/api/rate-limits).

**Endpoint:** `GET https://api.kotova.io/x/v1/me` (Your key, account and limits)

- Authentication: API key, sent as `Authorization: Bearer $KOTOVA_API_KEY`
- Weight: 1

Example request:

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

Example response (`200`):

```json
{
  "key": {
    "public_key": "ktv_pk_3f9a7c2kd8q1m4x6b2n5r7t9",
    "created_at": "2026-11-03T09:12:44Z",
    "last_used_at": "2026-11-10T10:12:01Z"
  },
  "affiliate": {
    "ref_code": "KX7M2Q",
    "status": "active"
  },
  "rate_limit": {
    "limit": 250,
    "remaining": 243,
    "window_seconds": 60,
    "resets_at": "2026-11-10T10:16:00Z",
    "weights": {
      "create_order": 50,
      "default": 1
    }
  },
  "usage_today": {
    "quotes": 12,
    "orders": 1,
    "reads": 40,
    "validations": 0,
    "limited": 0
  },
  "mock": false
}
```

## Check availability

`GET /health` tells you whether the API can answer requests. It needs no key and carries
no weight.

**curl**

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

**JavaScript**

```js
const res = await fetch('https://api.kotova.io/x/v1/health');
const up = res.ok; // 200: the API answers; 503: it cannot right now
const health = await res.json();
```

**Python**

```python
import requests

res = requests.get("https://api.kotova.io/x/v1/health", timeout=10)
up = res.status_code == 200
health = res.json()
```

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

| Field | Type | Meaning |
|---|---|---|
| `status` | string | `ok`, or `degraded` when the catalogue is more than 26 hours old. Quotes and orders still work while it is `degraded`, but recently added currencies may be missing. |
| `api_version` | string | `v1`. |
| `time` | timestamp | The server's current time, in UTC. |
| `catalog_version` | string | The catalogue being served, the same value as `catalog_version` in the [catalogue answers](https://docs.kotova.io/x/api/currencies). |
| `mock` | boolean | Always `false` on `api.kotova.io`. |

When the API cannot serve requests, the answer is a 503
[`service_unavailable`](https://docs.kotova.io/x/api/errors#service_unavailable) with one of these
`details.reason` values (the list can grow), and a `Retry-After` header for
`storage_unavailable` (10 seconds) and `catalog_missing` (30 seconds):

| `details.reason` | Meaning |
|---|---|
| `storage_unavailable` | The API's own storage does not answer. Quotes and orders cannot be created. |
| `catalog_missing` | The catalogue cannot be read. |
| `disabled` | The API is switched off. |

A 200 answer is cached for 10 seconds
(`Cache-Control: public, s-maxage=10, stale-while-revalidate=10`) and has no ETag, so
polling more often than every 10 seconds gains nothing; error answers are not cached.

The check covers the API itself, not the sources. A healthy API can still be unable to
reach the sources for a quote, which the quote reports as a 503
[`sources_unavailable`](https://docs.kotova.io/x/api/errors#sources_unavailable).

`/health` is a signal for your own monitoring, not a promise. The API is provided without
an SLA or an uptime commitment.

**Endpoint:** `GET https://api.kotova.io/x/v1/health` (Health check)

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

Example request:

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

Example response (`200`):

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

## Get the OpenAPI document

`GET /openapi.json` returns the OpenAPI 3.1 document that describes every endpoint,
parameter, field, header and error code of v1, with examples. It is the API's single
source of truth: these pages are written from it, and the Try it consoles are built from
it. It needs no key, carries no weight, and is cached like the catalogue, with a weak
`ETag` and `304` answers to `If-None-Match`.

Its address is
[`https://api.kotova.io/x/v1/openapi.json`](https://api.kotova.io/x/v1/openapi.json).

**curl**

```bash
curl -s https://api.kotova.io/x/v1/openapi.json -o kotova-x-openapi.json
```

**JavaScript**

```js
const res = await fetch('https://api.kotova.io/x/v1/openapi.json');
const spec = await res.json();
console.log(spec.info.version, Object.keys(spec.paths));
```

**Python**

```python
import requests

res = requests.get("https://api.kotova.io/x/v1/openapi.json", timeout=10)
res.raise_for_status()
spec = res.json()
```

Things to know when you generate a client from it:

- Keyed operations use the `bearerAuth` scheme; keyless ones declare `security: []`, so a
  generated client sends no key to them.
- Each keyed operation carries its weight in the `x-kotova-weight` extension.
- Fields and values of extensible enums are added within v1 without notice. Configure
  your client to ignore fields it does not know and to accept enum values it does not
  know where the document marks an enum as extensible.

The base URL itself, `https://api.kotova.io/x/v1`, answers with links to this
documentation, the OpenAPI document, the changelog and the health check. Use it without a
trailing slash: `/x/v1/` only answers with a redirect.

**Endpoint:** `GET https://api.kotova.io/x/v1/openapi.json` (This OpenAPI document)

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

Example request:

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