Skip to content

API documentation / Payment gateway

Payment gateway API

Take crypto on your website. You ask for a payment, send your customer to our checkout, and get a signed webhook when it is paid. Confirmed payments credit your Plaxswap merchant balance. Withdraw to your wallet after reviewing the network fee.

Overview

JSON over HTTPS at https://plaxswap.com. Coin amounts are decimal strings in coin units ("99.5"), never floats. Times are ISO 8601 in UTC. A coin on one network has an id such as usdt-tron, usdc-base or btc.

How one payment works:

  1. Your server creates a payment with the price and your order number. You get a checkoutUrl.
  2. You send your customer to it. They pick a coin, see exactly how much to send, and send it.
  3. The coin you receive is passed on as it is. Another coin is converted by Uniswap (same network) or SwapKit (between networks). It is added to your Plaxswap merchant balance.
  4. We POST a signed payment.paid webhook to your server, and the customer returns to your returnUrl.

Confirmed payments are added to your Plaxswap merchant balance, in the coin and network you receive. Plaxswap holds these coins until you withdraw them to your own wallet; you pay the network fee of each withdrawal. (Older direct-wallet payments still go straight to the address set when they were created.) The platform fee is 1.5% of a payment. Network fees and Uniswap or SwapKit costs are paid by your customer, inside the amount they send.

Quickstart

1. Get your keys

Sign up on /merchants. You get a live key (pxs_live_...), a test key (pxs_test_...) and a webhook secret (whsec_...), shown once. Keep them on your server.

2. Create a payment

curl
curl -s -X POST https://plaxswap.com/api/v1/payments \
  -H "Authorization: Bearer pxs_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042" \
  -d '{
    "amount": "100",
    "currency": "USD",
    "receive": "usdt-tron",
    "orderId": "1042",
    "description": "2 x T-shirt",
    "returnUrl": "https://shop.example/orders/1042/thanks",
    "cancelUrl": "https://shop.example/cart"
  }'

The answer is the payment. Send your customer to its checkoutUrl:

201 Created
{
  "id": "pay_7m2k9q4x8c1v5b3n",
  "object": "payment",
  "livemode": false,
  "status": "pending",
  "amount": "100",
  "currency": "USD",
  "receive": { "id": "usdt-tron", "assetId": "USDT", "networkId": "tron", "label": "USDT · Tron (TRC-20)", "decimals": 6, "memoRequired": false },
  "receiveAddress": null,
  "receiveAmount": "100",
  "expectedAmount": "100",
  "receivedAmount": "0",
  "fee": { "bps": 150, "paidBy": "customer", "amount": "1.5" },
  "orderId": "1042",
  "checkoutUrl": "https://plaxswap.com/checkout/pay_7m2k9q4x8c1v5b3n",
  "expiresAt": "2026-10-20T11:00:00.000Z",
  "transactions": [],
  ...
}

3. Handle the webhook

When the payment changes we POST it to your webhook address. Check the signature, then act on data.status. Ship when it is paid.

Node.js (Express)
import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.PLAXSWAP_WEBHOOK_SECRET; // whsec_...

// The raw body is needed for the signature check.
app.post("/plaxswap-webhook", express.raw({ type: "application/json" }), async (req, res) => {
  const header = req.get("x-px-signature") || "";
  const [, t, v1] = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header) || [];
  const expected = crypto.createHmac("sha256", SECRET).update(`${t}.${req.body}`).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
  if (!v1 || !fresh || !crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))) return res.sendStatus(400);

  const event = JSON.parse(req.body);
  const payment = event.data;
  // Test events must never fulfil real orders. Bind to the stored gateway payment ID, not a customer-provided order number.
  if (!event.livemode || !payment.livemode) return res.sendStatus(200);
  if (event.event === "payment.paid") {
    const expected = await findShopOrderByPaymentId(payment.id);
    if (!expected || expected.gatewayPaymentId !== payment.id || expected.orderId !== payment.orderId) return res.sendStatus(400);
    // This function must deduplicate event.id and atomically fulfil only once; compare the expected coin and amount too.
    await fulfilOrderOnce(expected, event.id, payment);
  }
  res.sendStatus(200); // any 2xx within 8 seconds
});

4. When the customer comes back

The return page should not trust the browser. Read the payment from your server with GET /api/v1/payments/:id (or wait for the webhook) before you show "paid".

Test mode

Payments created with a test key never move real coins. The checkout shows a test banner, a pretend address, and buttons that act out each outcome. You can do the same from your server:

