On this page
Overview
The partner API is for services that send their own users to CoinVast: exchange aggregators, wallets and apps. Your server talks to ours; your user sends one deposit and receives one payout. They never need an account with us.
- 01
Currencies
Load the list and refresh it every few minutes. It says what is live, the limits in each coin, and when a payout needs a memo.
- 02
Estimate
Ask for a rate in either direction. You get a rateId that holds the price for about five minutes.
- 03
Exchange
Create the exchange with that rateId and the user's payout address. You get the deposit address to show them.
- 04
Status
Poll the exchange every ten seconds or so until it reaches a final status.
Quickstart
The whole flow in four calls. Set your key once, then run them in order.
export COINVAST_API_KEY="your partner key"1. What can be exchanged right now
curl https://coinvast.io/api/v1/currencies \
-H "X-Api-Key: $COINVAST_API_KEY"2. Lock a rate for 0.01 BTC to XMR
curl -G https://coinvast.io/api/v1/estimate \
-H "X-Api-Key: $COINVAST_API_KEY" \
-d from=btc -d to=xmr -d amount=0.013. Create the exchange with that rateId
curl -X POST https://coinvast.io/api/v1/exchange \
-H "X-Api-Key: $COINVAST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "btc",
"to": "xmr",
"rateId": "eyJmcm9tIjoiYnRjIiwidG8iOiJ4bXIi...2a278f26",
"address": "<the user's XMR address>",
"refundAddress": "<the user's BTC address>"
}'4. Follow it to the payout
curl https://coinvast.io/api/v1/exchange/K7TQ2WXMNR \
-H "X-Api-Key: $COINVAST_API_KEY"Authentication
Send your key in the X-Api-Key header on every request. Keys are issued per partner, and every exchange you create is recorded against yours.
Keep the key on your server. The API is built for server to server calls, and a key shipped inside a web page or a mobile app can be copied. If a key leaks, write to us and we will replace it.
Getting a key
401 unauthorized.Rates and limits
Every exchange is fixed rate. The toAmount in an estimate is what the user receives, provided the deposit matches fromAmount, however the market moves while it confirms. There is no floating option: once a rate is locked, the payout does not change.
toAmount = fromAmount * rate - networkFee
# rounded down to the payout currency's decimalsEvery currency being exchanged right now, with its limits and network details.
Only live currencies are listed, and every one of them can be quoted. Anything missing from the list should be hidden in your interface. Refresh it every few minutes; limits move with the price.
Request
curl https://coinvast.io/api/v1/currencies \
-H "X-Api-Key: $COINVAST_API_KEY"Response
{
"rateType": "fixed",
"count": 10,
"currencies": [
{
"id": "btc",
"ticker": "BTC",
"name": "Bitcoin",
"network": "Bitcoin",
"decimals": 8,
"confirmations": 2,
"hasExtraId": false,
"extraIdName": null,
"addressRegex": "^(bc1[a-z0-9]{25,87}|[13][a-km-zA-HJ-NP-Z1-9]{25,34})$",
"minAmount": 0.00023253,
"maxAmount": 0.05813074
},
{
"id": "xmr",
"ticker": "XMR",
"name": "Monero",
"network": "Monero",
"decimals": 6,
"confirmations": 10,
"hasExtraId": false,
"extraIdName": null,
"addressRegex": "^[48][1-9A-HJ-NP-Za-km-z]{94,105}$",
"minAmount": 0.05,
"maxAmount": 8.80902
}
]
}Currency object
| Field | Type | Description |
|---|---|---|
| id | string | The id every other call expects. Network qualified where tickers repeat, for example usdt-trc20 and usdt-erc20. |
| ticker | string | Display ticker. |
| name | string | Display name. |
| network | string | The chain the currency moves on. |
| decimals | integer | Precision we quote and pay in. |
| confirmations | integer | Confirmations a deposit needs before the exchange proceeds. |
| hasExtraId | boolean | True when payouts on this network can need a memo or destination tag. |
| extraIdName | string | null | What to call that field in your interface: Memo or Destination tag. |
| addressRegex | string | A format check we apply to addresses. Useful to catch typos early; it is not full validation. |
| minAmount | number | Smallest deposit accepted, in this currency. |
| maxAmount | number | null | Largest deposit accepted, in this currency. Null means no maximum. |
A fixed rate for a pair, in either direction, with a rateId that locks it.
Query parameters
| Parameter | Type | Description |
|---|---|---|
| fromRequired | string | Currency id the user sends. |
| toRequired | string | Currency id the user receives. |
| amountConditional | number | Amount of from the user sends. Send this or amountTo, not both. |
| amountToConditional | number | Amount of to the user wants to receive. The response gives the deposit that pays at least this much. |
Request
curl -G https://coinvast.io/api/v1/estimate \
-H "X-Api-Key: $COINVAST_API_KEY" \
-d from=btc -d to=xmr -d amount=0.01Response
{
"from": "btc",
"to": "xmr",
"rateType": "fixed",
"fromAmount": 0.01,
"toAmount": 1.484899,
"rate": 148.509910322592,
"spreadPct": 2,
"networkFee": 0.0002,
"rateId": "eyJmcm9tIjoiYnRjIiwidG8iOiJ4bXIi...2a278f26",
"expiresAt": 1790072966987,
"validFor": 300,
"minAmount": 0.00023253,
"maxAmount": 0.05813074
}By the amount received
Pass amountTo to ask what the user has to send for a given payout. Deposits are rounded up to the currency's precision, so toAmount can come back a fraction above the amount you asked for, never below it.
curl -G https://coinvast.io/api/v1/estimate \
-H "X-Api-Key: $COINVAST_API_KEY" \
-d from=btc -d to=xmr -d amountTo=1.5Response fields
| Field | Type | Description |
|---|---|---|
| rateType | string | Always fixed. |
| fromAmount | number | The deposit to ask the user for. |
| toAmount | number | What the user receives, after the network fee. |
| rate | number | Units of to per one unit of from, after the spread, before the network fee. |
| spreadPct | number | Our spread, in percent, already inside rate. |
| networkFee | number | Payout network fee, in the to currency. |
| rateId | string | Pass to POST /exchange to lock this rate. Opaque; works once. |
| expiresAt | integer | When the rateId stops working, in epoch milliseconds. |
| validFor | integer | Seconds until expiresAt at the time of the response. |
| minAmount, maxAmount | number | Limits for the from currency. maxAmount is null when there is no cap. |
/api/v1/exchange
Create an exchange and get the deposit address for your user.
Body
| Parameter | Type | Description |
|---|---|---|
| fromRequired | string | Currency id the user sends. |
| toRequired | string | Currency id the user receives. |
| rateIdOptional | string | From /estimate. Recommended: it guarantees the user gets the amount you showed them. |
| amountConditional | number | The deposit amount. Required without a rateId. With one, it must match the amount the rateId was issued for. |
| addressRequired | string | The user's payout address, on the network of to. |
| extraIdOptional | string | Payout memo or destination tag, for currencies where hasExtraId is true. |
| refundAddressOptional | string | Where the deposit goes back if the exchange cannot finish, on the network of from. Strongly recommended. |
Request
curl -X POST https://coinvast.io/api/v1/exchange \
-H "X-Api-Key: $COINVAST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "btc",
"to": "xmr",
"rateId": "eyJmcm9tIjoiYnRjIiwidG8iOiJ4bXIi...2a278f26",
"address": "<the user's XMR address>",
"refundAddress": "<the user's BTC address>"
}'Response
{
"id": "K7TQ2WXMNR",
"status": "waiting",
"rateType": "fixed",
"from": "btc",
"to": "xmr",
"expectedAmountFrom": 0.01,
"expectedAmountTo": 1.484899,
"amountReceived": null,
"payinAddress": "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4",
"payinExtraId": null,
"payoutAddress": "<the user's XMR address>",
"payoutExtraId": null,
"refundAddress": "<the user's BTC address>",
"depositTxid": null,
"payoutTxid": null,
"createdAt": 1790072702114,
"updatedAt": 1790072702114,
"expiresAt": 1790074502114
}What to show your user
payinAddress and expectedAmountFrom, plus payinExtraId when it is not null. The deposit has to arrive before expiresAt, 30 minutes after creation.Exchange object
| Field | Type | Description |
|---|---|---|
| id | string | Ten characters, uppercase. Lookups ignore case. |
| status | string | See Statuses. |
| rateType | string | Always fixed for exchanges created here. |
| from, to | string | Currency ids. |
| expectedAmountFrom | number | The deposit the exchange was priced for. |
| expectedAmountTo | number | The payout, locked at creation. |
| amountReceived | number | null | The deposit as we saw it on chain. Null until it arrives. |
| payinAddress | string | Where the user sends the deposit. Unique to this exchange. |
| payinExtraId | string | null | Memo or tag the deposit must carry, when the network needs one. |
| payoutAddress, payoutExtraId | string | As you sent them. |
| refundAddress | string | null | As you sent it. |
| depositTxid, payoutTxid | string | null | Transaction hashes, set as each one happens. |
| createdAt, updatedAt, expiresAt | integer | Epoch milliseconds. updatedAt moves whenever the status does. |
The current state of an exchange.
Returns the same exchange object as the create call. Poll it every ten seconds or so while the exchange is live, and stop once the status is final.
Request
curl https://coinvast.io/api/v1/exchange/K7TQ2WXMNR \
-H "X-Api-Key: $COINVAST_API_KEY"Response
{
"id": "K7TQ2WXMNR",
"status": "confirming",
"rateType": "fixed",
"from": "btc",
"to": "xmr",
"expectedAmountFrom": 0.01,
"expectedAmountTo": 1.484899,
"amountReceived": 0.01,
"payinAddress": "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4",
"payinExtraId": null,
"payoutAddress": "<the user's XMR address>",
"payoutExtraId": null,
"refundAddress": "<the user's BTC address>",
"depositTxid": "9f1c4e7a0b...d2e8",
"payoutTxid": null,
"createdAt": 1790072702114,
"updatedAt": 1790073015880,
"expiresAt": 1790074502114
}Statuses
The usual path is waiting → confirming → exchanging → sending → finished.
| Status | Final | Meaning |
|---|---|---|
| waiting | No | Waiting for the deposit to reach payinAddress. |
| confirming | No | Deposit seen on chain, collecting the confirmations the network needs. |
| exchanging | No | Deposit confirmed. The exchange is being filled at the locked rate. |
| sending | No | Payout broadcast. payoutTxid is set. |
| finished | Yes | Payout confirmed. The exchange is finished and will not change again. |
| expired | Yes | No deposit arrived before expiresAt. |
| refunded | Yes | The deposit was returned to refundAddress. |
| failed | Yes | The deposit was not accepted and is returned to refundAddress. |
Errors
Every error has the same shape: a code in error and a sentence in message. Branch on the code. The message is written for people and its wording can change. Limit errors also carry minAmount and maxAmount, so you can correct the amount without another call.
{
"error": "amount_too_low",
"message": "The minimum deposit is 0.00023253 BTC.",
"minAmount": 0.00023253,
"maxAmount": 0.05813074
}| Code | HTTP | Meaning |
|---|---|---|
| invalid_request | 400 | A required parameter is missing or has the wrong type. |
| unknown_currency | 400 | A currency id is not one we list. Use the ids from /currencies. |
| same_currency | 400 | from and to are the same currency. |
| pair_unavailable | 400 | One side of the pair is not being exchanged right now. |
| amount_too_low | 400 | Below the deposit minimum. The response includes minAmount. |
| amount_too_high | 400 | Above the deposit maximum. The response includes maxAmount. |
| invalid_address | 400 | The payout address does not match the network of to. |
| invalid_refund_address | 400 | The refund address does not match the network of from. |
| rate_expired | 400 | The rateId is past its expiry or was not issued by us. Request a new estimate. |
| rate_mismatch | 400 | The rateId was issued for a different pair or amount than this request. |
| unauthorized | 401 | The X-Api-Key header is missing or not recognised. |
| not_found | 404 | No exchange exists with this id. |
| rate_used | 409 | This rateId already created an exchange. Each one works once. |
| rate_limited | 429 | Too many requests for this key. Wait for the Retry-After seconds. |
| internal_error | 500 | Something failed on our side. Safe to retry once. |
| api_disabled | 503 | The partner API is switched off on this deployment. |
| trading_paused | 503 | Exchanges are paused for maintenance. Retry later. |
| insufficient_liquidity | 503 | We cannot fill this size into to right now. Try a smaller amount. |
| rates_unavailable | 503 | The market price source is unreachable. Retry shortly. |
Rate limits
Limits apply per key. Past a limit you get 429 rate_limited with a Retry-After header in seconds.
Going live
There is no sandbox. Exchanges created with your key are real, but nothing moves until a deposit arrives, and an exchange with no deposit simply expires after 30 minutes. To test the whole path, run one exchange at the minimum amount.
Before you launch
- Refresh
/currenciesevery few minutes and hide anything not in it. - Send the
rateIdwith every exchange, so the user gets exactly the amount you showed. - Show
payinExtraIdwhenever it is present. - Always pass a
refundAddress. - Treat
finished,expired,refundedandfailedas final. - Keep the exchange
idwith your own order, so support can find it in one step.
Attribution
Every exchange created with your key carries your partner id, which is how volume is credited to you. Commercial terms are agreed with each partner directly.
Keyless endpoints
The swap card on coinvast.io runs on a small set of endpoints that need no key. They are limited per IP address, which suits a personal tool or a script. For anything that sends many users, use the partner API above.
Changelog
Partner API reference published. Limits in coin units on /currencies and /estimate, estimates by amount received with amountTo, rateId now works once, error codes on every response, and rateType and updatedAt on the exchange object.
First reference for the keyless endpoints.
Support
Integration questions go through the contact form or to [email protected]. Include the exchange id and the full error body; it saves a round trip.