Public API · v1 · Free
Swapnoir API
Live exchange rates, supported currencies and order tracking for your wallet, bot or website. Read only, no API key, no account.
- Base URL
https://swapnoir.com/api/v1- Authentication
- None
- Format
- JSON, UTF-8
- Errors
- RFC 9457
Overview
The Swapnoir API gives your app the same live data the swapnoir.com exchange uses. It is a small REST API over HTTPS with three endpoints, returns JSON and is described in an OpenAPI 3.1 file you can load into Postman, Insomnia, Scalar or a code generator.
Show live rates
Price widgets, calculators and bots. Estimates already include all fees for the payout.
Send users to an exchange
Open a prefilled exchange on swapnoir.com from your app with one link.
Track orders
Follow an exchange by its order ID and notify your users when it completes.
Quickstart
No signup needed. Run these in a terminal; each one works on its own.
- List what you can exchange. Use the
idof each currency in other calls.curl "https://swapnoir.com/api/v1/currencies" - Get a live estimate. How much XMR does 0.01 BTC buy, after fees?
curl "https://swapnoir.com/api/v1/estimate?from=BTC&to=XMR&amount=0.01" - Send your user to the exchange and track it. The user finishes on swapnoir.com and gets an order page. Its ID lets you follow the order.
curl "https://swapnoir.com/api/v1/orders/K7M2QX9PRT4B"
Requests and data
| Base URL | https://swapnoir.com/api/v1. HTTPS only. |
|---|---|
| Methods | All endpoints are GET. Parameters go in the query string or path. |
| Responses | application/json, UTF-8. Errors use application/problem+json. |
| Amounts | Decimal strings such as "0.01", so no precision is lost. Send amounts with a dot and at most the currency’s decimals. |
| Currency IDs | Currency plus network, for example BTC, USDT-TRC20. Not case sensitive in requests; always upper case in responses. |
| Times | ISO 8601 in UTC, for example 2026-10-05T12:00:00.000Z. |
| Browsers | CORS is open (Access-Control-Allow-Origin: *), so you can call the API from a web page. No cookies are used. |
| Tor | Also available over Tor at http://swapnoqyyp3mfh3hoypzmvpnau7gymt7c32tybx2qew3s7sw7bzz7aqd.onion/api/v1, with the same endpoints and limits. |
| Versioning | The version is in the path. Within v1 we only add things: new endpoints, new optional parameters, new response fields and new currencies, so ignore fields you don’t know. Anything that would break an integration ships as a new version, and v1 keeps working alongside it. |
Authentication
None. There are no API keys, accounts or tokens, and requests are not linked to anyone. Never send credentials, seed phrases or private keys to the API; it will never ask for them.
Rate limits
Limits are counted per connection over a rolling minute.
| Endpoint | Requests per minute |
|---|---|
GET /currencies | 60 |
GET /estimate | 90 |
GET /orders/{id} | 30 |
Every response carries the standard RateLimit-Policy (for example 90;w=60) and RateLimit (for example limit=90, remaining=89, reset=60) headers. Over the limit you get 429 with a Retry-After header in seconds.
- Cache
/currenciesfor at least 5 minutes; it rarely changes. - While a user types an amount, wait about half a second after the last keystroke before asking for an estimate.
- Poll an order at most every 10 seconds, and stop when
finalistrue.
Errors
Errors use the standard RFC 9457 problem details format. Check the HTTP status, then branch on code, which never changes. detail is a plain-English message you can show to your user, and type links to the matching row below.
{
"type": "https://swapnoir.com/api#error-amount_too_small",
"title": "Amount below minimum",
"status": 422,
"detail": "The minimum for this pair is 0.00005799 BTC.",
"code": "amount_too_small",
"min_amount": "0.00005799"
}| Code | HTTP | Meaning |
|---|---|---|
invalid_parameter | 400 | A query parameter is missing or malformed. param names it. |
same_currency | 400 | from and to are the same currency. |
receive_not_supported | 400 | The to currency is send only (Lightning). |
amount_too_small | 422 | The amount is below the minimum for this pair. min_amount holds the minimum. |
pair_not_supported | 422 | This pair of currencies and networks is not offered. |
pair_unavailable | 503 | No rate is available for this pair right now. Retry later or try another amount. |
not_found | 404 | No order with this ID, or the order has been deleted. |
rate_limited | 429 | You hit the rate limit. Wait the number of seconds in Retry-After. |
internal_error | 500 | Something went wrong on our side. Retry later. |
Reference
List currencies
GET/api/v1/currencies
Every currency and network Swapnoir supports. A token on another network is a separate currency, for example USDT-TRC20 and USDT-ERC20. The list changes rarely; cache it for a few minutes.
The example shows three of the currencies. Response fields below are per currency.
Request
curl "https://swapnoir.com/api/v1/currencies"const res = await fetch("https://swapnoir.com/api/v1/currencies");
const { currencies } = await res.json();
// Currencies a user can receive
const receivable = currencies.filter((c) => c.receive);import requests
r = requests.get("https://swapnoir.com/api/v1/currencies", timeout=10)
r.raise_for_status()
currencies = r.json()["currencies"]Response
{
"currencies": [
{
"id": "BTC",
"ticker": "BTC",
"name": "Bitcoin",
"network": "Bitcoin",
"decimals": 8,
"send": true,
"receive": true,
"memo": null,
"icon_url": "https://swapnoir.com/assets/coins/btc.svg"
},
{
"id": "BTC-LN",
"ticker": "BTC",
"name": "Bitcoin",
"network": "Lightning",
"decimals": 8,
"send": true,
"receive": false,
"memo": null,
"icon_url": "https://swapnoir.com/assets/coins/btc.svg"
},
{
"id": "XRP",
"ticker": "XRP",
"name": "XRP",
"network": "XRP Ledger",
"decimals": 6,
"send": true,
"receive": true,
"memo": "Destination tag",
"icon_url": "https://swapnoir.com/assets/coins/xrp.svg"
}
]
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Currency and network. |
ticker | string | |
name | string | |
network | string | |
decimals | integer | Maximum decimals accepted in amounts. |
send | boolean | Can be used as from. |
receive | boolean | Can be used as to. |
memo | string or null | Name of the extra field some networks need with the address, such as Destination tag; null when not used. |
icon_url | string |
Get an estimate
GET/api/v1/estimate
How much of to the user receives for amount of from, with all exchange and network fees for the payout already included. Estimates are live and are not reserved: the final amount is set when the exchange is created (fixed rate) or when the deposit is confirmed (floating rate).
Parameters
| Name | In | Description |
|---|---|---|
fromrequired | query | Currency the user sends. Example: BTC. |
torequired | query | Currency the user receives. BTC-LN is send only. Example: XMR. |
amountrequired | query | Amount of from, as a decimal with a dot and at most the currency’s decimals. Example: 0.01. |
rate | query | float (default) follows the market until the deposit is confirmed. fixed locks the amount when the exchange is created. One of float, fixed. Example: float. |
Request
curl "https://swapnoir.com/api/v1/estimate?from=BTC&to=XMR&amount=0.01&rate=float"const params = new URLSearchParams({ from: "BTC", to: "XMR", amount: "0.01" });
const res = await fetch(`https://swapnoir.com/api/v1/estimate?${params}`);
const data = await res.json();
if (!res.ok) {
// RFC 9457 problem, e.g. data.code === "amount_too_small"
throw new Error(data.detail);
}
console.log(data.amount_to, data.to);import requests
r = requests.get(
"https://swapnoir.com/api/v1/estimate",
params={"from": "BTC", "to": "XMR", "amount": "0.01"},
timeout=10,
)
data = r.json()
if not r.ok:
raise RuntimeError(data["code"] + ": " + data["detail"])
print(data["amount_to"], data["to"])Response
{
"from": "BTC",
"to": "XMR",
"rate_type": "float",
"amount_from": "0.01",
"amount_to": "1.57166029",
"rate": "157.166029",
"usd_value": "866.32",
"quoted_at": "2026-10-05T12:00:00.000Z"
}{
"type": "https://swapnoir.com/api#error-amount_too_small",
"title": "Amount below minimum",
"status": 422,
"detail": "The minimum for this pair is 0.00005799 BTC.",
"code": "amount_too_small",
"min_amount": "0.00005799"
}{
"type": "https://swapnoir.com/api#error-invalid_parameter",
"title": "Invalid parameter",
"status": 400,
"detail": "Unknown currency `DOG`. See /api/v1/currencies.",
"code": "invalid_parameter",
"param": "from"
}Response fields
| Field | Type | Description |
|---|---|---|
from | string | Currency and network. |
to | string | Any currency except send-only ones. |
rate_type | string | Rate type |
amount_from | string | The amount you asked about. |
amount_to | string | What the user receives, after all fees. |
rate | string | Amount of to per 1 from, after fees. |
usd_value | string or null | Approximate US dollar value of amount_from, when known. |
quoted_at | string |
Get an order
GET/api/v1/orders/{id}
Live status and details of an exchange. The order ID is the last part of the order page URL. Anyone with the ID can see the order, so treat it like a password. Poll at most every 10 seconds and stop when final is true. Orders are deleted 7 days after they finish.
Parameters
| Name | In | Description |
|---|---|---|
idrequired | path | Order ID, for example K7M2QX9PRT4B. |
Request
curl "https://swapnoir.com/api/v1/orders/K7M2QX9PRT4B"const res = await fetch(`https://swapnoir.com/api/v1/orders/${orderId}`);
if (res.status === 404) {
// wrong ID, or the order was deleted
}
const order = await res.json();
console.log(order.status, order.final);import time
import requests
while True:
r = requests.get(f"https://swapnoir.com/api/v1/orders/{order_id}", timeout=10)
if r.status_code == 404:
break # wrong ID, or the order was deleted
order = r.json()
print(order["status"])
if order["final"]:
break
time.sleep(15)Response
{
"id": "K7M2QX9PRT4B",
"status": "confirming",
"final": false,
"rate_type": "float",
"created_at": "2026-10-05T12:00:00.000Z",
"updated_at": "2026-10-05T12:06:12.000Z",
"from": {
"currency": "BTC",
"amount": "0.01"
},
"to": {
"currency": "XMR",
"amount": "1.57166029",
"amount_is_final": false
},
"deposit": {
"address": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
"memo": null,
"expires_at": null
},
"payout": {
"address": "888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H",
"memo": null
},
"refund": null,
"transactions": {
"deposit": {
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"url": "https://mempool.space/tx/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"payout": null
},
"links": {
"order_page": "https://swapnoir.com/order/K7M2QX9PRT4B",
"receipt": "https://swapnoir.com/order/K7M2QX9PRT4B/receipt.txt"
}
}{
"type": "https://swapnoir.com/api#error-not_found",
"title": "Not found",
"status": 404,
"detail": "No order with this ID. Orders are deleted 7 days after they finish.",
"code": "not_found"
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Order ID |
status | string | Order status |
final | boolean | True for done, expired, refunded and failed: stop polling. |
rate_type | string | Rate type |
created_at | string | |
updated_at | string | |
from | object | |
from.currency | string | Currency and network. |
from.amount | string | Amount to deposit. |
to | object | |
to.currency | string | Currency and network. |
to.amount | string or null | Estimated payout, or the final payout once amount_is_final is true. |
to.amount_is_final | boolean | |
deposit | object | |
deposit.address | string or null | Where the user sends the deposit; null while the status is preparing. |
deposit.memo | string or null | Memo or destination tag that must be included with the deposit, when set. |
deposit.expires_at | string or null | Send before this time, when set. |
payout | object | |
payout.address | string | |
payout.memo | string or null | |
refund | null or object | |
refund.address | string | |
refund.memo | string or null | |
transactions | object | |
transactions.deposit | null or object | |
transactions.deposit.hash | string | |
transactions.deposit.url | string or null | Block explorer link, when one is known. |
transactions.payout | null or object | |
transactions.payout.hash | string | |
transactions.payout.url | string or null | Block explorer link, when one is known. |
links | object | |
links.order_page | string | |
links.receipt | string |
Guides
Create an exchange
Exchanges are created on swapnoir.com, so your user always gets their own order page and keeps control of it. Your app sends the user there with the coins and amount already filled in:
| Link | Opens |
|---|---|
https://swapnoir.com/exchange?from=BTC&to=XMR&amount=0.01&rate=float | The exchange at the address step, with the live estimate shown. Best for “Exchange now” buttons. |
https://swapnoir.com/?from=BTC&to=XMR&amount=0.01 | The home page with the exchange box filled in, so the user can still change coins and amount. |
Both take the same from, to, amount and optional rate as Get an estimate. To open the page in another language, put the language code first, for example /de/exchange?….
<a href="https://swapnoir.com/exchange?from=BTC&to=XMR&amount=0.01&rate=float" rel="noopener">
Exchange BTC to XMR on Swapnoir
</a>- The user pastes their receiving address and an optional refund address, passes a quick security check, and presses Exchange now.
- They land on their order page,
https://swapnoir.com/order/<ID>, with the deposit address and amount. - If your app should follow the order, ask the user to paste the order link or ID, then poll Get an order.
Order statuses
A normal exchange moves through awaiting → confirming → exchanging → sending → done. Show status to your user in your own words and stop polling once final is true.
| Status | Final | Meaning |
|---|---|---|
preparing | No | The deposit address is being prepared. It usually appears within minutes. |
awaiting | No | Waiting for the deposit. Send only while the order is in this status and before deposit.expires_at. |
confirming | No | The deposit was seen and is waiting for network confirmations. |
exchanging | No | The deposit is confirmed and is being exchanged. |
sending | No | The exchanged coins are being sent to the payout address. |
processing | No | In progress. Used when a more specific status is not known. |
done | Yes | Complete. The payout was sent; see transactions.payout. |
expired | Yes | No deposit arrived in time. A late deposit can still be completed or refunded through support. |
hold | No | Paused for a routine review. The user should contact support with the order ID. |
attention | No | Something needs checking, for example a deposit of the wrong amount. The user should contact support. |
refunding | No | The deposit is being returned to the refund address. |
refunded | Yes | The deposit was returned. |
failed | Yes | The exchange could not be completed. The user should contact support. |
Supported currencies
The same list List currencies returns. New currencies are added over time, so read the list from the API instead of hard-coding it.
| ID | Currency | Network | Decimals | Receive | Memo |
|---|---|---|---|---|---|
BTC | Bitcoin | 8 | Yes | – | |
XMR | Monero | 12 | Yes | – | |
ETH | Ethereum | 8 | Yes | – | |
USDT-TRC20 | Tron (TRC20) | 6 | Yes | – | |
USDT-ERC20 | Ethereum (ERC20) | 6 | Yes | – | |
USDT-SOL | Solana | 6 | Yes | – | |
USDC-ERC20 | Ethereum (ERC20) | 6 | Yes | – | |
LTC | Litecoin | 8 | Yes | – | |
SOL | Solana | 9 | Yes | – | |
BTC-LN | Lightning | 8 | Send only | – | |
TRX | Tron | 6 | Yes | – | |
BNB | BNB Smart Chain (BEP20) | 8 | Yes | – | |
DOGE | Dogecoin | 8 | Yes | – | |
ZEC | Zcash | 8 | Yes | – | |
BCH | Bitcoin Cash | 8 | Yes | – | |
DASH | Dash | 8 | Yes | – | |
XRP | XRP Ledger | 6 | Yes | Destination tag |
Integration checklist
- Show the networkAlways show the network next to the coin, e.g. USDT on Tron. Sending on the wrong network can lose funds.
- Show the memoWhen
deposit.memois set, the deposit must include it. Display it as clearly as the address. - Explain fixed ratesA fixed-rate exchange pays the quoted amount only if the user sends exactly
from.amountbeforedeposit.expires_at. - Keep order IDs privateAnyone with an ID can see that order. Don’t put IDs in analytics, logs or public URLs.
- Handle errors by codeBranch on
code, showdetail, and respectRetry-After. - Link to the real siteOnly link to https://swapnoir.com. Never ask users for seed phrases or private keys.
Changelog
- v1 released.
GET /currencies,GET /estimate,GET /orders/{id}, RFC 9457 errors,RateLimitheaders and the OpenAPI 3.1 description.
Support
Questions, ideas or higher limits for your integration: send us a message or reach us at info@swapnoir.com. Use of the API is covered by our Terms and Privacy pages.
Last updated .