Utility endpoints
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
Section titled “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. |
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.
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"}'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();import osimport 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(){ "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:
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). The source can still refuse an address it receives: a 422 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 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. 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. 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 is spent.
Try itPOST/addresses/validateAPI key · weight 1
Read your key and limits
Section titled “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 -s https://api.kotova.io/x/v1/me \ -H "Authorization: Bearer $KOTOVA_API_KEY"const res = await fetch('https://api.kotova.io/x/v1/me', { headers: { Authorization: `Bearer ${process.env.KOTOVA_API_KEY}` },});const me = await res.json();import osimport 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(){ "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 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.
Try itGET/meAPI key · weight 1
Check availability
Section titled “Check availability”GET /health tells you whether the API can answer requests. It needs no key and carries
no weight.
curl -s https://api.kotova.io/x/v1/healthconst res = await fetch('https://api.kotova.io/x/v1/health');const up = res.ok; // 200: the API answers; 503: it cannot right nowconst health = await res.json();import requests
res = requests.get("https://api.kotova.io/x/v1/health", timeout=10)up = res.status_code == 200health = res.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. |
mock |
boolean | Always false on api.kotova.io. |
When the API cannot serve requests, the answer is a 503
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.
/health is a signal for your own monitoring, not a promise. The API is provided without
an SLA or an uptime commitment.
Try itGET/healthNo key
Get the OpenAPI document
Section titled “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.
curl -s https://api.kotova.io/x/v1/openapi.json -o kotova-x-openapi.jsonconst 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));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
bearerAuthscheme; keyless ones declaresecurity: [], so a generated client sends no key to them. - Each keyed operation carries its weight in the
x-kotova-weightextension. - 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.
Try itGET/openapi.jsonNo key