Exchange API
A crypto-to-crypto swap API you can call without an API key, an account, or a signup form. Quote a pair, create the order, poll its status. The same endpoints the SwapSS site itself runs on.
https://swapss.lol/api/v1
What you earn
A key is optional for trading and mandatory for earning — it is what attributes an order to you. Everything below is granted the moment you join, except the markup ceiling: that one is a number a person sets for your account.
- You earn 30% of our own service fee on every order you send, whatever you charge your users. Elsewhere an affiliate is paid a cut of their own markup and nothing else, so passing the raw rate through pays them zero. Here it does not work that way.
- On top of that, your own markup. You set it in the cabinet, you keep every basis point of it, and the ceiling we can open reaches 4.5% of the trade. It is granted per account rather than at signup, because the number has to fit inside what the route can actually charge — tell us what you are building and we open it.
- One cabinet for the whole thing: keys and their scopes, the markup, webhook endpoints with every delivery attempt and its response code, a per-order ledger, and withdrawals. None of it needs an email to us, and there is no account manager to chase.
- Your users stay yours. We never contact them, never market to them and never put our name in front of them.
- No minimum payout. The common floor in this market is 300 USDT; ours is none.
- The network fee is passed through at cost and shown before you confirm. We never take a percentage of it.
- No business verification to get a key, to earn, or to withdraw.
- Free to use. No setup fee, no monthly fee, no volume commitment.
Two things we would rather put here than have you find in a balance that does not move. Your share is paid out of the service fee actually recorded on that order and becomes withdrawable once that fee has been collected — it is tied to a real accrual, not to a timer, and an order that never settles never accrues. And a markup rides inside what a route charges, so it applies on the routes that can carry one; where a route cannot, the order goes out at our raw rate and you are paid the share instead. GET /reports/orders says which happened, order by order, with the arithmetic.
A worked example
A $1,000 order with a 2.5% markup of your own. The markup is yours in full and it does not replace the share — the two are added, and that is the whole structural difference. Set the markup to zero and the left column still pays $1.50 while the right one pays nothing at all.
| Line | With SwapSS | Typical elsewhere |
|---|---|---|
| Order size | $1,000.00 | $1,000.00 |
| Your markup, 2.5% | $25.00 | $25.00 |
| Our service fee on that order, 0.5% | $5.00 | $5.00 |
| Your share of it, 30% | $1.50 | $0.00 |
| You receive | $26.50 | $25.00 |
Those are the numbers the accounting code computes, not a marketing rounding: the split is integer arithmetic on the order commission, and a test pins $26.50 and $1.50 exactly so this table cannot drift away from what we actually pay.
Start in three steps
None of this needs a sales conversation, and none of it has to be redone when you go live.
- Join Open your API cabinet and join. One click, no application waiting to be reviewed, and your 30% share of the fee is granted on the spot.
- Create a key Label it, keep the default scopes, optionally fence it to your server IPs. The secret is shown exactly once — we keep only a one-way digest of it, so there is nothing to look up later, by us or by anyone who reaches our database.
- Send your first quote The same endpoints as the rest of this page; the key only attributes the call to you. A person opens live access when you are ready, and nothing about the integration changes when they do.
First authenticated request
curl -X POST https://swapss.lol/api/v1/quote \
-H 'authorization: Bearer ssx_sec_test_YOUR_KEY' \
-H 'content-type: application/json' \
-d '{
"fromChain": "ethereum",
"fromAsset": "USDT",
"toChain": "monero",
"toAsset": "XMR",
"amount": "500",
"rateType": "float"
}' Open your API cabinet You will be asked to sign in first.
Endpoints
| Method | Path | What it returns |
|---|---|---|
| GET | /health | Liveness. Use it as the availability check before offering a swap. |
| GET | /assets | Every asset with its chain, decimals, whether it can be a source, a destination, or both, and a wide per-asset bound. That bound is not the trading minimum: the amount a swap accepts depends on the pair, and the authoritative range comes back with the quote. |
| GET | /networks?scope=live | Live networks with block time, required confirmations and explorer base URL — enough to show an honest wait estimate before a deposit. |
| GET | /pairs?from_chain=&from_asset= | Every destination reachable from one source asset. Ask this instead of assuming a pair exists. |
| GET | /indicative?amount= | ?amount_to= | Display-only rate for a pair, cached briefly. Pass amount to estimate the output, or amount_to to estimate the input needed — both directions work on every pair here, because it is arithmetic on the cached rate rather than a fresh call. Carries no quote token and cannot create an order: use it to fill a rate field, never to settle one. Send your API key here too: an unauthenticated call is the public feed, priced and rate-limited like any visitor, while an authenticated one is priced on your own terms and metered against your key instead of your address. |
| GET | /availability | Whether we are accepting exchanges right now, which rate types are open, and whether one pair is servable. Not /health: that reports the process is alive, which stays true while intake is paused. |
| POST | /quote | A binding quote: amounts, fees, limits, the refund-address policy for that route, and an expiry. Returns a quoteId the order call must use. |
| POST | /orders | Creates the order from a live quoteId. The server never recomputes amounts from your fields — an expired or unknown quote is refused rather than re-priced. |
| GET | /reports/orders | The orders your API key created, newest first, cursor-paginated, with expected and actual amounts kept separate. The one endpoint that requires a key: it is how you reconcile without polling every order. |
| GET | /orders/{publicId} | Order status, timeline, confirmation progress and transaction links. Send your key and you read your own orders by id alone. Without a key, pass the per-order secret issued at creation as ?s=. |
Quoting a pair
A quote is the only thing an order can be built from. It carries the amounts, the limits, the refund-address policy for that route, and an expiry — order creation binds the quoteId and refuses to re-price from client fields.
Request
curl -X POST https://swapss.lol/api/v1/quote \
-H 'content-type: application/json' \
-d '{
"fromChain": "bitcoin",
"fromAsset": "BTC",
"toChain": "monero",
"toAsset": "XMR",
"amount": "0.01",
"rateType": "float"
}' Response
{
"quoteId": "qt_Eq-_r6he8u5SLNur7yffJcc5ibP-9T0igoOyVcW1xl8",
"rateType": "float",
"from": { "chain": "bitcoin", "asset": "BTC", "amount": "0.01" },
"to": { "chain": "monero", "asset": "XMR", "amount": "1.72765483" },
"fees": {
"serviceFee": "0", "serviceFeeAsset": "BTC", "serviceFeeBps": 0,
"networkFee": "0", "networkFeeAsset": "BTC",
"exchangeFee": "0", "exchangeFeeAsset": "USD"
},
"limits": { "min": "0.0001", "max": "6.814" },
"expiresAt": "2026-07-24T19:44:28Z",
"warnings": ["Final amount is determined after deposit confirmations"],
"exchangePreview": { "label": "SwapSS", "priceImpactBps": 0, "etaSeconds": 1800 },
"startMethod": "deposit",
"requiresConnectedWallet": false,
"sourceAddressRequired": false,
"requiresRefundAddress": false,
"refundAddressPolicy": "optional",
"requiresRecipientMemo": false,
"requiresRefundMemo": false,
"supportsRecipientMemo": false,
"supportsRefundMemo": false
}Creating the order
Creates the order from a live quoteId. The server never recomputes amounts from your fields — an expired or unknown quote is refused rather than re-priced.
Request
# Seven fields are required. The five asset fields and the amount are not
# re-priced — they are checked against the quote and must match it exactly,
# so echo them back from the quote response.
# Idempotency-Key is required. Sending the same one again replays the same
# order instead of creating a second one.
# Send the key on BOTH calls. A quote priced with your key carries your
# markup; if you then create the order without the key, the customer still
# pays that markup and the order is attributed to nobody — you earn nothing
# on it, and it cannot be repaired afterwards.
curl -X POST https://swapss.lol/api/v1/orders \
-H 'content-type: application/json' \
-H 'authorization: Bearer ssx_sec_live_...' \
-H 'idempotency-key: 5f1c9a3e-2b47-4a1d-9c8e-7d0b6a4f2e11' \
-d '{
"quoteId": "qt_Eq-_r6he8u5SLNur7yffJcc5ibP-9T0igoOyVcW1xl8",
"fromChain": "bitcoin",
"fromAsset": "BTC",
"toChain": "monero",
"toAsset": "XMR",
"amount": "0.01",
"recipientAddress": "4AdUndXHHZ6cfufTMvppY6JwXNouMBzSkbLYfpAV5Usx3skxNgYeYTRj5UzqtReoS44qo9mtmXCqY45DJ852K5Jv2684Rge"
}'
# Optional, and driven by the quote — not guessed per chain:
# refundAddress when requiresRefundAddress / refundAddressPolicy says so
# sourceAddress when sourceAddressRequired is true
# recipientExtraId when requiresRecipientMemo is true (memo/tag networks)
# refundExtraId when requiresRefundMemo is true
# rateType if sent, must equal the quote's
# contactEmail, telegramChatId
#
# Returns publicId, status, expectedAmount, expiresAt, statusPageUrl, and
# depositAddress with depositMemo where the route has one.
#
# depositAddress can be ABSENT on the first response. On some routes it is
# generated for your order and arrives moments later; until then the order
# sits in status "created". Treat a missing depositAddress as "not yet":
# poll GET /orders/{publicId} and show the customer an address only once
# status is "awaiting_deposit". Never invent or reuse one.Order webhooks
Register an endpoint in your cabinet and we POST every status change to it, signed. A webhook is a notification, never a step in the order: if your endpoint is down for a day the order still completes, and its final state is still readable from the API. These headers are the exchange API's own — the Pay product signs with a different set of names.
# The body. Amounts are integer minor units as STRINGS, with decimals
# alongside, because 18-decimal assets overflow what most JSON parsers
# hold. finalAmountMinor is null until the payout has actually gone out.
{
"id": "0f1e2d3c-4b5a-6978-8796-a5b4c3d2e1f0",
"type": "order.status",
"createdAt": "2026-08-31T20:19:21Z",
"data": {
"publicId": "SS-GB6MDJRWM53Z71",
"status": "awaiting_deposit",
"from": { "chain": "bitcoin", "asset": "BTC", "amountMinor": "1000000", "decimals": 8 },
"to": { "chain": "monero", "asset": "XMR", "amountMinor": "1487000000000", "decimals": 12 },
"finalAmountMinor": null
}
}
# Every delivery carries these four headers.
SwapSS-Signature: t=1788206400,v1=<64 lowercase hex>
SwapSS-Event-Id: 0f1e2d3c-4b5a-6978-8796-a5b4c3d2e1f0
SwapSS-Event-Type: order.status
SwapSS-Delivery-Id: 9a8b7c6d-5e4f-3021-1234-56789abcdef0
# The signed pre-image is the timestamp, a dot, and the raw body bytes.
# Verify over the bytes you received: re-serialising a parsed object changes
# whitespace and key order, and the signature will not match.
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
p = dict(kv.split("=", 1) for kv in header.split(","))
if abs(time.time() - int(p["t"])) > tolerance:
return False
expected = hmac.new(
secret.encode(), p["t"].encode() + b"." + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, p["v1"])
# The secret is the one shown once when you register the endpoint; it starts
# with ssx_whsec_. t is stamped when we send, not when we queue, so a delivery
# that waited hours in the retry queue still arrives inside your window.
# Deliveries are at-least-once and can arrive out of order: dedupe on the event
# id, and order your own processing by the status, not by arrival.
# Retries: 1m, 5m, 30m, 2h, 6h, 24h. Twenty consecutive failures pause the
# endpoint until you re-enable it in the cabinet.Knowing the wait before the deposit
The network registry reports block time and required confirmations per chain, so an integration can state the real wait instead of a generic "a few minutes". Bitcoin below is 3 confirmations at a 600-second block time — about 30 minutes.
{
"id": "bitcoin",
"name": "Bitcoin",
"nativeCurrency": "BTC",
"blockTimeSeconds": 600,
"confirmationsRequired": 3,
"explorerUrl": "https://mempool.space",
"status": "active",
"chainType": "utxo"
}What the cabinet actually does
Everything an integration needs in order to run itself, in one place. The word "convenient" is doing no work in that sentence — this list is:
- Keys: create, label, scope, fence to your server IPs, rotate, revoke. The secret appears once; after that even we hold only a digest of it.
- Markup: one field. It applies from the next quote, not from a support ticket.
- Webhooks: register an endpoint, then see every delivery attempt with its response code and timing, and replay the ones that failed. You never have to guess whether we called you.
- Ledger: every order you sent, what it accrued, in which asset, and whether that accrual is still pending or withdrawable. The arithmetic is shown per order rather than summarised into a monthly figure.
- Withdrawals: request one against the balance and watch it move. No floor to reach first, no monthly window to wait for.
Rates and fees
Two rate types, and the current values are always readable from GET /fees rather than hardcoded:
- Floating — 0.5%. The final amount settles at the rate when the deposit confirms.
- Fixed — 1%. The amount is locked when the order is created; the order carries the deadline by which the deposit must arrive.
- The network fee is quoted before confirmation. Nothing is deducted that a quote did not show.
What the integration is responsible for
- Treat every amount as a decimal string. Amounts are integers in minor units internally and are never floats on any money path.
- Never assume a pair exists: read it from /pairs, and let a 422 disable that direction rather than retry it.
- Order statuses are one fixed vocabulary — waiting_deposit, confirming_deposit, checking, exchanging, sending, completed, action_required, review, refunding, refunded, expired, cancelled, failed. Internal state names are never exposed. Treat a status you do not recognise as non-terminal and keep polling: new ones are added additively and an unknown one never means the order is over.
- review is not a failure. It means the order needs attention before it continues — an automatic retry or an operator check — and telling a user their funds are lost there is the worst thing an integration can do. A refund is neither automatic nor instant, so do not promise your users a refund window; the one that binds us is in the Terms.
- A quote expires. Re-quote instead of holding one; order creation on an expired quote is refused, not silently re-priced.
- Show chain, asset and address together wherever a user can copy or confirm. A correct address on the wrong chain loses the funds.
Limits and failure
- Endpoints are rate limited per family; a 429 carries a retry hint and is not a reason to fail the swap.
- 503 means the pair cannot be served right now. Fail closed and hide the direction — an exchange that quotes what it cannot execute is worse than one that says no.
- Errors are JSON with a human-readable message and never leak internals.
- Every error carries a stable code beside that message: INVALID_PARAMETER, AMOUNT_BELOW_MINIMUM, AMOUNT_ABOVE_MAXIMUM, AMOUNT_OUTSIDE_LIMITS, PAIR_UNAVAILABLE, RATE_LIMITED, UPSTREAM_UNAVAILABLE, INTERNAL_ERROR, REQUEST_REJECTED. Branch on the code, show the message. Once a code is published it is never renamed or given a new meaning — new situations get new codes — so mapping them into your own states is safe to do once.
- An amount rejection hands you the bounds as data, not only inside the sentence: limits.min, limits.max and limits.asset, always expressed in the source asset. AMOUNT_OUTSIDE_LIMITS rather than a specific edge means the side genuinely could not be determined — a reverse quote prices the destination while the bounds are source-side — so show the range instead of telling the user which way to move.
- limits.destination repeats those bounds in the destination asset, for the case where the user priced that side. It is marked estimated because it comes from a recently cached rate, and it is absent rather than stale when no rate is known. The minimum rounds up and the maximum rounds down, so acting on either edge cannot land you outside the real range.
- A 400 means we could not read the request; a 200 carrying available:false means we read it and cannot serve that pair. They are different answers on purpose — a misspelled parameter used to look like a permanently dead pair. A 400 names the parameter and carries a code.
- Query parameters are accepted in both snake_case and camelCase, so from_chain and fromChain both work.
- A chain can be named either by our id (ethereum, base, tron) or, for EVM chains, by the EIP-155 number your wallet already carries (1, 8453, 137). /availability echoes back the resolved id, which is the fastest way to learn the spelling the quote request wants.
- Indicative amounts are estimated from the cached rate, not from a fresh call, so you can ask on every keystroke without being rate limited. They are a preview: only a quote binds a number, and amountWithinLimits tells you whether the trade would be accepted before you ask for one.
- On an indicative response, available is always true and maxFrom is an empty string when the pair has no declared ceiling — unbounded, not zero. A pair we cannot serve answers available:false instead, so one field decides it either way.
Wallets and aggregators
If you are integrating SwapSS into a wallet, a rate comparison site, or a swap widget, get in touch before you start. We will confirm the pair coverage you need, raise the rate limits for your traffic, and answer route questions directly rather than through a form.
Integration questions: @swappsy
Questions integrators ask first
Do I need an API key or an account?
- Not to trade. Quoting and order creation are unauthenticated, and reading an order back requires the token issued when that order was created, so an order id alone never exposes someone else’s swap. A key is optional: it attributes the orders it creates, lets you read them back without holding each token, and is the only way to reach the reports endpoint.
Is there a sandbox?
- Your first key is labelled sandbox. It quotes and reads against the real exchange, at a lower rate limit. It cannot create orders. Build and test everything else against it; a person opens live access when you are ready, and the same code then creates real orders.
Do I have to poll for status, or can you call me?
- We call you. Register an endpoint in your cabinet and you get an order-status callback signed with a secret shown to you once, carrying the event id, the event type and a delivery id. Failures are retried six times with a widening gap, every attempt is listed in the cabinet with its response code, and a delivery left hanging by a restart is picked back up rather than lost. Polling still works if you prefer it.
Which assets are supported?
- Read GET /assets rather than trusting a list on a page. As of this writing 21 networks are live, including Monero and Zcash in both directions.
What happens if a deposit arrives late or in the wrong amount?
- The order is not silently dropped. A late or partial deposit on a fixed rate moves to the floating rate at confirmation time, and anything genuinely ambiguous goes to human review with the funds preserved rather than to an automatic guess.
Can I list SwapSS rates in an aggregator?
- Yes. Indicative rates are display-only, cached briefly, and carry no quote token, which makes them the right feed for a comparison table. Message us before you build so we can size the rate limits for your traffic.
What does it cost to integrate?
- Nothing. There is no setup fee, no monthly fee and no volume commitment, and there is no minimum you have to reach before you can withdraw what you have earned. The network fee on a payout is passed through at cost.
How can I earn if I charge my users nothing extra?
- Because your share is a share of our service fee, not a slice of something you added on top. Your users get our raw rate and you still earn 30% of the fee on every order you send. A programme that only pays you a cut of your own markup pays you nothing in that case.
Do I have to verify my business to get a key?
- No. Enrolling is self-serve and immediate, and it needs no company documents to get a key, to earn, or to withdraw. You start in sandbox — real endpoints, no real money — and a person opens live access when you are ready.