API reference

SwapZilla Partner API

Put non-custodial crypto exchange inside your own product: one JSON API quotes every connected exchange, opens orders and private swaps, and reports their status through to settlement.

  • Base URLhttps://api.swapzilla.io
  • Prefix/v1/partner
  • AuthX-API-Key: <key>
  • FormatJSON · UTF-8 · SSE

Overview

The partner API gives a third-party service — a wallet, a bot, a widget, an aggregator — the whole SwapZilla exchange under its own brand. One request quotes every connected exchange provider at once; one more opens the order. The swap itself stays non-custodial: the user's coins never touch SwapZilla, they go straight to the provider's deposit address and the provider pays out to the address you supply.

Every call made with your key is tagged with your PartnerID, and you only ever see your own orders.

What partners build with it — payment links and invoicing, crypto acquiring, payroll and multi-send payouts, an MCP server for AI agents, streaming rates — is laid out endpoint by endpoint under /solutions/.

Base URLhttps://api.swapzilla.io — the same service is reachable at https://swapzilla.io/api, but integrate against the API host
Prefix/v1/partner for everything that needs a key
AuthX-API-Key: <your key> on every request
Content typeJSON in, JSON out; text/event-stream for the two streaming endpoints
Rate limitsnone on our side — but providers throttle repeated quote requests, so cache for 5–10 seconds
Test networknot available in production; ask us to enable the mock provider if you need deterministic smoke tests

Getting a key

Register at swapzilla.io/partners/ and the key is issued immediately — or ask a SwapZilla admin, who can issue one by hand. Either way you end up with two values: your partner id (a UUID) and the api_key itself — about 43 characters of base64url, e.g. BmPmSIZkY7d_JBmFBIPLC9N7V8I-gAS-7stbPlBQVvE.

