API documentation
Quote and create swaps from your own app or site. Payouts go straight to your user's address. You can add your own commission on top of the rate.
OpenAPI 3.1 reference/api/v1/openapi.jsonEvery endpoint, request and response field, error and webhook, in a format API tools and code generators read.
Overview
JSON over HTTPS. Coin amounts are decimal strings in coin units ("0.015"), never floats. US dollar values are numbers. Times are ISO 8601 strings in UTC. A coin on a particular network is a method, with an id such as btc, eth-base or usdt-tron: use those ids for from and to.
Sandbox
A build running in sim mode is the sandbox: every chain is simulated and no real coins move. The same endpoints work, plus two developer helpers that only exist outside mainnet: POST /api/dev/deposit sends a test deposit to an order (with a clean, high_risk or sanctioned scenario) and POST /api/dev/mine mines blocks so it confirms. GET /api/v1/meta tells you which mode you are talking to.
BASE=http://localhost:4200
curl -s -X POST "$BASE/api/dev/deposit" -H "content-type: application/json" \
-d '{"orderId":"ORDER_ID","scenario":"clean"}'
curl -s -X POST "$BASE/api/dev/mine" -H "content-type: application/json" -d '{"blocks":3}'Authentication
Partner requests carry an API key in the x-api-key header (Authorization: Bearer also works). Keys look like pxk_<prefix>_<secret>, are created in the admin console and are shown only once. Requests without a key are treated as a regular visitor with no commission. A wrong or switched-off key gets 401 invalid_api_key.
End-user IP
Country rules apply to the person swapping, not to your server. With an API key, every request that prices or creates something must send that person's public IP address in x-user-ip. Requests without it fail with user_ip_required, and private or reserved addresses with user_ip_invalid.
Rate limits
Limits count requests per IP address (your end user's, with an API key). Going over returns HTTP 429 with the code rate_limited. Endpoints not listed here have no fixed limit, but cache /api/v1/coins: it changes rarely.
| Endpoint | Limit |
|---|---|
| GET /api/v1/pair | 120 per minute |
| POST /api/v1/quotes | 20 per minute |
| POST /api/v1/orders | 10 per minute |
| POST /api/v1/kyc/{id}/submit | 10 per minute |
| POST /api/v1/payment-links | 10 per minute without an API key, 60 with one |
| GET /api/v1/payment-links/{id} | 120 per minute |
| POST /api/v1/payment-links/{id}/quote | 20 per minute |
| POST /api/v1/payment-links/{id}/orders | 10 per minute |
| GET /api/v1/payment-links/{id}/payments | 60 per minute |
| POST /api/v1/payment-links/{id}/cancel | 60 per minute |
| GET /api/v1/alerts | 60 per minute |
| POST /api/v1/alerts | 10 per 10 minutes |
| GET /api/v1/alerts/{id} | 60 per minute |
| DELETE /api/v1/alerts/{id} | 30 per minute |
| GET /api/v1/telegram | 60 per minute |
| GET /api/v1/account | 120 per minute |
| POST /api/v1/account | 10 per minute |
| POST /api/v1/account/restore | 10 per minute, 30 per hour |
| POST /api/v1/account/key | 10 per minute |
| POST /api/v1/account/forget | 20 per minute |
| GET /api/v1/account/orders | 120 per minute |
| POST /api/v1/account/orders/{id}/access | 30 per minute |
| GET /api/v1/account/addresses | 120 per minute |
| POST /api/v1/account/addresses | 30 per minute |
| PATCH /api/v1/account/addresses/{id} | 30 per minute |
| DELETE /api/v1/account/addresses/{id} | 30 per minute |
| GET /api/v1/account/referrals | 120 per minute |
| GET /api/v1/account/withdrawals | 120 per minute |
| POST /api/v1/account/withdrawals | 5 per minute |
| GET /api/v1/partner | 60 per minute |
| GET /api/v1/partner/orders | 60 per minute |
| GET /api/v1/partner/withdrawals | 60 per minute |
| POST /api/v1/partner/withdrawals | 5 per minute |
| POST /api/v1/support | 20 tries per 10 minutes, 5 new requests per hour |
| GET /api/v1/support/{id} | 60 per minute |
| POST /api/v1/support/{id}/messages | 10 per 10 minutes |
Two more limits protect the service: at most 10 new orders per device per hour (velocity_limit), and at most 6 open live streams per IP address (too_many_streams).
Endpoints
Service
Mode, coins, status and policies. No authentication.
| Method | Path | What it does |
|---|---|---|
| GET | /api/v1/meta | Service mode, the caller's country rules and time limits |
| GET | /api/v1/coins | Coins, networks and methods (a coin on a network) |
| GET | /api/v1/status | Price feed, queues and network health |
| GET | /api/v1/reserves | Balances held against payouts owed |
| GET | /api/v1/stats | Swap counts, volume and median time to payout |
| GET | /api/v1/recent | The latest completed swaps, without addresses |
| GET | /api/v1/compliance/policy | Country rules, limits, review deadline and travel-rule threshold |
| GET | /api/v1/telegram | Whether the Telegram bot is set up, its links and commands |
| GET | /api/v1/openapi.json | This document |
Swaps
Prices, quotes and new orders. An API key is optional and adds your commission.
| Method | Path | What it does |
|---|---|---|
| GET | /api/v1/pair | Rate, limits and fees for a pair, with an estimate when amount is given |
| POST | /api/v1/quotes | Lock a price for a fixed-rate swap |
| POST | /api/v1/orders | Create a fixed or variable swap |
Orders
Reading and changing an order. Changes need the order token.
| Method | Path | What it does |
|---|---|---|
| GET | /api/v1/orders/{id} | An order, its deposits and its history |
| GET | /api/v1/orders/{id}/stream | Live updates for an order (server-sent events) |
| POST | /api/v1/orders/{id}/refund-address | Set or change the refund address |
| POST | /api/v1/orders/{id}/cancel | Cancel an order before any deposit |
| POST | /api/v1/orders/{id}/kyc | Start identity verification for an order |
| POST | /api/v1/orders/{id}/travel-rule | Say who owns an exchange payout address |
| GET | /api/v1/kyc/{id} | An identity verification session |
Payment links
"Pay me exactly X of coin Y": links that anyone can pay with any supported coin.
| Method | Path | What it does |
|---|---|---|
| POST | /api/v1/payment-links | Create a payment link |
| GET | /api/v1/payment-links/{id} | A payment link (public view) |
| POST | /api/v1/payment-links/{id}/quote | What a payer would send, in the coin they choose |
| POST | /api/v1/payment-links/{id}/orders | Start a payment: create the fixed-rate order that pays the link |
| GET | /api/v1/payment-links/{id}/payments | Every payment of a link (creator only) |
| POST | /api/v1/payment-links/{id}/cancel | Close a link to new payments (creator only) |
Price alerts
One Telegram message when a rate crosses a number.
| Method | Path | What it does |
|---|---|---|
| GET | /api/v1/alerts | The alerts with these ids |
| POST | /api/v1/alerts | Create a price alert, delivered on Telegram |
| GET | /api/v1/alerts/{id} | One price alert |
| DELETE | /api/v1/alerts/{id} | Cancel a price alert |
Accounts
Optional accounts with no sign-up: order history, address book, referrals. The account key lives in an HttpOnly cookie; writes need x-px-account: 1.
| Method | Path | What it does |
|---|---|---|
| GET | /api/v1/account | This browser's account, if it has one |
| POST | /api/v1/account | Create an account for this browser (or return the one it has) |
| POST | /api/v1/account/restore | Use an account in this browser, with its account key |
| POST | /api/v1/account/key | Show the account key this browser holds, to back it up |
| POST | /api/v1/account/forget | Remove the account key from this browser |
| GET | /api/v1/account/orders | This account's orders, newest first |
| POST | /api/v1/account/orders/{id}/access | An order key for one of this account's orders |
| GET | /api/v1/account/addresses | Saved payout addresses (the address book) |
| POST | /api/v1/account/addresses | Save a payout address |
| PATCH | /api/v1/account/addresses/{id} | Change a saved address |
| DELETE | /api/v1/account/addresses/{id} | Delete a saved address |
| GET | /api/v1/account/referrals | Referral link, referred volume, earnings and withdrawals |
| GET | /api/v1/account/withdrawals | This account's referral withdrawals, newest first |
| POST | /api/v1/account/withdrawals | Withdraw every available referral earning as one stablecoin payout |
Partner portal
Your volume, commission, webhook deliveries and withdrawals. Needs your API key.
| Method | Path | What it does |
|---|---|---|
| GET | /api/v1/partner | Your volume, commission, recent orders, webhook deliveries and withdrawals |
| GET | /api/v1/partner/orders | Orders made with your API key, newest first, 25 per page |
| GET | /api/v1/partner/withdrawals | Your commission withdrawals |
| POST | /api/v1/partner/withdrawals | Withdraw all available commission as one stablecoin payout |
Support
Support requests without an account, read on a private page.
| Method | Path | What it does |
|---|---|---|
| POST | /api/v1/support | Open a support request |
| GET | /api/v1/support/{id} | A support request and its messages |
| POST | /api/v1/support/{id}/messages | Write again on a support request |
Sandbox
Only where developer tools are on.
| Method | Path | What it does |
|---|---|---|
| POST | /api/v1/kyc/{id}/submit | Submit the mock identity check (sandbox only) |
Request and response fields are in the OpenAPI document. The TypeScript types live in src/lib/api-types.ts in this project.
Fixed-rate flow
Lock the price first, then create the order within the quote's lifetime. The user must send exactly depositAmount, and receives exactly settleAmount: network fees are already taken off. You can quote by the amount sent (depositAmount) or by the amount received (settleAmount), not both.
curl -s "$BASE/api/v1/quotes" \
-H "content-type: application/json" \
-H "x-api-key: $PX_KEY" \
-H "x-user-ip: 203.0.113.7" \
-d '{"from":"btc","to":"usdt-tron","depositAmount":"0.005"}'{
"id": "q_zp4h32n4f18gsz53",
"from": "btc",
"to": "usdt-tron",
"depositAmount": "0.005",
"settleAmount": "407.417394",
"rate": "81831.8587259",
"expiresAt": "2026-10-13T15:58:54.958Z",
"fees": {
"receiveFeeUsd": 0.3, "sendFeeUsd": 1.44, "totalNetworkFeesUsd": 1.74,
"sendFeeInSettle": "1.441572", "networkFeesInSettle": "1.741899",
"spreadBps": 180, "commissionBps": 0,
"spreadParts": { "tierBps": 150, "fixedBps": 30, "volatilityBps": 0, "surchargeBps": 0, "nudgeBps": 0, "minEarningsBps": 0 }
}
}curl -s "$BASE/api/v1/orders" \
-H "content-type: application/json" \
-H "x-api-key: $PX_KEY" \
-H "x-user-ip: 203.0.113.7" \
-d '{
"type": "fixed",
"quoteId": "q_zp4h32n4f18gsz53",
"settleAddress": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"refundAddress": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
"payoutDestination": "self_hosted",
"acceptTerms": true
}'The response holds order (show order.depositAddress, order.depositMemo when set, and order.expectedDeposit) and orderToken. When depositAddress is null, the user must verify their identity first: start it with POST /api/v1/orders/:id/kyc.
Variable-rate flow
No quote. Create the order with from and to. The user can send any amount between order.depositMin and order.depositMax; the rate is set when each deposit confirms. GET /api/v1/pair with an amount gives an estimate to show beforehand.
curl -s "$BASE/api/v1/orders" \
-H "content-type: application/json" \
-H "x-api-key: $PX_KEY" \
-H "x-user-ip: 203.0.113.7" \
-d '{"type":"variable","from":"eth","to":"sol","settleAddress":"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v","acceptTerms":true}'Payouts to an exchange account: send "payoutDestination": "exchange". If the swap can reach the travel-rule threshold, also send beneficiary: { name, vaspName }, the account holder's full name and the exchange's name. See the compliance policy for the threshold.
Order token
orderToken is returned once, when the order is created. Store it with the order. Send it as x-order-token to set a refund address, cancel, start identity verification or send travel-rule details. Anyone with the order id can read the order, with addresses shortened; the token holder (or the partner whose key created the order) sees them in full and can change the order.
Order statuses
| Status | Meaning |
|---|---|
| waiting | No deposit has arrived yet. |
| pending | Deposit seen. Waiting for network confirmations. |
| processing | Deposit confirmed. Pricing and checks are running. |
| review | Held for a compliance check. |
| settling | Payout broadcast. Waiting for it to confirm. |
| settled | Payout confirmed. |
| refund | Needs a refund address, or is queued for a refund. |
| refunding | Refund broadcast. Waiting for it to confirm. |
| refunded | Refund confirmed. |
| frozen | Blocked by a sanctions match or a legal hold. |
| failed | Cannot be paid out or returned. |
| expired | No deposit arrived before the deadline. |
| cancelled | Cancelled before any deposit. |
| multiple | Deposits with different outcomes. See each one. |
Follow an order with the event stream (GET /api/v1/orders/:id/stream, an order event with the full order on connect and on every change), by polling GET /api/v1/orders/:id every few seconds, or with webhooks. When a deposit leaves the normal path, deposit.reason names why and deposit.reasonText explains it in plain English. Reviews with manualDecision: true have no automatic refund deadline: a person decides.
Payment links
A payment link says pay me exactly 25 USDT on Tron at this address. Whoever pays can use any coin we support: they get a fixed-rate quote for exactly that amount, and their payment is a normal fixed-rate swap whose payout goes to the link's address. The payer pays our spread and the network fees; the recipient gets the stated amount. Links created with your API key belong to you and send payment_link.* webhooks.
curl -s -X POST "$BASE/api/v1/payment-links" \
-H "content-type: application/json" \
-H "x-api-key: $PX_KEY" \
-H "x-user-ip: 203.0.113.7" \
-d '{
"title": "Invoice 1042",
"settleMethodId": "usdt-tron",
"settleAmount": "25",
"settleAddress": "TBFQ4wTTgtsK4SvFuBNVCbhHJPbWdxE9zN",
"expiresIn": "7d",
"payoutDestination": "self_hosted"
}'The response has link, payUrl (share it) and manageToken with manageUrl (shown once: keep them private). One-payment links (the default) take one payment at a time and are paid once it settles; reusable links ("reusable": true) take any number of full payments. Expiry: 1h, 24h, 7d, 30d or never.
curl -s -X POST "$BASE/api/v1/payment-links/pl_pztpm3f8c04ez2/quote" \
-H "content-type: application/json" -d '{"from":"btc"}'
curl -s -X POST "$BASE/api/v1/payment-links/pl_pztpm3f8c04ez2/orders" \
-H "content-type: application/json" \
-d '{"from":"btc","quoteId":"q_h9tga45ztbr27xdk","refundAddress":"bc1q...","acceptTerms":true}'curl -s "$BASE/api/v1/payment-links/pl_pztpm3f8c04ez2/payments" -H "x-link-token: $MANAGE_TOKEN"
curl -s -X POST "$BASE/api/v1/payment-links/pl_pztpm3f8c04ez2/cancel" -H "x-link-token: $MANAGE_TOKEN"GET /api/v1/payment-links/:id is public (no secrets, no payer details). Its availability says whether a payment can start now and, if not, why: paid, expired, cancelled, payment_open (someone else has an open order, with retryAfter), payment_in_progress or unavailable. Every payer goes through the same country rules, limits and screening as any other swap.
Webhooks
Set a webhook URL on your partner account. We POST JSON to it for these events. Each body is { "event", "createdAt", "data" }.
Orders
Orders created with your API key.
- order.createdAn order was created with your API key.
- order.expiredThe order closed before a deposit arrived.
- order.cancelledThe order was cancelled before any deposit.
Deposits
Each deposit on those orders, as it moves through the steps.
- deposit.detectedA deposit was seen on the network and is waiting for confirmations.
- deposit.confirmedThe deposit has its confirmations; pricing and checks run next.
- deposit.reviewThe deposit is held for a check (reason and deadline in the order).
- deposit.settlingThe payout was sent and is waiting for the network.
- deposit.settledThe payout is confirmed. The swap is complete.
- deposit.refundThe deposit will be returned (it may need a refund address).
- deposit.refundingThe refund was sent and is waiting for the network.
- deposit.refundedThe refund is confirmed.
- deposit.frozenThe deposit is frozen under a sanctions match or a legal hold.
- deposit.failedThe deposit could not be paid out or returned (for example, too small to send back).
Payment links
Links created with your API key.
- payment_link.paidA one-payment link created with your API key was paid. Sent once.
- payment_link.expiredAn open link created with your API key passed its expiry. Sent once.
Commission withdrawals
Payouts of the commission you earned.
- withdrawal.sentA commission withdrawal to your payout address was broadcast.
- withdrawal.completedA commission withdrawal is confirmed on the network.
{
"event": "deposit.settled",
"createdAt": "2026-10-13T15:40:02.114Z",
"data": { "orderId": "82mvk6s12m9sz0", "depositId": "d_ac88wy833k5h8tb4", "message": "Sent 0.0033 ETH to your address." }
}{
"event": "payment_link.paid",
"createdAt": "2026-10-13T20:02:30.101Z",
"data": {
"linkId": "pl_pztpm3f8c04ez2", "status": "paid", "title": "Invoice 1042",
"settleMethodId": "usdt-tron", "settleAmount": "25", "amountReceived": "25.000112",
"orderId": "d29k7x4wbq5psa", "depositId": "d_f9wvve7nksmtjwc0",
"settleTxid": "634b39ce...", "paidAt": "2026-10-13T20:02:25.573Z"
}
}Every request carries x-px-signature: t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of t + "." + body using your webhook secret. Check it against the raw body before you parse it, and reject old timestamps. Answer with any 2xx status within 8 seconds; failed deliveries are retried with growing gaps, up to 10 attempts over several hours, so the same event can arrive twice. Use the ids to ignore repeats.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyPlaxswapSignature(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(",").map((part) => part.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
const given = Buffer.from(parts.v1 ?? "", "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
}Embeddable widget
Put a small swap box on your own site. It shows two coins, an amount and the live rate. There is no address field in it: Continue opens the swap page in a new tab with the pair and amount filled in, where your visitor adds their address and checks every detail. Pass your referral code with ref.
URL: /widget?from=btc&to=eth&amount=0.01&theme=auto&ref=CODE. from and to take method ids or coins, theme is light, dark or auto (follows your visitor's system setting). A frame of about 400 by 460 pixels fits it.
Leave empty for an example amount.
Passed on to the swap page.
At least 420. The width fits the space it is given, up to 400.
<iframe
src="https://plaxswap.com/widget?from=btc&to=eth&amount=0.01"
title="Crypto swap"
width="400" height="460"
style="border:0;border-radius:20px;max-width:100%"
loading="lazy"
sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox"
></iframe>The frame only shows coins, an amount and the live rate. There is no address field in it, and nothing is created there: Continue opens the swap page in a new tab, where your visitor adds their address and reviews every detail. The sandbox attribute keeps the frame away from your page.
Live preview
Errors
Errors use HTTP status codes and a JSON body: { "error": { "code", "message", "details" } }. The message is plain English and safe to show to your user. Codes you will see most:
| Code | HTTP | Meaning |
|---|---|---|
| bad_request | 400 | A field is missing or malformed. The message says which. |
| invalid_address | 400 | The payout or refund address failed its format or checksum check. |
| invalid_memo | 400 | The memo, tag or comment is not valid for that network. |
| travel_rule_required | 400 | An exchange payout over the threshold needs the account holder's name. |
| user_ip_required | 400 | A request with an API key did not send x-user-ip. |
| user_ip_invalid | 400 | x-user-ip is not a public IP address. |
| invalid_api_key | 401 | The API key is wrong or switched off. |
| forbidden | 403 | The order token (x-order-token) or link token (x-link-token) is missing or wrong. |
| address_not_allowed | 403 | The address failed sanctions or risk screening. |
| private_pilot | 403 | Real-money swaps are a private test and not open to the public yet. |
| not_found | 404 | No order, quote, link or alert with that id. |
| quote_expired | 409 | The quote ran out. Request a new one. |
| coin_unavailable | 409 | Deposits or payouts for that coin are paused. |
| limit_exceeded | 409 | The caller's remaining limit without verification is below the pair minimum. |
| refund_address_locked | 409 | A deposit is being checked, so the refund address cannot change now. |
| rate_limited | 429 | Too many requests from this IP address in the time window. |
| velocity_limit | 429 | Too many orders from one device in the last hour. |
| too_many_streams | 429 | Too many open live streams from one IP address. Poll instead. |
| jurisdiction_blocked | 451 | Swaps are not offered in the end user's country. |
| service_paused | 503 | New swaps are paused for now. Open orders still complete. |
The full list for each endpoint is in the OpenAPI document.