Act out an outcome
curl -s -X POST https://plaxswap.com/api/v1/payments/pay_7m2k9q4x8c1v5b3n/simulate \
  -H "Authorization: Bearer pxs_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "outcome": "paid" }'

Outcomes: paid, partially_paid, overpaid, late, expired, failed. Each sends the same webhook a live payment would, with "livemode": false and the header x-px-livemode: false. A test payment takes one outcome; make another to try the next. Test keys see only test payments, live keys only live ones.

Keys and requests

Send your secret key as Authorization: Bearer pxs_live_... (or in x-api-key). Call the API from your server only: never put a secret key in a web page or an app. Every answer carries an x-request-id; quote it when you write to support.

Each key can make 300 requests a minute. Make more keys, or switch one off if it leaked, on your merchant page or with POST /api/v1/merchant/keys and PATCH /api/v1/merchant/keys/:id.

Make retries safe with an Idempotency-Key header on POST /api/v1/payments: the same key with the same body returns the same payment (200 instead of 201); with a different body it is refused (409).

Creating a payment

FieldRequiredWhat it is
amountyesThe price, as text: "100", "25.50".
currencyyesA fiat code (USD, EUR, INR...) converted when the payment is made, or a coin (USDT, BTC...). USD prices count one to one for USDT, USDC and DAI.
receivenoThe coin you receive, such as usdt-tron. Default: the one saved on your account.
receiveAddressnoOlder direct-wallet accounts only: where this payment goes. Default: your saved address (needed when you name another coin). Accounts with a merchant balance cannot set it: payments go to the balance.
receiveMemonoMemo or tag of that address, on networks that use one.
orderIdnoYour order number, shown to the payer and searchable (?orderId=).
descriptionnoShown to the payer.
returnUrlnohttps address the payer goes to after paying.
cancelUrlnohttps address of the checkout's back link.
expiresInMinutesno15 to 1440. Default 60.
customerPaysFeenotrue: the payer pays the platform fee on top. Default: your account's choice. Accounts with a merchant balance: always true.
metadatanoUp to 20 keys of your own, text values. Sent back in every webhook.

Amounts and fees

FieldMeaning
receiveAmountThe price in your coin, fixed when the payment is made.
expectedAmountWhat you should receive: receiveAmount, less the 1.5% platform fee when you pay it (fee.paidBy merchant).
receivedAmountWhat reached you (your balance, or your address on older direct-wallet payments) so far.
feeThe platform fee: bps (100 = 1%), who pays it, and its amount in your coin.

The payer is asked for a little more than the price when their coin is converted (a price cushion of 0.5%), so a small price move while their coins confirm still covers you. Whatever is left over reaches you. What arrives is passed on as it is: a short payment is passed on and marked partly paid, an extra one is passed on with overpaid: true, and coins that arrive after the payment expired (up to 24 hours) are passed on with late: true. Refunds of payments that reached you are yours to make.

Statuses

StatusMeaning
pendingWaiting for the payer. They may have picked a coin and be about to send.
confirmingCoins arrived and are being confirmed, checked or converted. Do not ship yet.
paidThe payer sent the amount asked and the payment settled. Verify the payment reference, amount and mode before fulfilling the order.
partially_paidLess than asked settled. While the payment is open the payer can send the rest; after that it stays partly paid.
expiredNothing arrived in time. Coins can still arrive up to 24 hours later: then a payment.paid or payment.partially_paid with late true follows.
cancelledYou cancelled it before anything arrived.
failedThe payer's coins were sent back to them (the conversion failed or they could not be accepted).

Flags: late (coins arrived after it expired), overpaid (clearly more than asked), underReview (a transfer is held for a compliance check; it continues, or goes back to the payer). Each transfer is listed in transactions with its transaction ids and what it gave you.

Coins

GET /api/v1/gateway/coins lists the coins you can receive and what payers can pay each one with. The payer can always pay with your own coin. Other coins work where Uniswap swaps them on the same network, or where SwapKit carries them between networks (mainly BTC, ETH, and USDT or USDC on Ethereum and Arbitrum, into Tron USDT as well). The checkout shows only what can pay that payment at that moment. Payers pass our checkout checks: country rules, an identity check above our limits, and sanctions and wallet screening.

Webhooks

Copy your shop’s callback URL into Merchant → Webhooks, or configure it with PATCH /api/v1/merchant. Every event is a POST with the whole payment in data:

