XoibitXoibitXoibitXoibit
登录注册

Xoibit Pay for developers

Accept USDT on the TRON (TRC-20) network on your website, app or shop. Payments land in your Xoibit wallet like any deposit, with no gateway fee on our side.

Open the merchant dashboard

它如何工作

  1. You create a payment (dashboard, shop QR or API). Your price gets a tiny unique tail, e.g. 25 becomes 25.0037.
  2. The customer sends exactly that amount of USDT TRC-20 to your own Xoibit deposit address (QR on the checkout page).
  3. Xoibit credits your wallet as a normal deposit, marks the payment paid and calls your webhook.
Only on-chain USDT TRC-20 payments are recognised automatically. A wrong amount, a late payment or an internal transfer still reaches your wallet: link it by hand under Unmatched in the dashboard.

1. Get set up

  1. Open Deposit > USDT > TRON (TRC-20) once so your deposit address exists, then turn on Xoibit Pay in the merchant dashboard.
  2. Save the webhook secret shown once, and set your webhook URL (https).
  3. For the API: create an API key with only the pay_invoice permission at API键. Keep its secret on your server only, never in a browser or app.

2. REST API

POST/api/v1/pay/invoicescreate a payment
GET/api/v1/pay/invoices/:uidread one
GET/api/v1/pay/invoices?order_id=…find by your order id

Base URL: https://gateway.xoibit.com. Headers: x-public-key, x-timestamp (ms, within 60 s), x-nonce (UUID v4, new for every POST), x-signature = hex HMAC-SHA256 of METHOD + PATH (no query string) + BODY + TIMESTAMP (+ NONCE for POST). BODY is your compact JSON, or {} for GET.

Body fields: amount (string, at most 2 decimals, required), order_id (unique per merchant: sending it again returns the same payment), description, customer_email, return_url (https), expires_in_min (15–1440), metadata (JSON object up to 2 kB), test (true = never matched to real money), fee_payer ('merchant' or 'customer'; empty = your default). With fee_payer customer the amount to pay is amount + fee_amount; the response shows both.

curl
# 1. sign: HMAC-SHA256(secret, METHOD + PATH + BODY + TIMESTAMP + NONCE), hex
BODY='{"amount":"25.00","order_id":"ORDER-1001","description":"T-shirt"}'
TS=$(date +%s%3N); NONCE=$(uuidgen | tr 'A-Z' 'a-z')
SIG=$(printf '%s' "POST/api/v1/pay/invoices${BODY}${TS}${NONCE}" | openssl dgst -sha256 -hmac "$XOIBIT_SECRET" -hex | sed 's/^.* //')

# 2. call
curl -s -X POST 'https://gateway.xoibit.com/api/v1/pay/invoices' \
  -H 'Content-Type: application/json' \
  -H "x-public-key: $XOIBIT_PUBLIC_KEY" -H "x-timestamp: $TS" -H "x-nonce: $NONCE" -H "x-signature: $SIG" \
  --data "$BODY"
Node.js
// Node 18+ (no packages needed)
const crypto = require("crypto");
const API = "https://gateway.xoibit.com";
const PUBLIC_KEY = process.env.XOIBIT_PUBLIC_KEY;
const SECRET = process.env.XOIBIT_SECRET;

async function xoibit(method, path, body) {
  const json = body ? JSON.stringify(body) : "{}";        // GET requests sign "{}"
  const ts = Date.now().toString();
  const nonce = crypto.randomUUID();
  const msg = method + path + json + ts + (method === "GET" ? "" : nonce);
  const sig = crypto.createHmac("sha256", SECRET).update(msg).digest("hex");
  const res = await fetch(API + path, {
    method,
    headers: { "Content-Type": "application/json", "x-public-key": PUBLIC_KEY, "x-timestamp": ts, "x-nonce": nonce, "x-signature": sig },
    body: method === "GET" ? undefined : json,
  });
  if (!res.ok) throw new Error(await res.text());
  return res.json();
}

// create a payment and send the customer to checkout_url
const inv = await xoibit("POST", "/api/v1/pay/invoices", {
  amount: "25.00", order_id: "ORDER-1001", description: "T-shirt",
  return_url: "https://shop.example.com/thanks",
});
console.log(inv.checkout_url, inv.amount_due, inv.address);