A self-registered key starts inactive. It is yours and it is visible in your cabinet, but every /v1/partner/* request answers 403 with {"error":"api key is awaiting activation…"} until support switches it on. Write to @swapzilla_support_bot quoting the name and e-mail you registered with, and say in a sentence what you are building. A key an admin issued by hand works straight away.

Treat the key like a password. It is a bearer credential: whoever holds it trades as you. If it leaks, re-issue it — the button in your cabinet, or POST /v1/partner/key/regenerate authenticated with the key you are replacing. The old key stops working on the very next request, while orders already created keep their historical partner id.

GET /v1/partner/me key required

Confirms the key works and tells you who you are. The key itself is never echoed back.

curl -s https://api.swapzilla.io/v1/partner/me \
  -H "X-API-Key: $API_KEY"
{
  "ID": "12ec0f35-2f4a-4c0e-9f1b-7d0a9e5a1c33",
  "Name": "Acme Inc",
  "Enabled": true,
  "CreatedAt": "2026-05-04T12:59:13Z",
  "UpdatedAt": "2026-05-04T12:59:13Z"
}

A missing header answers 401 with {"error":"missing X-API-Key header"}, and a key we do not know answers 401 with {"error":"invalid api key"}. A key we do know but which is not allowed to work — not activated yet, rejected, or revoked — answers 403 and says which. The two codes mean different things: 401 is "check the key", 403 is "the key is right, write to support".

Keep the key on your server. The API answers with Access-Control-Allow-Origin: *, so a browser can call it directly — and then anyone can read your key out of DevTools and spend your reputation. Proxy partner calls through your own backend and add the header there.

Quickstart

Three calls take a user from "I want to swap" to a funded order.

  1. Quote the pair

    Ask for offers and pick one — usually the first, they come back sorted best-first.

    curl -sG "https://api.swapzilla.io/v1/partner/quotes" \
      -H "X-API-Key: $API_KEY" \
      --data-urlencode "from_asset=BTC" \
      --data-urlencode "from_network=BITCOIN" \
      --data-urlencode "to_asset=USDT" \
      --data-urlencode "to_network=TRC20" \
      --data-urlencode "amount_from=0.05"
  2. Create the order

    Send the offer's ID together with the user's destination address. You get back a DepositAddress.

    curl -s -X POST "https://api.swapzilla.io/v1/partner/orders" \
      -H "X-API-Key: $API_KEY" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: $(uuidgen)" \
      -d '{
        "offer_id": "0b0c8f0e-2c1a-4f3f-8f57-2a1d1c4e77aa",
        "to_address": "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
        "refund_address": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"
      }'
  3. Show the deposit address, then poll

    The user sends AmountFrom of the source asset to DepositAddress. From there, poll the order every 5–10 seconds until it reaches a terminal status.

    curl -s "https://api.swapzilla.io/v1/partner/orders/$ORDER_ID" \
      -H "X-API-Key: $API_KEY"

Offers expire after 5 minutes. Creating an order with a stale offer_id fails with 400 — re-quote and use the fresh offer. Rates move; that expiry is what keeps the number you showed the user honest.

Quotes

GET /v1/partner/quotes key required

Queries every enabled provider in parallel and returns their offers. A provider that errors or times out is simply absent from the list.

Query parameters

ParameterDescription
from_assetrequiredTicker of the source asset, e.g. BTC, USDT. Case-insensitive.
from_networkrequiredIts network. Common aliases are accepted: ERC20/ETH/ETHEREUM, TRC20/TRX, BEP20/BSC, BITCOIN/BTC.
to_assetrequiredTicker of the destination asset.
to_networkrequiredIts network.
amount_fromusuallyHow much the user sends, in from_asset units.
amount_tooptionalQuote by the amount the user should receive instead. Use one or the other.
rate_typeoptionalfloating (default) or fixed — the latter keeps only providers that lock the rate in.

Response

{
  "offers": [
    {
      "ID": "0b0c8f0e-2c1a-4f3f-8f57-2a1d1c4e77aa",
      "ProviderCode": "fixedfloat",
      "ProviderName": "FixedFloat",
      "RateType": "floating",
      "FromAsset": "BTC",
      "FromNetwork": "BTC",
      "ToAsset": "USDT",
      "ToNetwork": "TRX",
      "FromAmount": 0.05,
      "ToAmount": 3920.55,
      "AmountFromUsd": 3940.15,
      "AmountToUsd": 3919.57,
      "Price": 78411.08,
      "EstimatedMins": 6,
      "KycRating": "B",
      "DeviationPercent": 0,
      "MinFromAmount": 0.00064584,
      "MaxFromAmount": 7.10422758,
      "CreatedAt": "2026-08-18T09:12:44Z"
    }
  ]
}

Offer fields worth reading

FieldMeaning
IDWhat you pass as offer_id when creating the order. Valid for 5 minutes.
ProviderNameHuman-readable name — show this, not ProviderCode.
RateTypefloating follows the market until the deposit confirms; fixed is locked.
EstimatedMinsMedian completion time of that provider's last 100 successful orders.
KycRatingA (least intrusive) to D (aggressive KYC). Users deserve to see it before they commit.
DeviationPercentHow much worse than the best offer, in percent. 0 marks the best one.
AmountFromUsd / AmountToUsdUSD equivalents. Omitted when the price feed has no quote for the asset.
MinFromAmount / MaxFromAmountThe provider's limits for this pair, in FromAsset units. Omitted until the provider reports them. Validate the input against these before opening an order.
IsPaymentPresent on offers produced by a provider's payment (amount-to) endpoint. Nothing extra to do — the order flow is the same.

GET /v1/partner/quotes-sse key required

The same query, streamed. Offers arrive as each provider answers instead of after the slowest one — the first usually lands in 200–400 ms. This is what you want behind a live UI.

Takes the same parameters as /quotes, plus timeout_ms — capped at, and defaulting to, 5000 ms. The stream always ends by itself.

Events

EventData
offerOne offer object, identical to an entry of the offers array.
private_routeThe single best private route for the same pair, when amount_from was given. See private swaps.
provider_error{"provider":"…","error":"…"} — that provider dropped out. Log it, don't show it.
ping{"elapsed_ms":1000}, once a second, so proxies keep the connection open.
done{"total":7,"errors":1,"elapsed_ms":2140,"timed_out":false,"deviations":{"<offer_id>":0.16}}. The deviations map is the final ranking — apply it to the offers you already rendered.
// From your backend, which adds the key. In the browser, proxy this endpoint.
const res = await fetch(url, { headers: { "X-API-Key": key, Accept: "text/event-stream" } });
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
// … split on "\n\n", parse "event:" / "data:" lines, render each `offer` as it arrives

Orders

POST /v1/partner/orders key required

Turns an offer into a real order at the provider. Answers 201 with the full order.

Body

FieldDescription
offer_idrequiredThe ID of the chosen offer, at most 5 minutes old.
to_addressrequiredWhere the exchanged asset goes. Cannot be changed later.
refund_addressstrongly advisedWhere the source asset returns if the swap fails. Without it a refund may need manual work at the provider.
from_addressoptionalThe address the user pays from, when you know it.
client_order_idoptionalYour own identifier, stored with the order.
expected_to_amountoptionalSlippage guard: if the recalculated payout differs, the order is refused instead of created.
expected_from_amountoptionalThe amount you promised the user would send; stored as AmountFromExpected.
idempotency_keyoptionalSame meaning as the Idempotency-Key header, for clients that cannot set headers.

Response

{
  "ID": "7a0e2c31-4f9a-4b2e-9a77-9f0b2e5d1c02",
  "ProviderCode": "fixedfloat",
  "ProviderOrderID": "FF9K2LQ",
  "OfferID": "0b0c8f0e-2c1a-4f3f-8f57-2a1d1c4e77aa",
  "Status": "NEW",
  "FromAsset": "BTC",
  "ToAsset": "USDT",
  "FromNetwork": "BTC",
  "ToNetwork": "TRX",
  "DepositAddress": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
  "PayoutAddress": "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
  "RefundAddress": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
  "AmountFrom": 0.05,
  "AmountFromExpected": 0.05,
  "AmountToExpected": 3920.55,
  "AmountToReceived": 0,
  "RateType": "floating",
  "TxID": "",
  "PartnerID": "12ec0f35-2f4a-4c0e-9f1b-7d0a9e5a1c33",
  "CreatedAt": "2026-08-18T09:13:02Z",
  "UpdatedAt": "2026-08-18T09:13:02Z"
}

DepositAddress is the whole point of this response. Show it to the user with the exact AmountFrom and the network it must be sent on. Nothing is held by SwapZilla — the funds move from the user straight to the provider, and the provider pays out to PayoutAddress.

GET /v1/partner/orders/{id} key required

The current state of one of your orders. An order belonging to someone else answers 404, not 403.

We poll the provider every 10 seconds for every order that is not yet in a terminal status, so polling this endpoint every 5–10 seconds is enough — you never need to talk to the provider yourself.

GET /v1/partner/orders key required

Your orders, newest first. page defaults to 1, limit to 20 and cannot exceed 100.

curl -s "https://api.swapzilla.io/v1/partner/orders?page=1&limit=50" \
  -H "X-API-Key: $API_KEY"
{ "orders": [ /* … */ ], "page": 1, "limit": 50, "has_more": true }

