Exchange API

Build against the same exchange API used by SwapSS. Sign in to the cabinet to enroll, create a sandbox key, and test quotes and orders before requesting live access.

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 integration: keys and scopes, webhook delivery history, an order ledger, and withdrawals. Enrollment and key management require a signed-in SwapSS account.
  • We do not market to your users. Where applicable, they may receive transactional updates about their orders.
  • 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.
  • Live access is reviewed before it is enabled. Ask SwapSS support what information is needed for your integration.
  • 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.

LineWith SwapSSTypical 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

Use the cabinet for enrollment and keys. Sandbox access is for testing; live access requires review and a separate key.

  1. Sign in and enroll Sign in to your SwapSS account, open the API cabinet, and enroll as an integrator. Enrollment starts with sandbox access; it does not enable live orders.
  2. Test with a sandbox key Create a sandbox key, request a quote, then create a test order and advance it with the simulation endpoint. Sandbox orders use the real API shape but do not request payment or move funds.
  3. Request review, then create a live key When testing is complete, contact SwapSS support to request an account review for live access. An operator must approve the account. Then create a new live key in the cabinet; your sandbox key remains sandbox-only.

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

MethodPathWhat it returns
POST/orders/{publicId}/simulateSandbox only. Send {"status":"confirming_deposit"} to advance a test order. Continue through exchanging, sending and completed; no payment is needed.
GET/healthLiveness. Use it as the availability check before offering a swap.
GET/assetsEvery 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=liveLive 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/availabilityWhether 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/quoteA binding quote: amounts, fees, limits, the refund-address policy for that route, and an expiry. Returns a quoteId the order call must use.
POST/ordersCreates 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/ordersThe 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?

Public quote and order calls can be made without an API key. Partner enrollment and key management are different: they require a signed-in SwapSS account. Enroll in the cabinet for a sandbox key; live access requires review and a newly created live key.

Is there a sandbox?

Yes. Enroll from your signed-in cabinet and create a sandbox key. Use it for a quote, test order, status reads and POST /orders/{publicId}/simulate. Test orders start with SS-TEST-, require no payment and last 24 hours. After review and account promotion, create a separate live key for real exchanges; the sandbox key remains sandbox-only.

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?

Sandbox enrollment is self-serve after you sign in. Live access is reviewed separately; approval is not automatic, and it allows you to create a new live key rather than changing your sandbox key.

Payload samples on this page are real responses captured on 24 July 2026. Values that change with configuration — fees, limits, supported assets — should be read from the API, not copied from here.