Payout API
Gửi rút tiền từ số dư merchant đến bất kỳ địa chỉ blockchain nào.
Payout API cho phép bạn rút tiền theo chương trình từ số dư merchant đến bất kỳ địa chỉ blockchain nào.
Đối với tất cả endpoint Payout, bạn phải sử dụng một Payout API key riêng để tạo chữ ký sign. Key này khác với API key thông thường và phải được tạo trong phần cài đặt project.
Tạo rút tiền
Tạo một yêu cầu rút tiền từ số dư merchant.
/v1/payoutTham số yêu cầu
| Field | Type | Required | Description |
|---|---|---|---|
currency | string | yes | Đồng tiền rút (xem References) |
network | string | yes | Mã mạng (xem References) |
amount | string | yes | Số tiền rút |
to_address | string | yes | Địa chỉ blockchain nhận tiền |
order_id | string | no | Idempotency key — duy nhất trong phạm vi project. Một POST lặp lại với cùng order_id sẽ không tạo rút tiền mới — thay vào đó trả về cái đã tồn tại |
url_callback | string | no | URL nhận webhook rút tiền. Bỏ trống để tắt webhook cho rút tiền này |
memo | string | null | no | Destination tag / memo. Hiện tại chỉ được dùng bởi mạng TON và SOL; tối đa 255 ký tự |
from_currency | string | no | Số dư nguồn sẽ bị trừ và tự động quy đổi sang currency tại thời điểm rút tiền. Cho phép bạn rút bằng các tài sản biến động (BTC, ETH, …) trong khi vẫn giữ số dư bằng stablecoin như USDT — bạn không phải tự nắm giữ tiền điện tử biến động. Truyền "USDT" để trừ từ số dư USDT |
fee_option | string | no | Cách thu phí. deduct (mặc định) — phí mạng + phí nền tảng được trừ vào amount, người nhận nhận được amount - fees. add — phí được cộng thêm, merchant bị trừ amount + fees, người nhận nhận đúng amount |
Idempotency. Trong một project, một rút tiền là duy nhất theo order_id. Gửi lại cùng POST với cùng order_id là an toàn — API trả về rút tiền hiện có thay vì tạo bản trùng lặp. Hãy luôn truyền order_id cho các rút tiền production.
Ví dụ yêu cầu
curl -X POST https://api.2328.io/api/v1/payout \
-H "Content-Type: application/json" \
-H "User-Agent: MyShop/1.0 (+https://myshop.example)" \
-H "project: YOUR_PROJECT_UUID" \
-H "sign: YOUR_HMAC_SIGNATURE" \
-d '{"currency":"TRX","network":"TRX-TRC20","amount":"1.00","to_address":"TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t","order_id":"9ed25264-8be4-439f-acf5-2a8732538d27","url_callback":"https://your-site.com/webhook/payout","memo":null,"fee_option":"deduct"}'<?php
function apiSign(string $body, string $apiKey): string {
return hash_hmac('sha256', base64_encode($body), $apiKey);
}
$project = 'YOUR_PROJECT_UUID';
$apiKey = 'YOUR_PAYOUT_API_KEY';
$data = [
'currency' => 'TRX',
'network' => 'TRX-TRC20',
'amount' => '1.00',
'to_address' => 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t',
'order_id' => '9ed25264-8be4-439f-acf5-2a8732538d27',
'url_callback' => 'https://your-site.com/webhook/payout',
'memo' => null,
'fee_option' => 'deduct',
];
$body = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$sign = apiSign($body, $apiKey);
$ch = curl_init('https://api.2328.io/api/v1/payout');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'User-Agent: MyShop/1.0 (+https://myshop.example)',
"project: $project",
"sign: $sign",
],
]);
$response = json_decode(curl_exec($ch), true);import { createHmac } from "crypto";
function apiSign(body, apiKey) {
const base64 = Buffer.from(body, "utf8").toString("base64");
return createHmac("sha256", apiKey).update(base64).digest("hex");
}
const PROJECT_UUID = "YOUR_PROJECT_UUID";
const PAYOUT_API_KEY = process.env.PAYOUT_API_KEY;
const data = {
currency: "TRX",
network: "TRX-TRC20",
amount: "1.00",
to_address: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
order_id: "9ed25264-8be4-439f-acf5-2a8732538d27",
url_callback: "https://your-site.com/webhook/payout",
memo: null,
fee_option: "deduct",
};
const body = JSON.stringify(data);
const sign = apiSign(body, PAYOUT_API_KEY);
const res = await fetch("https://api.2328.io/api/v1/payout", {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": "MyShop/1.0 (+https://myshop.example)",
project: PROJECT_UUID,
sign,
},
body,
});
const json = await res.json();import json
import hmac
import hashlib
import base64
import httpx
def api_sign(body: str, api_key: str) -> str:
b64 = base64.b64encode(body.encode("utf-8")).decode()
return hmac.new(api_key.encode(), b64.encode(), hashlib.sha256).hexdigest()
PROJECT_UUID = "YOUR_PROJECT_UUID"
PAYOUT_API_KEY = "YOUR_PAYOUT_API_KEY"
data = {
"currency": "TRX",
"network": "TRX-TRC20",
"amount": "1.00",
"to_address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"order_id": "9ed25264-8be4-439f-acf5-2a8732538d27",
"url_callback": "https://your-site.com/webhook/payout",
"memo": None,
"fee_option": "deduct",
}
body = json.dumps(data, separators=(",", ":"), ensure_ascii=False)
sign = api_sign(body, PAYOUT_API_KEY)
r = httpx.post(
"https://api.2328.io/api/v1/payout",
headers={
"Content-Type": "application/json",
"User-Agent": "MyShop/1.0 (+https://myshop.example)",
"project": PROJECT_UUID,
"sign": sign,
},
content=body.encode("utf-8"),
)
response = r.json()package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"net/http"
)
func apiSign(body []byte, apiKey string) string {
b64 := base64.StdEncoding.EncodeToString(body)
h := hmac.New(sha256.New, []byte(apiKey))
h.Write([]byte(b64))
return hex.EncodeToString(h.Sum(nil))
}
func marshalCanonical(v any) ([]byte, error) {
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
if err := enc.Encode(v); err != nil {
return nil, err
}
return bytes.TrimRight(buf.Bytes(), "\n"), nil
}
type CreatePayout struct {
Currency string `json:"currency"`
Network string `json:"network"`
Amount string `json:"amount"`
ToAddress string `json:"to_address"`
OrderID string `json:"order_id"`
URLCallback string `json:"url_callback"`
Memo *string `json:"memo"`
FeeOption string `json:"fee_option"`
}
func main() {
const projectUUID = "YOUR_PROJECT_UUID"
const payoutAPIKey = "YOUR_PAYOUT_API_KEY"
data := CreatePayout{
Currency: "TRX",
Network: "TRX-TRC20",
Amount: "1.00",
ToAddress: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
OrderID: "9ed25264-8be4-439f-acf5-2a8732538d27",
URLCallback: "https://your-site.com/webhook/payout",
Memo: nil,
FeeOption: "deduct",
}
body, err := marshalCanonical(data)
if err != nil {
panic(err)
}
sign := apiSign(body, payoutAPIKey)
req, _ := http.NewRequest("POST",
"https://api.2328.io/api/v1/payout",
bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "MyShop/1.0 (+https://myshop.example)")
req.Header.Set("project", projectUUID)
req.Header.Set("sign", sign)
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
}Ví dụ phản hồi
{
"state": 0,
"result": {
"uuid": "019dea62-1727-72aa-ac2c-eaf2ade193ef",
"order_id": "9ed25264-8be4-439f-acf5-2a8732538d27",
"status": "pending",
"currency": "TRX",
"network": "TRX-TRC20",
"amount": "1.00",
"merchant_amount": "1",
"network_amount": "0.89",
"amount_usd": "0.33",
"to_address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"memo": null,
"txid": null,
"block_number": null,
"error_type": null,
"created_at": "2026-05-02T23:29:50+03:00",
"updated_at": "2026-05-02T23:29:50+03:00"
}
}Phí. Mặc định fee_option: deduct — phí mạng + phí nền tảng được trừ vào amount (người nhận nhận amount - fees). Truyền fee_option: add để cộng phí lên trên — người nhận nhận đúng amount và merchant bị trừ amount + fees.
Tính toán rút tiền
Ước tính số tiền và phí rút mà không tạo rút tiền và không trừ số dư của bạn. Dùng để hiển thị cho người dùng số tiền chính xác họ sẽ nhận (hoặc trả) trước khi xác nhận.
/v1/payout/calcTham số yêu cầu
Giống hệt với Tạo rút tiền — cùng các trường, cùng chữ ký. order_id, url_callback, to_address và memo được chấp nhận nhưng bị bỏ qua: không có rút tiền nào được lưu và không có callback nào được gửi.
Ví dụ yêu cầu
curl -X POST https://api.2328.io/api/v1/payout/calc \
-H "Content-Type: application/json" \
-H "User-Agent: MyShop/1.0 (+https://myshop.example)" \
-H "project: YOUR_PROJECT_UUID" \
-H "sign: YOUR_HMAC_SIGNATURE" \
-d '{"currency":"USDT","network":"TRX-TRC20","amount":"100","fee_option":"add"}'Ví dụ phản hồi
{
"state": 0,
"result": {
"currency": "USDT",
"network": "TRX-TRC20",
"amount": "100",
"fee_option": "add",
"merchant_amount": "103.00000000",
"network_amount": "100",
"total_fee": "3.00000000",
"total_fee_usd": "3.00000000"
}
}Chỉ xem trước. Endpoint này chỉ đọc — không có số dư nào bị trừ và không có bản ghi rút tiền nào được tạo. Hãy gọi tuỳ ý để hiển thị chi tiết phí trong UI của bạn.
Trạng thái rút tiền
Lấy trạng thái của một yêu cầu rút tiền.
/v1/payout/status/{uuid}Tham số đường dẫn
| Field | Type | Required | Description |
|---|---|---|---|
uuid | string | yes | UUID rút tiền (lấy từ result.uuid khi tạo) |
Ví dụ phản hồi
{
"state": 0,
"result": {
"uuid": "019dff1f-0dbd-7277-8d45-271e7775388f",
"order_id": "4dfdcc84402b1185b71cbe399321533e",
"status": "completed",
"currency": "TRX",
"network": "TRX-TRC20",
"amount": "3.00",
"merchant_amount": "3.00",
"network_amount": "3.00",
"amount_usd": "1.04",
"to_address": "THauRv5tcucQRohXg8NiyGTk16DX1XQG5x",
"memo": null,
"txid": "9242e533703704ef3eaba840f70b4a26333e72c943377ee375fea17badb53def",
"block_number": null,
"error_type": null,
"created_at": "2026-05-07T00:08:38+03:00",
"updated_at": "2026-05-07T00:08:54+03:00",
"from_currency": "USDT",
"debited_amount": "1.050735",
"debited_currency": "USDT"
}
}Đối với yêu cầu GET này, chữ ký được tính từ thân rỗng:
hash_hmac('sha256', base64_encode(''), $apiKey)
Trường phản hồi
Các trường được trả về trong result từ POST /v1/payout và GET /v1/payout/status/{uuid}:
| Field | Type | Description |
|---|---|---|
uuid | string | UUID rút tiền do hệ thống cấp |
order_id | string | Mã định danh rút tiền nội bộ của bạn (duy nhất trong project) |
status | string | Trạng thái rút tiền hiện tại (xem bên dưới) |
currency | string | Đồng tiền rút |
network | string | Mã mạng |
amount | string | Số tiền rút theo yêu cầu |
merchant_amount | string | Số tiền bị trừ từ số dư merchant |
network_amount | string | Số tiền thực sự được gửi trên chuỗi (sau phí mạng + phí nền tảng) |
amount_usd | string | Giá trị quy đổi USD của số tiền rút |
to_address | string | Địa chỉ blockchain nhận tiền |
memo | string | null | Destination tag / memo (TON, SOL). Ngược lại là null |
txid | string | null | Hash giao dịch blockchain. null cho đến khi giao dịch được gửi |
block_number | int | null | Số block chứa giao dịch. null cho đến khi được đưa vào |
error_type | string | null | Lý do thất bại khi status = failed (xem Error types bên dưới). Ngược lại là null |
created_at | string (ISO 8601) | Thời điểm tạo rút tiền |
updated_at | string (ISO 8601) | Thời điểm trạng thái thay đổi gần nhất |
from_currency | string | null | Số dư nguồn mà khoản rút tiền đã bị trừ khi sử dụng quy đổi tự động (ví dụ USDT cho khoản rút BTC). null nếu không có quy đổi nào xảy ra |
debited_amount | string | null | Số tiền thực sự bị trừ từ số dư nguồn sau quy đổi. Chỉ có khi sử dụng tự quy đổi |
debited_currency | string | null | Đồng tiền của debited_amount — số dư từ đó tiền bị trừ |
Trạng thái rút tiền
Trường status có thể nhận các giá trị sau:
| Status | Description |
|---|---|
pending | Đã tạo, chờ xử lý |
completed | Hoàn tất thành công — txid đã được đặt |
failed | Lỗi gửi — xem error_type |
cancelled | Đã hủy |
Loại lỗi
Khi status = failed, trường error_type mô tả lý do:
| Code | Description |
|---|---|
aml_risk | Rút tiền bị chặn bởi kiểm tra rủi ro AML (địa chỉ người nhận bị đánh dấu rủi ro cao) |
Thông báo webhook
Khi trạng thái của một rút tiền thay đổi, hệ thống sẽ gửi webhook POST đến URL url_callback được truyền khi tạo rút tiền. Nếu không cung cấp url_callback, sẽ không có webhook nào được gửi cho rút tiền đó.
- Method:
POST - Content-Type:
application/json - Chữ ký: trường
signtrong thân yêu cầu, được tính bằng Payout API key (cùng key dùng để ký yêu cầu rút tiền).
Payload phản chiếu object result từ GET /v1/payout/status/{uuid} cộng với một trường sign để xác minh.
Payload
{
"uuid": "019dff1f-0dbd-7277-8d45-271e7775388f",
"order_id": "4dfdcc84402b1185b71cbe399321533e",
"status": "completed",
"currency": "TRX",
"network": "TRX-TRC20",
"amount": "3.00",
"merchant_amount": "3.00",
"network_amount": "3.00",
"amount_usd": "1.04",
"to_address": "THauRv5tcucQRohXg8NiyGTk16DX1XQG5x",
"memo": null,
"txid": "9242e533703704ef3eaba840f70b4a26333e72c943377ee375fea17badb53def",
"block_number": null,
"error_type": null,
"created_at": "2026-05-07T00:08:38+03:00",
"updated_at": "2026-05-07T00:08:54+03:00",
"from_currency": "USDT",
"debited_amount": "1.050735",
"debited_currency": "USDT",
"sign": "925ad7bf3d6841864101f7cc2c7e30652e70a06cdb04dbe07a0129480000ce4a"
}Xác minh chữ ký. Sử dụng cùng thuật toán như với webhook thanh toán, nhưng ký bằng Payout API key thay vì API key thông thường. Loại bỏ trường sign, mã hóa JSON phần payload còn lại, mã hóa Base64, sau đó tính hash_hmac('sha256', $base64, $payoutApiKey) và so sánh với sign nhận được.