POST /v1/partner/orders/{id}/refresh key required

Goes to the provider synchronously and returns the updated order — status, settled amounts and TxID. Useful right after the user says "I've sent it". Don't call it more than once every 5 seconds; the background poller is already doing this work.

Order lifecycle

NEWCONFIRMINGEXCHANGINGSENDINGDONE
StatusMeaning
NEWCreated; waiting for the user's deposit.in progress
WAIT_DEPOSITSame thing, confirmed explicitly by the provider.in progress
CONFIRMINGDeposit seen on-chain; waiting for confirmations.in progress
EXCHANGINGFunds confirmed; the swap is running.in progress
SENDINGProvider is paying out to PayoutAddress.in progress
DONESettled. AmountToReceived and TxID are final.terminal
TIME_EXPIREDThe deposit did not arrive in time.terminal
FAILEDThe exchange failed; ErrorMessage says what the provider reported.terminal
REFUNDEDFunds returned to RefundAddress.terminal

TxID is filled in when the provider broadcasts the payout — usually at SENDING, always by DONE. Stop polling once a status is terminal.

Log ProviderOrderID next to your own order id. It is the only reference a provider's support team will recognise if a swap needs chasing.

Private swaps

A private swap breaks the on-chain link between the sender and the receiver by routing through an anonymous intermediate asset — Monero by default. Under the hood it is two ordinary orders, chained so that the payout of the first is the deposit of the second, and tagged with one PrivateSwapID.