EventWhen
payment.confirmingCoins for the payment arrived and are being confirmed, checked or converted.
payment.paidThe payer sent the amount asked and it reached your address.
payment.partially_paidLess than asked reached your address. The payer can still send the rest while the payment is open.
payment.expiredNothing arrived in time. Coins can still arrive late (up to 24 hours), then a payment.paid or payment.partially_paid with late true follows.
payment.cancelledYou cancelled the payment.
payment.failedThe payer's coins were sent back (the conversion failed or they could not be accepted).
payment.updatedA flag or the amount changed without a status change (overpaid, a review, more coins arrived).
pingA test webhook from POST /api/v1/merchant/webhooks/test.
Body
{
  "id": "evt_3k9q2m7x8c1v5b4n",
  "event": "payment.paid",
  "livemode": true,
  "createdAt": "2026-10-20T10:22:05.000Z",
  "data": { "id": "pay_7m2k9q4x8c1v5b3n", "object": "payment", "status": "paid", "orderId": "1042", "receivedAmount": "99.84", ... }
}

The x-px-signature header is t=<unix seconds>,v1=<hex HMAC-SHA256 of "t.body" with your webhook secret>. Check it against the raw body, refuse old timestamps (older than 5 minutes), and answer any 2xx within 8 seconds. Failed deliveries are retried with growing gaps, 10 times over about a day, so the same event can arrive twice: use id to ignore repeats. See past deliveries and send one again on your merchant page. A test ping: POST /api/v1/merchant/webhooks/test.

Code samples

PHP

Create a payment and check a webhook
<?php
$key = getenv('PLAXSWAP_KEY');            // pxs_live_... or pxs_test_...
$ch = curl_init('https://plaxswap.com/api/v1/payments');
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key, 'Content-Type: application/json', 'Idempotency-Key: order-' . $orderId],
  CURLOPT_POSTFIELDS => json_encode([
    'amount' => '100', 'currency' => 'USD', 'receive' => 'usdt-tron',
    'orderId' => (string) $orderId, 'returnUrl' => 'https://shop.example/thanks',
  ]),
]);
$payment = json_decode(curl_exec($ch), true);
header('Location: ' . $payment['checkoutUrl']);

// The webhook endpoint:
$raw = file_get_contents('php://input');
if (!preg_match('/^t=(\d+),v1=([0-9a-f]{64})$/', $_SERVER['HTTP_X_PX_SIGNATURE'] ?? '', $m)) { http_response_code(400); exit; }
$expected = hash_hmac('sha256', $m[1] . '.' . $raw, getenv('PLAXSWAP_WEBHOOK_SECRET'));
if (!hash_equals($expected, $m[2]) || abs(time() - (int) $m[1]) > 300) { http_response_code(400); exit; }
$event = json_decode($raw, true);
if ($event['event'] === 'payment.paid') { mark_order_paid($event['data']['orderId']); }
http_response_code(200);

Python

Create a payment and check a webhook (Flask)
import hashlib, hmac, os, time, requests
from flask import Flask, abort, request

API = "https://plaxswap.com/api/v1"
KEY = os.environ["PLAXSWAP_KEY"]

def create_payment(order_id, usd):
    r = requests.post(f"{API}/payments", timeout=15,
        headers={"Authorization": f"Bearer {KEY}", "Idempotency-Key": f"order-{order_id}"},
        json={"amount": str(usd), "currency": "USD", "receive": "usdt-tron", "orderId": str(order_id)})
    r.raise_for_status()
    return r.json()["checkoutUrl"]

app = Flask(__name__)

@app.post("/plaxswap-webhook")
def webhook():
    raw = request.get_data()
    parts = dict(p.split("=", 1) for p in request.headers.get("x-px-signature", "").split(",") if "=" in p)
    expected = hmac.new(os.environ["PLAXSWAP_WEBHOOK_SECRET"].encode(), parts.get("t", "").encode() + b"." + raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, parts.get("v1", "")) or abs(time.time() - int(parts.get("t", 0))) > 300:
        abort(400)
    event = request.get_json()
    if event["event"] == "payment.paid":
        mark_order_paid(event["data"]["orderId"])
    return "", 200

Node.js

Create a payment
const res = await fetch("https://plaxswap.com/api/v1/payments", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.PLAXSWAP_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": `order-${order.id}`,
  },
  body: JSON.stringify({ amount: order.total, currency: "USD", receive: "usdc-base", orderId: order.id, returnUrl: `https://shop.example/orders/${order.id}` }),
});
if (!res.ok) throw new Error((await res.json()).error.message);
const payment = await res.json();
redirect(payment.checkoutUrl);

