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 URL | https://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 |
| Auth | X-API-Key: <your key> on every request |
| Content type | JSON in, JSON out; text/event-stream for the two streaming endpoints |
| Rate limits | none on our side — but providers throttle repeated quote requests, so cache for 5–10 seconds |
| Test network | not 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.
-
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"
-
Create the order
Send the offer's
IDtogether with the user's destination address. You get back aDepositAddress.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" }' -
Show the deposit address, then poll
The user sends
AmountFromof the source asset toDepositAddress. 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
| Parameter | Description | |
|---|---|---|
from_asset | required | Ticker of the source asset, e.g. BTC, USDT. Case-insensitive. |
from_network | required | Its network. Common aliases are accepted: ERC20/ETH/ETHEREUM, TRC20/TRX, BEP20/BSC, BITCOIN/BTC. |
to_asset | required | Ticker of the destination asset. |
to_network | required | Its network. |
amount_from | usually | How much the user sends, in from_asset units. |
amount_to | optional | Quote by the amount the user should receive instead. Use one or the other. |
rate_type | optional | floating (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
| Field | Meaning |
|---|---|
ID | What you pass as offer_id when creating the order. Valid for 5 minutes. |
ProviderName | Human-readable name — show this, not ProviderCode. |
RateType | floating follows the market until the deposit confirms; fixed is locked. |
EstimatedMins | Median completion time of that provider's last 100 successful orders. |
KycRating | A (least intrusive) to D (aggressive KYC). Users deserve to see it before they commit. |
DeviationPercent | How much worse than the best offer, in percent. 0 marks the best one. |
AmountFromUsd / AmountToUsd | USD equivalents. Omitted when the price feed has no quote for the asset. |
MinFromAmount / MaxFromAmount | The provider's limits for this pair, in FromAsset units. Omitted until the provider reports them. Validate the input against these before opening an order. |
IsPayment | Present 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
| Event | Data |
|---|---|
offer | One offer object, identical to an entry of the offers array. |
private_route | The 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
| Field | Description | |
|---|---|---|
offer_id | required | The ID of the chosen offer, at most 5 minutes old. |
to_address | required | Where the exchanged asset goes. Cannot be changed later. |
refund_address | strongly advised | Where the source asset returns if the swap fails. Without it a refund may need manual work at the provider. |
from_address | optional | The address the user pays from, when you know it. |
client_order_id | optional | Your own identifier, stored with the order. |
expected_to_amount | optional | Slippage guard: if the recalculated payout differs, the order is refused instead of created. |
expected_from_amount | optional | The amount you promised the user would send; stored as AmountFromExpected. |
idempotency_key | optional | Same 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
| Status | Meaning | |
|---|---|---|
| NEW | Created; waiting for the user's deposit. | in progress |
| WAIT_DEPOSIT | Same thing, confirmed explicitly by the provider. | in progress |
| CONFIRMING | Deposit seen on-chain; waiting for confirmations. | in progress |
| EXCHANGING | Funds confirmed; the swap is running. | in progress |
| SENDING | Provider is paying out to PayoutAddress. | in progress |
| DONE | Settled. AmountToReceived and TxID are final. | terminal |
| TIME_EXPIRED | The deposit did not arrive in time. | terminal |
| FAILED | The exchange failed; ErrorMessage says what the provider reported. | terminal |
| REFUNDED | Funds 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.
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…"
}'
| Field | Description | |
|---|---|---|
route_id | required | The route's ID. Like offers, routes expire after 5 minutes. |
to_address | required | Final destination, in the target asset. |
refund_address | strongly advised | Refund address for the first leg, in the source asset. |
intermediate_refund_address | strongly advised | Refund address for the second leg, in the intermediate asset (an XMR address by default). |
client_order_id | optional | Your 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.
| Status | Meaning |
|---|---|
| NEW | Created; waiting for the deposit on the first leg. |
| STAGE_1 | The first leg is running (source asset → intermediate). |
| STAGE_2 | The first leg settled; the second is running (intermediate → target). |
| DONE | Both legs settled. |
| FAILED | One 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" }
| Code | When |
|---|---|
200 | Successful read. |
201 | Order or private swap created. |
400 | Bad parameters, or a business rule said no — expired offer, disabled asset, amount below the provider's minimum. |
401 | Missing, invalid, revoked or disabled key. |
404 | No such resource — or it isn't yours. |
5xx | Our side or a provider is temporarily unwell. Retry with backoff. |
Errors you will actually meet
| Message | Cause | Fix |
|---|---|---|
offer not found / offer expired | More than 5 minutes since the quote. | Re-quote and create the order with the new offer_id. |
route not found or expired | The private route aged out, same 5 minutes. | Re-request /private-quotes. |
provider is disabled | We took that exchange offline after the quote. | Use another offer from the list. |
asset is disabled | The asset was switched off. | Offer the user a different asset. |
| amount below minimum / above maximum | Proxied 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/MaxFromAmountand the address against/v1/validate-addressbefore you create anything. - Show
KycRatingandEstimatedMinsnext 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 thedeviationsmap from thedoneevent. - 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.