TRXProvider AXMRProvider BUSDT

Routes that would use the same provider for both legs are discarded — they would defeat the point.

GET /v1/partner/private-quotes key required

Same pair parameters as /quotes, plus max_routes (a positive integer). Returns {"routes": [ … ]}, each route carrying LegA and LegB — both ordinary offers — plus the route's own AmountTo, EstimatedMins, KycRating and DeviationPercent.

curl -sG "https://api.swapzilla.io/v1/partner/private-quotes" \
  -H "X-API-Key: $API_KEY" \
  --data-urlencode "from_asset=TRX" \
  --data-urlencode "from_network=TRX" \
  --data-urlencode "to_asset=USDT" \
  --data-urlencode "to_network=ERC20" \
  --data-urlencode "amount_from=100" \
  --data-urlencode "max_routes=5"

A streaming twin lives at /v1/partner/private-quotes-sse: same parameters plus timeout_ms, events route, provider_error, ping and done.

POST /v1/partner/private-orders key required

curl -s -X POST "https://api.swapzilla.io/v1/partner/private-orders" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "route_id": "9d2f1a76-6c3b-4a55-b0f1-2c9a7e0d4411",
    "to_address": "0x750c44dB01899176f2e64bD25A2fabAC1140d8e9",
    "refund_address": "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
    "intermediate_refund_address": "48jLd5…"
  }'
FieldDescription
route_idrequiredThe route's ID. Like offers, routes expire after 5 minutes.
to_addressrequiredFinal destination, in the target asset.
refund_addressstrongly advisedRefund address for the first leg, in the source asset.
intermediate_refund_addressstrongly advisedRefund address for the second leg, in the intermediate asset (an XMR address by default).
client_order_idoptionalYour own identifier.

Answers 201 with a PrivateSwap containing both FirstOrder and SecondOrder. The address to show the user is FirstOrder.DepositAddress.

Only the Idempotency-Key header is read here — unlike /orders, there is no body field for it.

GET /v1/partner/private-orders/{id} key required

Aggregated status plus both legs in full, which is everything a timeline UI needs. GET /v1/partner/private-orders lists yours with the same page / limit paging as regular orders.

StatusMeaning
NEWCreated; waiting for the deposit on the first leg.
STAGE_1The first leg is running (source asset → intermediate).
STAGE_2The first leg settled; the second is running (intermediate → target).
DONEBoth legs settled.
FAILEDOne of the legs failed, expired or refunded. Read the legs to see which.

Endpoints without a key

These are public. Your key works on them too, it just isn't needed.

GET /v1/assets

Every asset and network on offer, with the providers that support it — the source of truth for your currency selector. Filtered to the top 100 coins by market cap, refreshed every few hours.

{ "assets": [ { "Code": "BTC", "Network": "BTC", "Providers": ["fixedfloat", "changee"] } ] }

GET /v1/providers

The registered exchanges with display names and whether they are currently enabled.

{ "providers": [ { "code": "changee", "name": "Changee", "enabled": true } ] }

GET /v1/validate-address

