Three methods: get a rate, create an exchange, read its status. Amounts as strings, errors in one shape, and you issue the key yourself.
A live key is issued immediately, with no forms and no waiting. Create it, put it in the header, go.
https://co-re.kzGet a keyThe key goes in the Authorization header as a Bearer token. It is shown once at creation. It can be revoked and rotated without downtime.
curl https://co-re.kz/api/v1/quotes \
-H "Authorization: Bearer <api_key>" \
-H "Content-Type: application/json" \
-d '{"send":{"asset":"BTC","network":"BTC"},
"receive":{"asset":"USDT","network":"TRON"},
"amount":"0.05"}'There is also a test key if you want to request rates without creating anything: it works on /quotes only, and answers 401 on create and status.
Money is always a string: "0.0213585", never a number. A float loses precision on somebody's balance.
An asset is a code and a network. A bare code is ambiguous: USDT on TRON and USDT on ETH are different coins, and getting it wrong is irreversible.
Every error has the same shape. Branch on code, show message.
{ "error": { "code": "pair_unavailable", "message": "..." } }Times are UTC, ISO 8601.
The best live rate for a pair and amount across active providers. Creates nothing and moves nothing. The executor is named in the response.
{
"send": { "asset": "BTC", "network": "BTC" },
"receive": { "asset": "USDT", "network": "TRON" },
"amount": "0.05"
}{
"quote": {
"send": { "asset": "BTC", "network": "BTC", "amount": "0.05" },
"receive": { "asset": "USDT", "network": "TRON", "amount": "3212.44" },
"rateType": "float",
"executor": {
"name": "ChangeNOW",
"disclosure": "...",
"passport": {
"name": "ChangeNOW",
"rateModes": ["float"],
"custody": "executor",
"refundAddressAtCreate": true,
"underpaymentResolution": false,
"terms": { "version": "2026-07-18", "url": "https://..." },
"memoNetworks": ["XRP"],
"coreTrackRecord": {
"legs": 4, "completed": 4, "failedOrRefunded": 0,
"averageMinutes": 11, "lastUsedAt": "2026-07-20T14:49:13Z"
}
}
},
"coreMarkup": null,
"comparedExecutors": [
{ "name": "ChangeNOW", "amount": "3212.44" },
{ "name": "LetsExchange", "amount": "3209.10" },
{ "name": "StealthEX", "amount": "3201.77" }
]
}
}The /quotes response carries three fields you can pass straight to your user without asking us: executor.passport (who holds the deposit, rate modes, terms, our own history with that provider), coreMarkup, and comparedExecutors with everyone who answered.
coreMarkup is always null: the receive amount is exactly what the provider promised, with nothing added on top.
Picks the executor, re-quotes with it and returns the deposit address your client pays into. A rate from /quotes is never accepted: it is always recomputed.
{
"send": { "asset": "BTC", "network": "BTC" },
"receive": { "asset": "USDT", "network": "TRON" },
"amount": "0.05",
"payoutAddress": "TQ5...",
"refundAddress": "bc1q...",
"memo": null
}{
"deal": {
"id": "7f1c...",
"status": "AWAITING_DEPOSIT",
"send": { "asset": "BTC", "network": "BTC", "amount": "0.05" },
"receive": { "asset": "USDT", "network": "TRON", "amount": "3212.44" },
"deposit": {
"address": "bc1q...",
"tag": null,
"amount": "0.05",
"deadline": "2026-07-27T10:15:00Z"
},
"trackUrl": "https://<site>/track/7f1c..."
}
}When the outcome of the provider order is unknown, the answer is 202 with status REVIEW and no deposit address. There is no auto-retry; an operator resolves it. Treating any 2xx as success tells your client to send coins to a deal with nowhere to receive them.
The current state of your deal, reconstructed from the event log. While the deposit is awaited the response repeats the deposit details.
{
"deal": {
"id": "7f1c...",
"status": "ACTIVE",
"send": { "asset": "BTC", "network": "BTC", "amount": "0.05" },
"receive": { "asset": "USDT", "network": "TRON", "amount": "3212.44", "final": false },
"trackUrl": "https://<site>/track/7f1c..."
}
}The trackUrl in a response is the public deal page. You can hand it to your client: no account is needed, and it shows the status, the hashes and a support form.
Create answers AWAITING_DEPOSIT or REVIEW, which describe the execution step. Below are the statuses that reading the deal returns. Poll the GET and branch on that.
| DRAFT | Recorded; the provider order is not confirmed yet. |
| ACTIVE | In flight: awaiting the deposit, confirmations, the exchange or the payout. |
| COMPLETED | The payout has been sent to the client. · terminal |
| FAILED | The exchange did not happen and no refund was made. · terminal |
| REFUNDED | The provider returned the coins to the refund address. · terminal |
| EXPIRED | The deposit did not arrive in time. · terminal |
There are no webhooks. Poll GET /api/v1/deals/:id, 15-30 seconds is a sensible interval, and stop on a terminal status.
The Idempotency-Key header is required on create. A retry with the same key returns the original deal instead of creating a second one. This used to fall back to a generated key when the header was absent, so a retry after a timeout created a second provider order, which is a second charge to the client. It is now a 400.
curl https://co-re.kz/api/v1/deals \
-H "Authorization: Bearer <api_key>" \
-H "Idempotency-Key: 6b1f2c58-0a1e-4b3f-9d21-8c0f4a7e5b12" \
-H "Content-Type: application/json" \
-d '{"send":{"asset":"BTC","network":"BTC"},
"receive":{"asset":"USDT","network":"TRON"},
"amount":"0.05",
"payoutAddress":"TQ5...",
"refundAddress":"bc1q..."}'Generate the key per exchange attempt, not per HTTP request. Retry with the same key until you get a final answer.
| HTTP | code | |
|---|---|---|
| 400 | invalid_request | The body is not JSON, or a required field is missing: send, receive, amount, payoutAddress, refundAddress. |
| 400 | invalid_amount | amount is not a positive decimal string. |
| 400 | idempotency_key_required | POST /deals came without an Idempotency-Key header. Without it a retry would create a second exchange. |
| 401 | unauthorized | No Authorization header, the key is invalid or revoked, or a sandbox key hit a live-only endpoint. |
| 404 | not_found | No deal with that id among the ones your key created. Another tenant's deal also returns 404: existence is not leaked. |
| 422 | amount_below_minimum | The amount is below the provider minimum for this pair. Increase it. |
| 422 | pair_unavailable | No active provider quotes this pair right now, or the instrument was disabled between the quote and the create. |
| 422 | invalid_payout_address | payoutAddress does not look like an address on the receive network. Checked before any provider call. |
| 422 | invalid_refund_address | refundAddress does not look like an address on the send network. |
| 422 | memo_required | The receive network requires a memo or tag and the memo field was not sent. |
| 422 | refund_memo_unsupported | The send network identifies the beneficiary by memo and there is no refund memo field. refundAddress is mandatory, so this send asset is not supported. |
| 429 | rate_limited | More than 60 requests per minute on one key. |
Right now the API cannot do: a fixed rate (every active provider is float-only), cancelling an exchange after creation, a separate network-fee breakdown (providers do not give us one and we will not invent it), or webhooks.