// read it back (the query string is NOT part of the signed path)
const again = await xoibit("GET", "/api/v1/pay/invoices/" + inv.uid);
PHP
<?php
$api = 'https://gateway.xoibit.com';
$public = getenv('XOIBIT_PUBLIC_KEY');
$secret = getenv('XOIBIT_SECRET');

function xoibit($method, $path, $body = null) {
  global $api, $public, $secret;
  // compact JSON, no escaped slashes / unicode: the server re-encodes the body the same way
  $json = $body === null ? '{}' : json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
  $ts = (string) round(microtime(true) * 1000);
  $nonce = sprintf('%04x%04x-%04x-4%03x-%04x-%04x%04x%04x', random_int(0, 0xffff), random_int(0, 0xffff), random_int(0, 0xffff),
    random_int(0, 0xfff), random_int(0, 0x3fff) | 0x8000, random_int(0, 0xffff), random_int(0, 0xffff), random_int(0, 0xffff));
  $msg = $method . $path . $json . $ts . ($method === 'GET' ? '' : $nonce);
  $sig = hash_hmac('sha256', $msg, $secret);
  $ch = curl_init($api . $path);
  curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => $method,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Content-Type: application/json', "x-public-key: $public", "x-timestamp: $ts", "x-nonce: $nonce", "x-signature: $sig"],
    CURLOPT_POSTFIELDS => $method === 'GET' ? null : $json,
  ]);
  $out = curl_exec($ch);
  return json_decode($out, true);
}

$inv = xoibit('POST', '/api/v1/pay/invoices', ['amount' => '25.00', 'order_id' => 'ORDER-1001']);
header('Location: ' . $inv['checkout_url']);
Python
import hashlib, hmac, json, os, time, uuid, requests

API = "https://gateway.xoibit.com"
PUBLIC_KEY = os.environ["XOIBIT_PUBLIC_KEY"]
SECRET = os.environ["XOIBIT_SECRET"].encode()

def xoibit(method, path, body=None):
    # compact JSON exactly like the server re-encodes it
    data = "{}" if body is None else json.dumps(body, separators=(",", ":"), ensure_ascii=False)
    ts = str(int(time.time() * 1000))
    nonce = str(uuid.uuid4())
    msg = method + path + data + ts + ("" if method == "GET" else nonce)
    sig = hmac.new(SECRET, msg.encode(), hashlib.sha256).hexdigest()
    headers = {"Content-Type": "application/json", "x-public-key": PUBLIC_KEY,
               "x-timestamp": ts, "x-nonce": nonce, "x-signature": sig}
    r = requests.request(method, API + path, headers=headers, data=None if method == "GET" else data.encode())
    r.raise_for_status()
    return r.json()

inv = xoibit("POST", "/api/v1/pay/invoices", {"amount": "25.00", "order_id": "ORDER-1001"})
print(inv["checkout_url"])
Response
{
  "uid": "inv_5f2c...",
  "order_id": "ORDER-1001",
  "status": "open",              // open | paid | paid_late | paid_manual | expired | cancelled
  "amount": "25",                // your price
  "amount_due": "25.0037",       // what the customer must send (unique tail)
  "currency": "USDT",
  "network": "TRC20",
  "address": "T...",             // your own Xoibit deposit address
  "checkout_url": "https://pay.xoibit.com/inv_5f2c...",
  "paid_amount": null,
  "txid": null,
  "paid_at": null,
  "expires_at": "2026-11-11T10:30:00.000Z",
  "created_at": "2026-11-11T10:00:00.000Z",
  "test": false,
  "metadata": { "cart_id": "abc" }
}

3. Webhooks

We POST JSON to your webhook URL with the headers X-Xoibit-Event, X-Xoibit-Delivery and X-Xoibit-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of t + '.' + raw body, keyed with your webhook secret>. Answer 2xx within 10 seconds. Otherwise we retry after 1 min, 5 min, 30 min, 2 h, 6 h and 24 h; you can also resend from the dashboard.

