Bring exchange to your product: get a quote, create an order and track execution. API methods, examples and integration steps in one place.
Create a key in your account and include it in the header of each request.
https://co-re.kzThe key goes in the Authorization header as a Bearer token. It is shown once at creation. It can be revoked and rotated without downtime. Up to five active keys per account. There is no sandbox: a key is live from the first minute.
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"}'Amounts are decimal strings, for example "0.0213585". Do not convert them to floating-point numbers: this loses precision.
Specify both the asset code and network. USDT on TRON and USDT on Ethereum require different addresses and are not interchangeable.
Use code to handle the error and message to inform the user.
{ "error": { "code": "pair_unavailable", "message": "..." } }Times are UTC, ISO 8601.
What CORE can exchange right now: code, network, which direction works, whether a memo is required, and the precision. It asks no provider, so it is cheap and cacheable. Integration starts here: without it a coin list has to be hardcoded.
{
"currencies": [
{
"asset": "USDT",
"network": "TRON",
"name": "Tether USD",
"sendEnabled": true,
"receiveEnabled": true,
"memoRequired": false,
"decimals": 6
},
{
"asset": "XRP",
"network": "XRP",
"name": "XRP",
"sendEnabled": false,
"receiveEnabled": true,
"memoRequired": true,
"decimals": 6
}
],
"count": 2
}The minimum and maximum for a pair, both denominated in the send asset. Derived by asking providers with two deliberately out-of-range amounts, so it costs two provider round-trips: cache it per pair, do not call it per keystroke. null means no provider stated one - not zero, and not unlimited.
{
"send": { "asset": "BTC", "network": "BTC" },
"receive": { "asset": "USDT", "network": "TRON" }
}{
"limits": {
"send": { "asset": "BTC", "network": "BTC" },
"receive": { "asset": "USDT", "network": "TRON" },
"min": "0.0007",
"max": null
}
}The same question create will ask, from the same module - validate first and create will not then refuse the address. It also answers the memo question, which is the one that loses coins: an XRP payout without its tag is unrecoverable, and knowing that before the form is submitted is worth more than knowing it after. Structural only: shape, charset, length. Not a checksum, and not whether anybody controls it.
{
"asset": "XRP",
"network": "XRP",
"address": "rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH"
}{
"valid": true,
"networkKnown": true,
"memoRequired": true,
"checked": "structure"
}What has been credited and what you can withdraw, in USD. Every figure is zero today: the partner rate is zero on every key until the commission has a way of being withheld at the executor. Computed from the same view the payout path uses, so this figure cannot disagree with what a payout would pay. `held` is not "not yet" but "something is wrong with that deal".
{
"balance": {
"currency": "USD",
"available": "0",
"pending": "0",
"held": "0",
"reserved": "0",
"paid": "0",
"minPayout": "10"
}
}The best live rate for a pair and amount across active providers. Creates nothing and moves nothing. The executor is named where its own terms require it, and marked undisclosed otherwise.
{
"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",
"expiresAt": "2026-08-24T09:14:44Z",
"executor": { "disclosed": false, "executorTermsAccepted": true },
"coreMarkup": "0",
"partnerMarkup": "0",
"comparedExecutors": 3
}
}The /quotes response carries executor: by default only the fact that there is one and that its terms are accepted. The name, disclosure and passport (who holds the deposit, rate modes, terms, and our own history with that provider) come back where the executor's terms require naming it. comparedExecutors is a count by default: how many executors answered. Whether you show any of it to your own user is your decision, not our requirement. The executor names are switched on per key, for a partner that really does show the comparison.
Partner rewards are not live yet. partnerMarkup is always the string "0"; do not use it to calculate earnings or promise payouts. The API and exchange tracking are available separately from the rewards programme. Agree accrual and payout terms with CORE before a commercial launch.
Your deals, newest first, paged by cursor. Query parameters: limit (default 50, maximum 100) and cursor from the previous page's nextCursor. Cursor rather than offset: new deals arrive at the top, so offset=50 means something different every time, and a partner walking the pages would see some rows twice and miss others. partnerMarkup and commissionUsd are zero today: the rate on the key is zero.
{
"deals": [
{
"id": "7f1c...",
"status": "COMPLETED",
"createdAt": "2026-08-24T09:12:44Z",
"send": { "asset": "BTC", "network": "BTC", "amount": "0.05" },
"receive": { "asset": "USDT", "network": "TRON", "amount": "3180.31", "final": true },
"partnerMarkup": "0",
"commissionUsd": "0"
}
],
"nextCursor": "MjAyNi0wOC0yNFQwOToxMjo0NFp8N2YxYw"
}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..."
}
}If creation is unconfirmed, the API returns 202 with status REVIEW and no deposit address. This is not confirmation that the exchange was created. Do not request a deposit from the customer; an operator will review the order.
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 },
"deposit": {
"address": "bc1q...",
"tag": null,
"amount": "0.05",
"deadline": "2026-08-24T09:42:44Z"
},
"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.
Idempotency-Key is required when creating an exchange. Without it, the API returns 400. A retry with the same key returns the original deal without creating another.
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. Format: 8 to 128 characters, ASCII letters, digits and . _ : - , starting with a letter or digit; a UUID works.
| 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, or the key is invalid or revoked. |
| 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. |
| 409 | idempotency_key_conflict | The same Idempotency-Key against a different exchange: another pair, amount or address. Generate one key per exchange attempt and reuse it only for retries of that attempt. Since migration 085 this no longer fires merely because the comparison routed elsewhere on the retry. |
| 503 | replay_executor_unavailable | A retry with the same Idempotency-Key: the exchange already exists, but the executor holding it cannot be reached right now. Retry with the SAME key shortly. A new key would create a second real order. |
| 503 | order_reservation_failed | The order could not be reserved; the provider was never called and no order exists. Retry with the SAME key. This used to surface as a bare 500. |
| 422 | executor_refused_request | The executor validated the request and refused it: a bad address, an amount out of range, an unsupported asset, or an expired quote. No order exists and no deposit is needed. Correct it and submit with a NEW Idempotency-Key. This used to return 202 REVIEW telling you to hold a deposit for an order that was never coming. |
| 422 | amount_below_minimum | The amount is below the provider minimum for this pair. Increase it. |
| 422 | amount_above_maximum | The amount is above the provider maximum for this pair. Reduce it. This case used to arrive as pair_unavailable, which was false. |
| 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. |
| 503 | balance_unavailable | The balance cannot be read right now. An error, not a zero: a false zero on a money endpoint is worse than a 5xx. |
| 429 | rate_limited | More than 60 requests per minute on one key. |
Not supported: fixed rates, cancelling a created exchange, specifying an exact receive amount, separate network-fee breakdowns, webhooks or a sandbox. Active providers use floating rates.