Checks an address against the network's format before you let the user submit. Parameters: network and address; missing ones answer 400.

curl -sG "https://api.swapzilla.io/v1/validate-address" \
  --data-urlencode "network=ERC20" \
  --data-urlencode "address=0x750c44dB01899176f2e64bD25A2fabAC1140d8e9"
{ "valid": true, "network": "ERC20", "known": true }

known: false means we have no pattern for that network and could not really check — treat it as "unverified", not "valid".

GET /v1/export/rates.xml

The best cross-provider rate per direction as a BestChange-style XML feed, regenerated every few seconds and cached. Also published on the site as /export/rates.xml. Currency codes follow the monitor convention — USDT on Tron is USDTTRC20.

<rates>
  <item>
    <from>BTC</from>
    <to>USDTTRC20</to>
    <in>1</in>
    <out>63684.46973</out>
    <minamount>0.00078071</minamount>
    <maxamount>1.17106129</maxamount>
    <param>floating</param>
  </item>
</rates>

Idempotency

Both creating endpoints accept an Idempotency-Key header. Repeat a request with the same key and you get the same order back instead of a second one — which is exactly what you want when a network timeout leaves you unsure whether the first attempt landed.

Generate a UUID v4 before the first attempt, keep it for every retry of that attempt, and use a new one for a genuinely new order. POST /orders also accepts it as the body field idempotency_key; POST /private-orders reads the header only.

Errors

Every failure has the same shape:

{ "error": "offer not found or expired" }
CodeWhen
200Successful read.
201Order or private swap created.
400Bad parameters, or a business rule said no — expired offer, disabled asset, amount below the provider's minimum.
401Missing, invalid, revoked or disabled key.
404No such resource — or it isn't yours.
5xxOur side or a provider is temporarily unwell. Retry with backoff.

Errors you will actually meet

MessageCauseFix
offer not found / offer expiredMore than 5 minutes since the quote.Re-quote and create the order with the new offer_id.
route not found or expiredThe private route aged out, same 5 minutes.Re-request /private-quotes.
provider is disabledWe took that exchange offline after the quote.Use another offer from the list.
asset is disabledThe asset was switched off.Offer the user a different asset.
amount below minimum / above maximumProxied from the provider.Validate against MinFromAmount / MaxFromAmount before submitting.

Working with the API well

  • Cache quotes for 5–10 seconds. Providers rate-limit long before your users do, and rates do not move meaningfully faster than that.
  • Always send an idempotency key. It costs one line and it is the difference between a retry and a duplicate swap.
  • Always ask for a refund address. A failed swap without one is a support ticket at best.
  • Validate the amount against MinFromAmount / MaxFromAmount and the address against /v1/validate-address before you create anything.
  • Show KycRating and EstimatedMins next to each provider. The cheapest offer is not always the one the user wants.
  • Stream quotes into the UI with /quotes-sse, then re-rank using the deviations map from the done event.
  • Poll every 5–10 seconds and stop at a terminal status. Anything faster only adds load; our poller refreshes every 10 seconds anyway.
  • Never ship the key to a browser or a mobile binary. Put your backend in front.

FAQ

Are there webhooks for status changes?

Not yet — polling only. If your integration needs them, say so and we will prioritise it.

Can the destination address be changed after the order is created?

No. Create a new order.

What happens if a provider dies mid-swap?

An order that has not reached DONE within the provider's own window (typically 30–60 minutes) moves to TIME_EXPIRED or FAILED. If the deposit was already made, the provider refunds it to RefundAddress.

How do I rotate my key?

Ask us for a new one. It comes with a new partner id, and orders created under the old key stay under the old one — so we issue the new key first, you switch over, and the old key is revoked once its last order has finished. Revocation itself takes effect on the very next request.

Is there a sandbox?

Not in production. For deterministic smoke tests we can enable the mock provider for your key, which returns synthetic offers and orders.


Questions, a key request, or something on this page that no longer matches what the API does — write to @swapzilla_support_bot.