Payout API
ส่งการถอนจากยอดคงเหลือของผู้ค้าไปยังที่อยู่บล็อกเชนใด ๆ
Payout API ให้คุณถอนเงินจากยอดคงเหลือของผู้ค้าไปยังที่อยู่บล็อกเชนใด ๆ ได้ผ่านโปรแกรม
สำหรับ endpoint การถอนทั้งหมด คุณต้องใช้ Payout API key แยกต่างหากเพื่อสร้างลายเซ็น sign คีย์นี้แตกต่างจาก API key ปกติของคุณและต้องสร้างขึ้นในการตั้งค่าโปรเจกต์
สร้างการถอน
สร้างคำขอถอนเงินจากยอดคงเหลือของผู้ค้า
/v1/payoutพารามิเตอร์ของคำขอ
| Field | Type | Required | Description |
|---|---|---|---|
currency | string | yes | สกุลเงินที่ถอน (ดู References) |
network | string | yes | รหัสเครือข่าย (ดู References) |
amount | string | yes | จำนวนเงินที่ถอน |
to_address | string | yes | ที่อยู่บล็อกเชนของผู้รับ |
order_id | string | no | คีย์ idempotency — ไม่ซ้ำกันภายในโปรเจกต์ การส่ง POST ซ้ำด้วย order_id เดิมจะไม่สร้างการถอนใหม่ — ระบบจะคืนรายการที่มีอยู่แทน |
url_callback | string | no | URL สำหรับ Webhook ของการถอน เว้นว่างไว้เพื่อปิดใช้งาน Webhook สำหรับการถอนนี้ |
memo | string | null | no | Destination tag / memo ปัจจุบันใช้กับเครือข่าย TON และ SOL เท่านั้น สูงสุด 255 ตัวอักษร |
from_currency | string | no | ยอดคงเหลือต้นทางที่จะถูกตัดและแปลงอัตโนมัติเป็น currency ในขณะที่ทำการถอน ช่วยให้คุณถอนเป็นสินทรัพย์ที่มีความผันผวน (BTC, ETH, …) ได้โดยที่ยังเก็บยอดคงเหลือเป็นสเตเบิลคอยน์อย่าง USDT — คุณไม่ต้องถือคริปโตที่มีความผันผวนเอง ส่ง "USDT" เพื่อตัดจากยอดคงเหลือ USDT |
fee_option | string | no | วิธีคิดค่าธรรมเนียม deduct (ค่าเริ่มต้น) — ค่าธรรมเนียมเครือข่าย + แพลตฟอร์มหักจาก amount ผู้รับได้ amount - fees add — ค่าธรรมเนียมบวกเพิ่ม ผู้ค้าถูกตัด amount + fees ผู้รับได้ amount พอดี |
Idempotency ภายในโปรเจกต์เดียวกัน การถอนจะไม่ซ้ำตาม order_id การส่ง POST ซ้ำด้วย order_id เดิมเป็นเรื่อง ปลอดภัย — API จะคืนการถอนที่มีอยู่แทนการสร้างซ้ำ ในระบบโปรดักชันให้ระบุ order_id เสมอสำหรับการถอน
ตัวอย่างคำขอ
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()
}ตัวอย่างการตอบกลับ
{
"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"
}
}ค่าธรรมเนียม ค่าเริ่มต้น fee_option: deduct — ค่าธรรมเนียมเครือข่าย + แพลตฟอร์มจะถูกหักจาก amount (ผู้รับได้ amount - fees) ส่ง fee_option: add เพื่อบวกค่าธรรมเนียมเพิ่ม — ผู้รับจะได้ amount พอดี และผู้ค้าจะถูกตัด amount + fees
คำนวณการถอน
ประเมินจำนวนเงินและค่าธรรมเนียมการถอน โดยไม่สร้างการถอนจริง และไม่หักจากยอดคงเหลือ ใช้เพื่อแสดงจำนวนเงินที่ผู้ใช้จะได้รับ (หรือจ่าย) อย่างแม่นยำก่อนยืนยัน
/v1/payout/calcพารามิเตอร์ของคำขอ
เหมือนกับ สร้างการถอน ทุกประการ — ฟิลด์เดียวกัน วิธีลงนามเดียวกัน ฟิลด์ order_id, url_callback, to_address และ memo รับได้แต่จะถูกละเว้น: ไม่มีการบันทึกการถอนและไม่มีการส่ง callback
ตัวอย่างคำขอ
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"}'ตัวอย่างการตอบกลับ
{
"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"
}
}เฉพาะตัวอย่าง. Endpoint นี้เป็นแบบอ่านอย่างเดียว — ไม่มีการหักยอดคงเหลือและไม่มีการสร้างเรกคอร์ดการถอน เรียกใช้ได้ตามต้องการเพื่อแสดงรายละเอียดค่าธรรมเนียมใน UI
สถานะการถอน
ดูสถานะของคำขอถอน
/v1/payout/status/{uuid}พารามิเตอร์ใน Path
| Field | Type | Required | Description |
|---|---|---|---|
uuid | string | yes | UUID ของการถอน (จาก result.uuid ตอนสร้าง) |
ตัวอย่างการตอบกลับ
{
"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"
}
}สำหรับคำขอ GET นี้ลายเซ็นคำนวณจาก body ที่ว่าง:
hash_hmac('sha256', base64_encode(''), $apiKey)
ฟิลด์ในการตอบกลับ
ฟิลด์ที่คืนใน result จาก POST /v1/payout และ GET /v1/payout/status/{uuid}:
| Field | Type | Description |
|---|---|---|
uuid | string | UUID ของการถอนที่ระบบกำหนด |
order_id | string | ตัวระบุการถอนภายในของคุณ (ไม่ซ้ำกันภายในโปรเจกต์) |
status | string | สถานะปัจจุบันของการถอน (ดูด้านล่าง) |
currency | string | สกุลเงินที่ถอน |
network | string | รหัสเครือข่าย |
amount | string | จำนวนเงินถอนที่ร้องขอ |
merchant_amount | string | จำนวนเงินที่หักจากยอดคงเหลือผู้ค้า |
network_amount | string | จำนวนเงินที่ส่งจริงบนเชน (หลังหักค่าธรรมเนียมเครือข่าย + แพลตฟอร์ม) |
amount_usd | string | มูลค่าเทียบเท่า USD ของจำนวนเงินถอน |
to_address | string | ที่อยู่บล็อกเชนของผู้รับ |
memo | string | null | Destination tag / memo (TON, SOL) null ในกรณีอื่น |
txid | string | null | hash ของธุรกรรมบล็อกเชน null จนกว่าจะส่งธุรกรรม |
block_number | int | null | หมายเลขบล็อกที่บรรจุธุรกรรม null จนกว่าจะถูกบรรจุ |
error_type | string | null | สาเหตุที่ล้มเหลวเมื่อ status = failed (ดู Error types ด้านล่าง) null ในกรณีอื่น |
created_at | string (ISO 8601) | เวลาที่สร้างการถอน |
updated_at | string (ISO 8601) | เวลาที่สถานะเปลี่ยนแปลงล่าสุด |
from_currency | string | null | ยอดคงเหลือต้นทางที่ถูกตัดเมื่อใช้การแปลงอัตโนมัติ (เช่น USDT สำหรับการถอนเป็น BTC) เป็น null หากไม่มีการแปลง |
debited_amount | string | null | จำนวนเงินที่ถูกตัดจริงจากยอดคงเหลือต้นทางหลังการแปลง ปรากฏเฉพาะเมื่อใช้การแปลงอัตโนมัติ |
debited_currency | string | null | สกุลเงินของ debited_amount — ยอดคงเหลือที่เงินถูกตัดออก |
สถานะของการถอน
ฟิลด์ status รับค่าต่อไปนี้:
| Status | Description |
|---|---|
pending | สร้างแล้ว รอการดำเนินการ |
completed | ดำเนินการสำเร็จ — txid ถูกตั้งค่า |
failed | เกิดข้อผิดพลาดในการส่ง — ดู error_type |
cancelled | ยกเลิกแล้ว |
ประเภทข้อผิดพลาด
เมื่อ status = failed ฟิลด์ error_type อธิบายสาเหตุ:
| Code | Description |
|---|---|
aml_risk | การถอนถูกบล็อกโดยการตรวจสอบความเสี่ยง AML (ที่อยู่ผู้รับถูกแฟล็กว่ามีความเสี่ยงสูง) |
การแจ้งเตือน Webhook
เมื่อ status ของการถอนเปลี่ยน ระบบจะส่ง Webhook แบบ POST ไปยัง URL url_callback ที่ระบุตอนสร้างการถอน หากไม่ได้ระบุ url_callback จะไม่มีการส่ง Webhook สำหรับการถอนนั้น
- Method:
POST - Content-Type:
application/json - Signature: ฟิลด์
signใน body ของคำขอ คำนวณด้วย Payout API key (คีย์เดียวกับที่ใช้ลงลายเซ็นคำขอถอน)
payload สะท้อนออบเจกต์ result จาก GET /v1/payout/status/{uuid} พร้อมเพิ่มฟิลด์ sign สำหรับการตรวจสอบ
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"
}การตรวจสอบลายเซ็น ใช้อัลกอริทึมเดียวกับ payment webhooks แต่ลงลายเซ็นด้วย Payout API key แทน API key ปกติ ตัดฟิลด์ sign ออก แปลง payload ที่เหลือเป็น JSON เข้ารหัสด้วย Base64 จากนั้นคำนวณ hash_hmac('sha256', $base64, $payoutApiKey) แล้วเปรียบเทียบกับ sign ที่ได้รับ