TopUP API
Version 1. Every field in the API reference is checked automatically against the implementation, so the reference and the API cannot drift apart.
Overview
- Network and token: USDT on TRON (TRC20) only. Amounts are decimal strings with up to 6 decimals, e.g. "100.5".
- Base URL:
https://{BASE_URL}/api/v1/— every path ends with "/". Requests without the trailing slash are redirected, which some HTTP clients do not follow for POST. - Collect by customer: each of your customers gets one permanent address (deposit callbacks). With order mode on, create orders for a customer and deposits are matched to them automatically (order-matched callbacks).
- A deposit counts after 19 confirmations (about 1 minute). Callbacks are normally sent within 2 minutes of payment.
- Every withdrawal is reviewed manually. Working hours are 8:00–23:00 (UTC+8): requests in working hours are processed within 1 hour; others at the start of the next working period.
| Endpoint | Purpose |
|---|---|
POSTorders/ | Create an order |
GETorders/ | Get an order |
GETorders/list/ | List orders |
POSTorders/match/ | Match a deposit to an order |
POSTorders/unmatch/ | Unmatch a deposit |
POSTcustomers/ | Create or update a customer |
GETcustomers/ | Get a customer |
GETcustomers/list/ | List customers |
GETcustomers/stats/ | Customer statistics |
GETbalance/ | Balance |
GETledger/ | Ledger |
GETdeposits/ | Deposits |
POSTwithdrawals/ | Request a withdrawal |
GETwithdrawals/ | Get a withdrawal |
POSTwithdrawals/cancel/ | Cancel a withdrawal |
POSTpayouts/batch/ | Submit a batch payout |
GETpayouts/batch/ | Get a batch payout |
Authentication
Every request carries three headers. The Secret is shown once in the dashboard (API settings); keep it on your server only.
| Header | Value |
|---|---|
X-TopUP-Key | Your API Key |
X-TopUP-Timestamp | Unix time in seconds; must be within 5 minutes of server time |
X-TopUP-Signature | hex( HMAC-SHA256( Secret, timestamp + "\n" + METHOD + "\n" + path_and_query + "\n" + hex(SHA-256(body)) ) ) — use an empty body for GET |
Idempotency-Key | Required on every POST (8–128 characters: letters, digits and _ . : -; a UUID works). Retrying with the same key and body returns the first result instead of creating a duplicate (kept 24 hours). A request that failed does not use up the key. |
Node.js
import crypto from 'node:crypto';
const BASE = process.env.TOPUP_BASE || 'https://{BASE_URL}/api/v1/';
const KEY = process.env.TOPUP_KEY, SECRET = process.env.TOPUP_SECRET;
async function topup(method, path, body) {
const ts = Math.floor(Date.now() / 1000).toString();
const raw = body ? JSON.stringify(body) : '';
const bodyHash = crypto.createHash('sha256').update(raw).digest('hex');
const url = new URL(path, BASE);
const toSign = [ts, method, url.pathname + url.search, bodyHash].join('\n');
const sig = crypto.createHmac('sha256', SECRET).update(toSign).digest('hex');
const res = await fetch(url, {
method,
headers: {
'content-type': 'application/json',
'X-TopUP-Key': KEY, 'X-TopUP-Timestamp': ts, 'X-TopUP-Signature': sig,
...(method === 'POST' ? { 'Idempotency-Key': crypto.randomUUID() } : {}),
},
body: raw || undefined,
});
return res.json();
}
const order = await topup('POST', 'orders/', { customer_id: 'user_88213', merchant_order_no: `ACME-${Date.now()}`, amount: '100.00' });
console.log(order.pay_url);
Python
import hashlib, hmac, json, os, time, uuid, requests
BASE = os.environ.get("TOPUP_BASE_URL", "https://{BASE_URL}")
KEY, SECRET = os.environ["TOPUP_KEY"], os.environ["TOPUP_SECRET"].encode()
def topup(method, path, body=None):
ts = str(int(time.time()))
raw = json.dumps(body, separators=(",", ":")) if body else ""
body_hash = hashlib.sha256(raw.encode()).hexdigest()
to_sign = "\n".join([ts, method, "/api/v1/" + path, body_hash])
sig = hmac.new(SECRET, to_sign.encode(), hashlib.sha256).hexdigest()
headers = {"content-type": "application/json", "X-TopUP-Key": KEY,
"X-TopUP-Timestamp": ts, "X-TopUP-Signature": sig}
if method == "POST":
headers["Idempotency-Key"] = str(uuid.uuid4())
return requests.request(method, BASE + "/api/v1/" + path, data=raw or None, headers=headers).json()
print(topup("GET", "balance/"))
Orders
POST /api/v1/orders/
{
"customer_id": "user_88213", // required: your own ID for this customer
"customer_name": "Lin Trading", // optional, used only if the customer is new
"customer_email": "lin@lintrading.example",
"merchant_order_no": "ACME-88513",
"amount": "100.00"
}{
"order_no": "ORD-20260929-0413",
"merchant_order_no": "ACME-88513",
"customer_id": "user_88213",
"amount": "100.00",
"matched": "0.00",
"address": "TMa7yK2d9Qp4XvB8sN1cF6rH3wZ5jL0eUt", // the customer's permanent address
"network": "TRON",
"token": "USDT",
"status": "pending",
"created_at": "2026-09-29T09:00:00.000Z",
"expires_at": "2026-09-29T09:30:00.000Z",
"deposits": [],
"pay_url": "https://{BASE_URL}/pay/ORD-20260929-0413/"
}- customer_id is required. A new customer_id creates the customer and its permanent address. merchant_order_no must be unique in your account.
- The expiry time and matching thresholds are set for your account by the platform (shown in API settings). Orders require order mode; otherwise the endpoint returns 403 order_mode_disabled.
- Send your customer to pay_url, or show the address yourself.
- The response can already show status completed if earlier deposits from this customer matched when the order was created. See Matching rules.
Customers
POST /api/v1/customers/
{ "customer_id": "user_88213", "name": "Lin Trading", "email": "lin@lintrading.example" }
→ {
"customer_id": "user_88213", "name": "Lin Trading", "email": "lin@lintrading.example", "remark": "",
"address": "TXk8fQ2m9sR7ZcY1hN4vB6pW3aLd5Z3wQe",
"total": "0.00", "count": 0, "fees": "0.00", "unmatched": "0.00",
"last_payment_at": null, "created_at": "2026-09-11T02:10:00.000Z"
}- Each customer has one permanent address. Every payment to it belongs to that customer, with or without an order. customer_id cannot be changed.
- Payments under 1 USDT are not credited, not matched, trigger no callback and do not appear in your deposits or exports (they are usually spam "dust" transfers).
- customers/stats/ returns received, count, fees and unmatched for a period.
Matching rules
Matching only links deposits to orders; it never changes your balance. It happens only within the same customer, in both directions:
- Deposit first reaches an open order: the deposit goes to the open order where the running total lands between the lower (L) and upper (H) thresholds; otherwise to the oldest open order.
- Order created after deposits: unmatched deposits from the look-back window are added from the oldest; if the total reaches L without exceeding H they are matched, otherwise none are.
- Status: total < L = partial (keeps waiting), L ≤ total ≤ H = completed, total > H = overpaid; at expiry an order below L becomes expired (nothing received) or expired_partial.
- Payments after expiry stay unmatched; match them manually with orders/match/ if needed. Avoid two open orders with similar amounts for the same customer.
Balance & ledger
GET balance/ returns { "available": "48215.40", "frozen": "1002.00" }. The ledger is append-only; each entry has type deposit, fee_in, freeze, unfreeze, withdraw or fee_out. Lists are paginated with cursor.
Withdrawals
POST /api/v1/withdrawals/
{
"kind": "payout", // "payout" = to your customer, "cashout" = to your registered wallet
"customer_id": "user_88213", // optional, for payouts
"to": "TXk8fQ2m9sR7ZcY1hN4vB6pW3aLd5Z3wQe",
"amount": "1000.00",
"merchant_ref": "PAYOUT-7781"
}
→ {
"withdrawal_no": "WD-20260929-7F3A21", "kind": "payout", "customer_id": "user_88213",
"to": "TXk8fQ2m9sR7ZcY1hN4vB6pW3aLd5Z3wQe", "amount": "1000.00", "fee": "2.00", // 1002.00 is frozen
"status": "pending", "txid": null, "reason": null, "merchant_ref": "PAYOUT-7781", "source": "api",
"created_at": "2026-09-29T09:12:00.000Z"
}- API withdrawals require an IP whitelist (API settings). Without one, the endpoint returns 403 ip_not_allowed.
- Amount + fee is frozen on submit and released if the request is rejected or cancelled.
- Status: pending → approved → broadcast → completed, or rejected / cancelled / failed. Each change sends a withdrawal.updated callback.
Callbacks
POST https://your-server.example/callback
X-TopUP-Timestamp: 1790665800
X-TopUP-Signature: 5f1c…
{
"id": "evt_9f2a02",
"type": "order.matched", // deposit | order.matched | withdrawal.updated
"created_at": "2026-09-29T09:10:03Z",
"data": {
"order_no": "ORD-20260929-0409",
"merchant_order_no": "ACME-88509",
"customer_id": "user_88213",
"status": "completed", // pending | partial | completed | overpaid | expired | expired_partial
"amount": "500.00",
"matched": "500.00",
"created_at": "2026-09-29T09:05:00.000Z",
"expires_at": "2026-09-29T09:35:00.000Z",
"deposits": [{ "id": "1042", "txid": "a3c1…", "amount": "500.00", "matched_by": "deposit", "time": "2026-09-29T09:10:00.000Z" }],
"manual": false
}
}Signature: hex( HMAC-SHA256( Secret, timestamp + "\n" + raw_body ) ). Reply with any 2xx status. Otherwise we retry 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h after the first attempt, then stop; you can resend from the dashboard. The same event can arrive more than once: deduplicate by id.
import crypto from 'node:crypto';
// Verify a TopUP callback. rawBody must be the exact request body you received,
// not an object you parsed and serialised again.
export function verifyCallback(secret, timestamp, signature, rawBody) {
const expected = crypto.createHmac('sha256', secret).update(`${timestamp}\n${rawBody}`).digest('hex');
const a = Buffer.from(expected), b = Buffer.from(String(signature || ''));
const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300;
return fresh && a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express example:
// app.post('/callback', express.raw({ type: 'application/json' }), (req, res) => {
// if (!verifyCallback(SECRET, req.get('X-TopUP-Timestamp'), req.get('X-TopUP-Signature'), req.body)) return res.sendStatus(401);
// const event = JSON.parse(req.body); // the same event may arrive more than once: deduplicate by event.id
// res.sendStatus(200);
// });
Callback body
| Field | Type | Description |
|---|---|---|
id | string | Event ID; deduplicate by it |
type | string | deposit, order.matched or withdrawal.updated |
created_at | timestamp | When the event was created |
data | object | Event data, see below |
deposit
A payment of 1 USDT or more to a customer address is credited. data fields:
| Field | Type | Description |
|---|---|---|
customer_id | string | Your customer ID |
chain | string | Chain of the payment: eth, bsc or tron |
address | string | Receiving address |
txid | string | On-chain transaction ID |
amount | amount | Amount received |
fee | amount | Collection fee |
credited | amount | Amount added to your balance |
order_no | string | Order it was matched to(may be null) |
time | timestamp | Block time of the payment |
order.matched
An order changes because deposits were matched or unmatched. data fields:
| Field | Type | Description |
|---|---|---|
order_no | string | Order number assigned by TopUP |
merchant_order_no | string | Your order number |
customer_id | string | Your customer ID |
amount | amount | Order amount |
matched | amount | Total of the deposits matched to this order |
status | string | pending, partial, completed, overpaid, expired or expired_partial (see Matching rules) |
created_at | timestamp | Created at |
expires_at | timestamp | Expiry time |
deposits | array | Deposits matched to this order (OrderDeposit objects) |
manual | boolean | true when matched or unmatched manually |
withdrawal.updated
A withdrawal is created or changes status. data fields:
| Field | Type | Description |
|---|---|---|
withdrawal_no | string | Withdrawal number |
kind | string | payout (to your customer) or cashout (to your registered wallet) |
chain | string | Chain it is paid on: eth, bsc or tron |
batch_no | string | Batch it belongs to (batch payouts); null for single withdrawals(may be null) |
customer_id | string | Customer, for payouts(may be null) |
to | string | Destination address on that chain |
amount | amount | Amount sent |
fee | amount | Withdrawal fee (amount + fee is frozen on submit) |
status | string | pending, approved, broadcast, completed, rejected, cancelled or failed |
txid | string | On-chain transaction ID once broadcast(may be null) |
reason | string | Rejection or failure reason(may be null) |
merchant_ref | string | Your reference(may be null) |
source | string | api or web (merchant dashboard) |
created_at | timestamp | Created at |
API reference
Amounts are decimal strings in USDT (e.g. "100.50"); times are ISO 8601 in UTC. Lists return { items, next_cursor }: pass next_cursor as cursor to get the next page.
OrderDeposit
Used in the deposits array of an order.
| Field | Type | Description |
|---|---|---|
id | string | Deposit ID |
txid | string | On-chain transaction ID |
amount | amount | Amount received |
matched_by | string | How it was matched: deposit (payment arrived after the order), order (order created after the payment) or manual |
time | timestamp | Block time of the payment |
POST/api/v1/orders/ · Create an order
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
customer_id | string | Yes | Your ID for the customer, 1–128 characters: letters, digits and _ . @ -. A new ID creates the customer (no address yet: addresses are generated per chain when the customer makes a deposit) |
merchant_order_no | string | Yes | Your order number, 1–64 characters: letters, digits, _ and -. Unique in your account |
amount | amount | Yes | Order amount in USDT, up to 6 decimals, e.g. "100.5" |
customer_name | string | No | Used only when the customer is new |
customer_email | string | No | Used only when the customer is new |
chain | string | No | eth or bsc: the payer can only pay on this chain, and the customer's address on this chain is generated now. Leave out to let the payer choose on the payment page (recommended); the address is generated when they choose |
Response
| Field | Type | Description |
|---|---|---|
order_no | string | Order number assigned by TopUP |
merchant_order_no | string | Your order number |
customer_id | string | Your customer ID |
amount | amount | Order amount |
matched | amount | Total of the deposits matched to this order |
status | string | pending, partial, completed, overpaid, expired or expired_partial (see Matching rules) |
created_at | timestamp | Created at |
expires_at | timestamp | Expiry time |
deposits | array | Deposits matched to this order (OrderDeposit objects) |
address | string | Address to pay to on the order's chain; null when no chain was given (the payer picks one on the payment page)(may be null) |
network | string | Network of address: ETH, BSC or TRON; null when no chain was given(may be null) |
chain | string | Chain given when the order was created (eth or bsc); null = the payer chooses on the payment page(may be null) |
addresses | array | Addresses already generated for this customer: [{ chain, address }] (only the order's chain when one was given). ETH and BSC use the same address; pay only on the chain you choose(may be null) |
token | string | Always USDT |
pay_url | string | Hosted payment page for this order |
Errors: order_mode_disabled、duplicate_merchant_order_no、chain_not_enabled、invalid_param
GET/api/v1/orders/ · Get an order
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
order_no | string | No | Order number (either this or merchant_order_no) |
merchant_order_no | string | No | Your order number |
Response
| Field | Type | Description |
|---|---|---|
order_no | string | Order number assigned by TopUP |
merchant_order_no | string | Your order number |
customer_id | string | Your customer ID |
amount | amount | Order amount |
matched | amount | Total of the deposits matched to this order |
status | string | pending, partial, completed, overpaid, expired or expired_partial (see Matching rules) |
created_at | timestamp | Created at |
expires_at | timestamp | Expiry time |
deposits | array | Deposits matched to this order (OrderDeposit objects) |
address | string | Address to pay to on the order's chain; null when no chain was given (the payer picks one on the payment page)(may be null) |
network | string | Network of address: ETH, BSC or TRON; null when no chain was given(may be null) |
chain | string | Chain given when the order was created (eth or bsc); null = the payer chooses on the payment page(may be null) |
addresses | array | Addresses already generated for this customer: [{ chain, address }] (only the order's chain when one was given). ETH and BSC use the same address; pay only on the chain you choose(may be null) |
token | string | Always USDT |
pay_url | string | Hosted payment page for this order |
Errors: not_found
GET/api/v1/orders/list/ · List orders
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
status | string | No | Filter by status |
customer_id | string | No | Filter by customer |
cursor | string | No | next_cursor from the previous page |
limit | integer | No | Page size, 1–100 (default 50) |
Response: { items, next_cursor }; each item:
| Field | Type | Description |
|---|---|---|
order_no | string | Order number |
merchant_order_no | string | Your order number |
customer_id | string | Your customer ID |
amount | amount | Order amount |
matched | amount | Matched total |
status | string | Order status |
created_at | timestamp | Created at |
expires_at | timestamp | Expiry time |
POST/api/v1/orders/match/ · Match a deposit to an order
Changes only the link between the deposit and the order; the balance does not change. Sends an order.matched callback with manual = true.
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
order_no | string | Yes | Order number |
deposit_id | string | Yes | Deposit ID from deposits/ (same customer, not yet matched) |
Response
| Field | Type | Description |
|---|---|---|
order_no | string | Order number assigned by TopUP |
merchant_order_no | string | Your order number |
customer_id | string | Your customer ID |
amount | amount | Order amount |
matched | amount | Total of the deposits matched to this order |
status | string | pending, partial, completed, overpaid, expired or expired_partial (see Matching rules) |
created_at | timestamp | Created at |
expires_at | timestamp | Expiry time |
deposits | array | Deposits matched to this order (OrderDeposit objects) |
Errors: not_found、cannot_match
POST/api/v1/orders/unmatch/ · Unmatch a deposit
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
order_no | string | Yes | Order number |
deposit_id | string | Yes | A deposit currently matched to this order |
Response
| Field | Type | Description |
|---|---|---|
order_no | string | Order number assigned by TopUP |
merchant_order_no | string | Your order number |
customer_id | string | Your customer ID |
amount | amount | Order amount |
matched | amount | Total of the deposits matched to this order |
status | string | pending, partial, completed, overpaid, expired or expired_partial (see Matching rules) |
created_at | timestamp | Created at |
expires_at | timestamp | Expiry time |
deposits | array | Deposits matched to this order (OrderDeposit objects) |
Errors: not_found、not_matched
POST/api/v1/customers/ · Create or update a customer
Fields you leave out are not changed. No address is created here: each chain's address is generated when the customer first makes a deposit on it, and never changes.
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
customer_id | string | Yes | Your ID for the customer (see Create an order). Cannot be changed later |
name | string | No | Name, up to 200 characters |
email | string | No | |
remark | string | No | Note, up to 500 characters |
Response
| Field | Type | Description |
|---|---|---|
customer_id | string | Your customer ID |
name | string | Name |
email | string | Email (stored encrypted) |
remark | string | Your note |
address | string | Permanent TRON address (older field; only customers who already had one, otherwise null. Use addresses)(may be null) |
addresses | array | Permanent address on each chain used so far: [{ chain, address }], chain is eth, bsc or tron. Generated per chain when the customer makes a deposit (an order names the chain, or the payer picks it); empty for a new customer. ETH and BSC use the same address |
total | amount | Total credited deposits |
count | integer | Number of credited deposits |
fees | amount | Total collection fees |
unmatched | amount | Credited deposits not matched to any order |
last_payment_at | timestamp | Time of the last payment(may be null) |
created_at | timestamp | Created at |
Errors: invalid_param
GET/api/v1/customers/ · Get a customer
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
customer_id | string | Yes | Your customer ID |
Response
| Field | Type | Description |
|---|---|---|
customer_id | string | Your customer ID |
name | string | Name |
email | string | Email (stored encrypted) |
remark | string | Your note |
address | string | Permanent TRON address (older field; only customers who already had one, otherwise null. Use addresses)(may be null) |
addresses | array | Permanent address on each chain used so far: [{ chain, address }], chain is eth, bsc or tron. Generated per chain when the customer makes a deposit (an order names the chain, or the payer picks it); empty for a new customer. ETH and BSC use the same address |
total | amount | Total credited deposits |
count | integer | Number of credited deposits |
fees | amount | Total collection fees |
unmatched | amount | Credited deposits not matched to any order |
last_payment_at | timestamp | Time of the last payment(may be null) |
created_at | timestamp | Created at |
Errors: not_found
GET/api/v1/customers/list/ · List customers
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
q | string | No | Search by customer ID, name or email (partial, case-insensitive), or by exact address |
cursor | string | No | next_cursor from the previous page |
limit | integer | No | Page size, 1–100 (default 50) |
Response: { items, next_cursor }; each item:
| Field | Type | Description |
|---|---|---|
customer_id | string | Your customer ID |
name | string | Name |
email | string | Email (stored encrypted) |
remark | string | Your note |
address | string | Permanent TRON address (older field; only customers who already had one, otherwise null. Use addresses)(may be null) |
addresses | array | Permanent address on each chain used so far: [{ chain, address }], chain is eth, bsc or tron. Generated per chain when the customer makes a deposit (an order names the chain, or the payer picks it); empty for a new customer. ETH and BSC use the same address |
total | amount | Total credited deposits |
count | integer | Number of credited deposits |
fees | amount | Total collection fees |
unmatched | amount | Credited deposits not matched to any order |
last_payment_at | timestamp | Time of the last payment(may be null) |
created_at | timestamp | Created at |
GET/api/v1/customers/stats/ · Customer statistics
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
customer_id | string | Yes | Your customer ID |
from | timestamp | No | Start (ISO 8601); default: the beginning |
to | timestamp | No | End (ISO 8601); default: now |
Response
| Field | Type | Description |
|---|---|---|
customer_id | string | Your customer ID |
from | timestamp | Start of the period |
to | timestamp | End of the period |
total | amount | Credited deposits in the period |
count | integer | Number of credited deposits |
fees | amount | Collection fees |
unmatched | amount | Credited deposits not matched to an order |
last_payment_at | timestamp | Last payment in the period(may be null) |
payouts | amount | Completed payouts to this customer |
payout_count | integer | Number of completed payouts |
Errors: not_found、invalid_param
GET/api/v1/balance/ · Balance
Response
| Field | Type | Description |
|---|---|---|
available | amount | Available balance |
frozen | amount | Frozen by pending withdrawals |
currency | string | Always USDT |
GET/api/v1/ledger/ · Ledger
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
customer_id | string | No | Filter by customer |
cursor | string | No | next_cursor from the previous page |
limit | integer | No | Page size, 1–100 (default 50) |
Response: { items, next_cursor }; each item:
| Field | Type | Description |
|---|---|---|
id | string | Entry ID; also the cursor for the next page |
type | string | deposit, fee_in, freeze, unfreeze, withdraw or fee_out |
amount | amount | Change: positive adds, negative subtracts (withdraw and fee_out change the frozen balance) |
available_after | amount | Available balance after this entry |
frozen_after | amount | Frozen balance after this entry |
ref | string | Related deposit ID or withdrawal number |
customer_id | string | Related customer(may be null) |
created_at | timestamp | Created at |
GET/api/v1/deposits/ · Deposits
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
customer_id | string | No | Filter by customer |
matched | string | No | "false" = credited and not matched to an order; "true" = matched |
cursor | string | No | next_cursor from the previous page |
limit | integer | No | Page size, 1–100 (default 50) |
Response: { items, next_cursor }; each item:
| Field | Type | Description |
|---|---|---|
id | string | Deposit ID (used by orders/match/) |
customer_id | string | Your customer ID |
chain | string | Chain of the payment: eth, bsc or tron |
address | string | Receiving address |
txid | string | On-chain transaction ID |
amount | amount | Amount received |
fee | amount | Collection fee (never more than the amount) |
credited | amount | Amount added to your balance (amount − fee) |
result | string | Always credited. Payments under 1 USDT are not credited and are not listed |
order_no | string | Matched order(may be null) |
matched_by | string | deposit, order or manual(may be null) |
time | timestamp | Block time of the payment |
POST/api/v1/withdrawals/ · Request a withdrawal
Requires an IP whitelist. Amount + fee is frozen immediately. Every withdrawal is reviewed manually.
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
kind | string | Yes | payout (to your customer) or cashout (to a wallet registered in your onboarding form) |
chain | string | No | eth, bsc or tron (default tron) |
to | string | Yes | Destination address on that chain (ETH / BSC: 0x address, mixed case must match the EIP-55 checksum; TRON: Base58). For cashout it must be a wallet registered for that chain |
amount | amount | Yes | Amount in USDT, up to 6 decimals |
customer_id | string | No | For payouts: the customer being paid |
merchant_ref | string | No | Your reference, up to 64 characters |
Response
| Field | Type | Description |
|---|---|---|
withdrawal_no | string | Withdrawal number |
kind | string | payout (to your customer) or cashout (to your registered wallet) |
chain | string | Chain it is paid on: eth, bsc or tron |
batch_no | string | Batch it belongs to (batch payouts); null for single withdrawals(may be null) |
customer_id | string | Customer, for payouts(may be null) |
to | string | Destination address on that chain |
amount | amount | Amount sent |
fee | amount | Withdrawal fee (amount + fee is frozen on submit) |
status | string | pending, approved, broadcast, completed, rejected, cancelled or failed |
txid | string | On-chain transaction ID once broadcast(may be null) |
reason | string | Rejection or failure reason(may be null) |
merchant_ref | string | Your reference(may be null) |
source | string | api or web (merchant dashboard) |
created_at | timestamp | Created at |
Errors: ip_not_allowed、invalid_address、address_not_registered、insufficient_balance、invalid_param
GET/api/v1/withdrawals/ · Get a withdrawal
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
withdrawal_no | string | Yes | Withdrawal number |
Response
| Field | Type | Description |
|---|---|---|
withdrawal_no | string | Withdrawal number |
kind | string | payout (to your customer) or cashout (to your registered wallet) |
chain | string | Chain it is paid on: eth, bsc or tron |
batch_no | string | Batch it belongs to (batch payouts); null for single withdrawals(may be null) |
customer_id | string | Customer, for payouts(may be null) |
to | string | Destination address on that chain |
amount | amount | Amount sent |
fee | amount | Withdrawal fee (amount + fee is frozen on submit) |
status | string | pending, approved, broadcast, completed, rejected, cancelled or failed |
txid | string | On-chain transaction ID once broadcast(may be null) |
reason | string | Rejection or failure reason(may be null) |
merchant_ref | string | Your reference(may be null) |
source | string | api or web (merchant dashboard) |
created_at | timestamp | Created at |
Errors: not_found
POST/api/v1/withdrawals/cancel/ · Cancel a withdrawal
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
withdrawal_no | string | Yes | Withdrawal number; only pending withdrawals can be cancelled. Items of a batch payout cannot be cancelled one by one: cancel the batch (its not-yet-reviewed items) instead |
Response
| Field | Type | Description |
|---|---|---|
withdrawal_no | string | Withdrawal number |
kind | string | payout (to your customer) or cashout (to your registered wallet) |
chain | string | Chain it is paid on: eth, bsc or tron |
batch_no | string | Batch it belongs to (batch payouts); null for single withdrawals(may be null) |
customer_id | string | Customer, for payouts(may be null) |
to | string | Destination address on that chain |
amount | amount | Amount sent |
fee | amount | Withdrawal fee (amount + fee is frozen on submit) |
status | string | pending, approved, broadcast, completed, rejected, cancelled or failed |
txid | string | On-chain transaction ID once broadcast(may be null) |
reason | string | Rejection or failure reason(may be null) |
merchant_ref | string | Your reference(may be null) |
source | string | api or web (merchant dashboard) |
created_at | timestamp | Created at |
Errors: not_found、invalid_state、batch_item_cancel
POST/api/v1/payouts/batch/ · Submit a batch payout
Requires an IP whitelist. If any item is invalid the whole request is rejected with 422 invalid_rows and error.rows = [{ index, line, code, errors }] (index starts at 0, line = index + 1; code is the first problem of the item, errors lists all of them); nothing is frozen. Otherwise amount + fee of every item is frozen at once and each item is reviewed manually. Network fees are not charged separately. Body limit for this endpoint is 256 KB.
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
items | array | Yes | Up to 500 items: [{ chain, to, amount, customer_id?, merchant_ref? }] (chain: eth, bsc or tron; amount in USDT, up to 6 decimals) |
Response
| Field | Type | Description |
|---|---|---|
batch_no | string | Batch number |
source | string | api or web (uploaded sheet) |
status | string | pending (some items not reviewed yet), approved, partially_rejected, rejected, completed or cancelled |
count | integer | Number of items accepted |
total | amount | Total amount (fees not included) |
created_at | timestamp | Created at |
items | array | Items: [{ withdrawal_no, chain, to, amount, fee, status, txid, reason, customer_id, merchant_ref }], in the order submitted |
service | object | Processing hours (UTC+8 08:00–23:00): { open } and, outside the hours, next_open_at |
Errors: ip_not_allowed、too_many_items、invalid_rows、insufficient_balance、invalid_param
GET/api/v1/payouts/batch/ · Get a batch payout
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
batch_no | string | Yes | Batch number |
Response
| Field | Type | Description |
|---|---|---|
batch_no | string | Batch number |
source | string | api or web (uploaded sheet) |
status | string | pending (some items not reviewed yet), approved, partially_rejected, rejected, completed or cancelled |
count | integer | Number of items accepted |
total | amount | Total amount (fees not included) |
created_at | timestamp | Created at |
items | array | Items: [{ withdrawal_no, chain, to, amount, fee, status, txid, reason, customer_id, merchant_ref }], in the order submitted |
Errors: not_found
Errors
Errors return { "error": { "code": "…", "message": "…", "field": "…" } }.
| HTTP | code | Meaning |
|---|---|---|
| 400 | idempotency_key_required | POST without an Idempotency-Key (8–128 characters: letters, digits and _ . : -) |
| 400 | invalid_param | The request body is not valid JSON |
| 401 | invalid_signature | Missing or wrong signature, or unknown API Key |
| 401 | timestamp_expired | Timestamp more than 5 minutes from server time |
| 403 | merchant_disabled | Account disabled |
| 403 | order_mode_disabled | Order mode is not enabled for your account |
| 403 | ip_not_allowed | Withdrawal request not from a whitelisted IP (or no whitelist set) |
| 404 | not_found | Unknown endpoint, or no such object in your account |
| 409 | duplicate_merchant_order_no | merchant_order_no already used |
| 409 | idempotency_conflict | Same Idempotency-Key with a different request |
| 409 | request_in_progress | A request with this Idempotency-Key is still running; retry shortly |
| 409 | cannot_match | The deposit cannot be matched to this order (other customer, already matched, not credited, or order closed) |
| 409 | not_matched | The deposit is not matched to this order |
| 409 | chain_not_enabled | This chain is not open for new addresses yet (TRON comes in a later release) |
| 409 | invalid_state | Not allowed in the current state (e.g. cancelling a withdrawal that is no longer pending) |
| 409 | batch_item_cancel | The withdrawal belongs to a batch payout and cannot be cancelled on its own; cancel the batch instead |
| 413 | too_large | Request body over 32 KB (256 KB for POST payouts/batch/) |
| 422 | invalid_param | A parameter is missing or invalid; see error.field |
| 422 | invalid_address | Not a valid address for the chain |
| 422 | too_many_items | More than 500 items in one batch |
| 422 | invalid_rows | Some batch items are invalid; see error.rows = [{ index, line, code, errors }] |
| 422 | address_not_registered | cashout to a wallet that is not registered |
| 422 | insufficient_balance | Amount + fee exceeds available balance |
| 429 | rate_limited | More than 20 requests per second; retry later |
| 503 | wallet_not_initialized | Service not ready yet |
| 500 | internal_error | Unexpected error; safe to retry with the same Idempotency-Key |
Sandbox
A sandbox is not available yet. During the early-access period, integrate on production with small amounts (for example 1 USDT).