Errors

Every error is { "error": { "code", "message", "details"? } } with a matching HTTP status. The message is plain English. details.field names the field when there is one.

CodeHTTPMeaning
invalid_api_key401The key is missing, mistyped, or not a merchant key.
key_disabled401The key was switched off. Use another key or switch it on from your merchant page.
merchant_disabled403Your account is switched off. Write to support.
invalid_amount400amount is not a plain positive number.
invalid_currency400currency is not a fiat code we convert, nor a coin we know.
invalid_receive_coin400receive is not a coin the gateway pays out in. details.allowed lists them.
invalid_address400The receiving address failed its format or checksum check, or is missing for that coin.
address_not_allowed403The receiving address failed the sanctions or risk screen.
amount_out_of_range400The payment is below about 1 US dollar or above the coin's limit.
invalid_url400returnUrl or cancelUrl is not an https address.
invalid_expiry400expiresInMinutes is outside 15 to 1440.
invalid_metadata400metadata has more than 20 keys, a bad key name or a value over 500 characters.
idempotency_conflict409The Idempotency-Key was used before with a different body.
not_found404No such payment for this key (test keys see only test payments).
not_cancellable409Coins arrived, or the payment is closed.
rate_limited429Too many requests with one key in a minute.
service_paused503New payments are paused for a moment. Try again later.
rate_unavailable503No current price to convert your currency. Try again in a minute.

Endpoint reference

Payment gateway

MethodPathWhat it does
GET/api/v1/merchant/swapsOwner wallet conversion history and auto-conversion policy
POST/api/v1/merchant/swapsConfirm a quoted wallet conversion batch
POST/api/v1/merchant/swaps/quotePreview a merchant balance swap or consolidation
POST/api/v1/merchant/swaps/cancelCancel an unstarted wallet conversion
PATCH/api/v1/merchant/swaps/preferencesSet or revoke auto-conversion consent
GET/api/v1/paymentsList your payments
POST/api/v1/paymentsCreate a payment
GET/api/v1/payments/{id}A payment as it stands now
POST/api/v1/payments/{id}/cancelCancel a payment nothing was paid to
POST/api/v1/payments/{id}/simulateTest mode: act out a payment's outcome
GET/api/v1/gateway/coinsCoins you can receive, and what payers can pay each with

Merchant account

MethodPathWhat it does
GET/api/v1/merchant/overviewFiltered settled income, coin prices and orders
GET/api/v1/merchant/securityAuthenticator status and recovery-code count
POST/api/v1/merchant/securityEnroll or recover a mandatory merchant authenticator
POST/api/v1/merchant/security/approveApprove one exact account or withdrawal request with TOTP
GET/api/v1/merchant/walletBalances, ledger activity and withdrawals
POST/api/v1/merchant/onboardingComplete business profile
POST/api/v1/merchant/withdrawals/quotePreview a withdrawal and network fee
POST/api/v1/merchant/withdrawalsConfirm the exact withdrawal preview
GET/api/v1/merchant/notificationsNotification inbox and channel preferences
PATCH/api/v1/merchant/notificationsMark inbox items read or change opted-in channels
GET/api/v1/merchantYour merchant account
PATCH/api/v1/merchantChange your account's settings
POST/api/v1/merchant/keysCreate another key
PATCH/api/v1/merchant/keys/{id}Switch a key on or off
POST/api/v1/merchant/webhook-secretA new webhook signing secret
GET/api/v1/merchant/webhooksRecent webhook deliveries
POST/api/v1/merchant/webhooks/testSend a signed ping webhook
POST/api/v1/merchant/webhooks/{id}/resendSend a webhook again
POST/api/v1/merchants/signupLegacy signup endpoint (retired)

Checkout

MethodPathWhat it does
GET/api/v1/checkout/{id}The hosted checkout's view of a payment
POST/api/v1/checkout/{id}/quoteHow much to send with one coin
POST/api/v1/checkout/{id}/payPay with a coin
POST/api/v1/checkout/{id}/simulateTest mode: the checkout's outcome buttons

Every field of every request and answer is in the OpenAPI document.

Going live

  1. Check the coin you receive on your merchant page. Payments are added to your merchant balance; withdraw to your own wallet from there.
  2. Make a test payment, act out paid, partly paid and expired, and check that your webhook handler does the right thing.
  3. Check the signature on every webhook, and read the payment from the API before you ship.
  4. Swap the test key for the live key on your server.
  5. Read the merchant terms.