Events: invoice.paid, invoice.paid_late (paid after the time limit), invoice.paid_manual (you linked a deposit by hand), invoice.expired, invoice.test_paid and invoice.test (dashboard tests: never ship goods for these). Always check the signature, ignore data.test = true, and process each order once.

Node.js (Express)
// Express: keep the RAW body, the signature is over the exact bytes we sent
const crypto = require("crypto");
app.post("/xoibit-pay/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const header = req.get("X-Xoibit-Signature") || "";       // t=1731318000,v1=hex
  const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header);
  const body = req.body.toString("utf8");
  if (!m || Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return res.sendStatus(400);
  const want = crypto.createHmac("sha256", process.env.XOIBIT_WEBHOOK_SECRET).update(m[1] + "." + body).digest("hex");
  if (!crypto.timingSafeEqual(Buffer.from(want, "hex"), Buffer.from(m[2], "hex"))) return res.sendStatus(400);

  const evt = JSON.parse(body);              // { event, created_at, data: { uid, order_id, status, ... } }
  if (evt.data.test) return res.sendStatus(200);           // never ship a TEST payment
  if (evt.event === "invoice.paid" || evt.event === "invoice.paid_late" || evt.event === "invoice.paid_manual") {
    // mark evt.data.order_id paid ONCE (the same event can arrive again: be idempotent)
  }
  res.sendStatus(200);                       // any 2xx = delivered; anything else is retried
});
PHP
<?php
$body = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_XOIBIT_SIGNATURE'] ?? '';
if (!preg_match('/^t=(\d+),v1=([0-9a-f]{64})$/', $header, $m) || abs(time() - (int) $m[1]) > 300) { http_response_code(400); exit; }
$want = hash_hmac('sha256', $m[1] . '.' . $body, getenv('XOIBIT_WEBHOOK_SECRET'));
if (!hash_equals($want, $m[2])) { http_response_code(400); exit; }
$evt = json_decode($body, true);
if (!empty($evt['data']['test'])) { http_response_code(200); exit; }
if (in_array($evt['event'], ['invoice.paid', 'invoice.paid_late', 'invoice.paid_manual'], true)) {
  // mark $evt['data']['order_id'] paid once
}
http_response_code(200);
Python (Flask)
import hashlib, hmac, os, re, time
from flask import Flask, request, abort
app = Flask(__name__)

@app.post("/xoibit-pay/webhook")
def hook():
    body = request.get_data()                        # raw bytes
    m = re.fullmatch(r"t=(\d+),v1=([0-9a-f]{64})", request.headers.get("X-Xoibit-Signature", ""))
    if not m or abs(time.time() - int(m.group(1))) > 300:
        abort(400)
    want = hmac.new(os.environ["XOIBIT_WEBHOOK_SECRET"].encode(), m.group(1).encode() + b"." + body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(want, m.group(2)):
        abort(400)
    evt = request.get_json()
    if evt["data"].get("test"):
        return "", 200
    if evt["event"] in ("invoice.paid", "invoice.paid_late", "invoice.paid_manual"):
        pass  # mark evt["data"]["order_id"] paid once
    return "", 200

4. No code: links, shop QR and a button

  • Payment links: create one in the dashboard and send it on chat or e-mail.
  • Shop QR: one printed QR for your counter; the customer types the amount.
  • Button: paste this into any website.
<!-- a pay button for a fixed checkout link (create it from your server with the API) -->
<script src="https://pay.xoibit.com/button.js" async></script>
<a class="xoibit-pay-button" href="https://pay.xoibit.com/inv_5f2c...">Pay with USDT</a>

<!-- or a simple button to your shop page with an amount filled in (no server needed) -->
<a class="xoibit-pay-button" href="https://pay.xoibit.com/s/your-shop?amount=25.00&note=Order%201001">Pay 25 USDT</a>

Good to know

  • Ship goods only after a signed paid webhook or after GET shows status paid. The checkout page is for the customer, not proof of payment.
  • Customers paying from an exchange must make sure the RECEIVED amount (after that exchange's fee) is exact.
  • An amount stays reserved for 24 hours after a payment expires, so a late payment is never confused with a newer order.
  • Test payments use the same flow without real money: create one with test = true and mark it paid in the dashboard.