POST /apiv2/withdraw
ถอน TRX จากยอดคงเหลือใน Netts ของคุณไปยังที่อยู่ TRON ใดก็ได้ คำขอจะส่งคืนหมายเลขคำสั่งซื้อทันที โดยการจ่ายเงินจริงบนบล็อกเชน (on-chain) จะดำเนินการแบบอะซิงโครนัส (asynchronously) โดยระบบเบื้องหลัง (ภายในประมาณ 5 นาที) ติดตามผลลัพธ์ได้โดยการโพลล์ไปยัง status endpoint หรือโดยการกำหนดค่า webhook
ℹ️ วิธีการทำงาน การส่งคำขอถอนเงินจะกันยอดเงินจากยอดคงเหลือของคุณในทันที (ยอดคงเหลือจะถูกหักทันทีที่คำสั่งซื้อได้รับการยอมรับ) จากนั้น daemon ระบบเบื้องหลังจะส่ง TRX และ ทำเครื่องหมายคำสั่งซื้อว่า
completedหรือfailedจะไม่มีผลลัพธ์บนบล็อกเชนแบบซิงโครนัสในการตอบกลับ เริ่มแรก — คุณจะได้รับการตอบรับสถานะpendingก่อนเสมอ
URL ของ Endpoint
POST https://netts.io/apiv2/withdrawHeaders ของคำขอ
| ส่วนหัว | จำเป็น | คำอธิบาย |
|---|---|---|
| Content-Type | ใช่ | application/json |
| X-API-KEY | ใช่ | คีย์ API ของคุณจากแดชบอร์ด Netts |
| X-Real-IP | ใช่ | ที่อยู่ IP จากรายการที่อนุญาต (whitelist) ของคุณ |
| X-Idempotency-Key | ไม่ | คีย์ทางเลือกที่สร้างโดยไคลเอนต์ (base64) เพื่อลองส่งคำขอซ้ำได้อย่างปลอดภัยโดยไม่เกิดการถอนซ้ำ หากเว้นไว้ เซิร์ฟเวอร์จะสร้างคีย์ขึ้นมาโดยอัตโนมัติ ค่านี้จะกลายเป็น orderId ของคุณ |
เนื้อหาคำขอ
{
"amount": 15,
"address": "TXXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}พารามิเตอร์
| พารามิเตอร์ | ประเภท | จำเป็น | คำอธิบาย |
|---|---|---|---|
| amount | ตัวเลข | ใช่ | จำนวนเงินรวม (Gross) ในหน่วย TRX (ขั้นต่ำ 3) ค่าธรรมเนียมจะถูกหักออกจากจำนวนนี้ — ผู้รับจะได้รับ amount − fee (net) |
| address | สตริง | ใช่ | ที่อยู่ TRON ปลายทาง (T…, 34 ตัวอักษร, base58) |
| sub_and_robot_out | บูลีน | ไม่ | โหมดการจ่ายเงินแบบหุ่นยนต์/บัญชีย่อย: ปรับใช้ค่าธรรมเนียม 2 TRX แทน 1 TRX ค่าเริ่มต้นคือ false |
ค่าธรรมเนียม มีการหักค่าธรรมเนียมคงที่จากยอด
amountรวม: ปกติ 1 TRX หรือ 2 TRX เมื่อsub_and_robot_out = trueคำสั่งซื้อจะถูกปฏิเสธหากamount − fee ≤ 0
ตัวอย่างคำขอ
ตัวอย่างด้านล่างยังแสดงการสร้างและส่ง
X-Idempotency-Keyเพื่อป้องกันไม่ให้การส่งซ้ำโดยไม่ได้ตั้งใจ ก่อให้เกิดการถอนเงินครั้งที่สอง โปรดดู ความเป็นไอเดมโพเทนต์ (Idempotency) สำหรับกฎเกณฑ์ฉบับเต็ม
cURL
API_KEY="your_api_key"
ADDR="TXXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
AMOUNT=15
NONCE=$(( $(date +%s) / 2 )) # stable for retries within a 2s window; or your own order UUID
# X-Idempotency-Key = base64url( HMAC-SHA256( API_KEY, "addr:amount:nonce" ) )
IDEMP=$(printf '%s' "${ADDR}:${AMOUNT}:${NONCE}" \
| openssl dgst -sha256 -hmac "$API_KEY" -binary | basenc --base64url | tr -d '=')
curl -X POST https://netts.io/apiv2/withdraw \
-H "Content-Type: application/json" \
-H "X-API-KEY: $API_KEY" \
-H "X-Real-IP: your_whitelisted_ip" \
-H "X-Idempotency-Key: $IDEMP" \
-d "{\"amount\": $AMOUNT, \"address\": \"$ADDR\"}"Python
import time, hmac, hashlib, base64, requests
API_KEY = "your_api_key"
url = "https://netts.io/apiv2/withdraw"
payload = {"amount": 15, "address": "TXXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}
# X-Idempotency-Key = base64url( HMAC-SHA256( API_KEY, "addr:amount:nonce" ) ), padding stripped.
# Generate ONCE per order and resend the same value on every retry.
nonce = str(int(time.time() // 2)) # 2s bucket; or your own order UUID
message = f"{payload['address']}:{payload['amount']}:{nonce}"
idem_key = base64.urlsafe_b64encode(
hmac.new(API_KEY.encode(), message.encode(), hashlib.sha256).digest()
).decode().rstrip("=")
headers = {
"Content-Type": "application/json",
"X-API-KEY": API_KEY,
"X-Real-IP": "your_whitelisted_ip",
"X-Idempotency-Key": idem_key,
}
resp = requests.post(url, headers=headers, json=payload)
detail = resp.json().get("detail", {})
if resp.status_code == 202 and detail.get("status") == "pending":
d = detail["data"]
print(f"Order ID: {d['orderId']}") # use it for the status endpoint / webhook
print(f"Net to recipient: {d['net']} TRX (fee {d['fee']})")
else:
print(f"Code {detail.get('code')}: {detail.get('msg', detail)}")การตอบกลับ
ยอมรับแล้ว — การถอนเงินเข้าคิวแล้ว (202 Accepted)
ยอดเงินจะถูกกันไว้จากยอดคงเหลือของคุณและกำหนดเวลาการจ่ายเงินแล้ว ทำการโพลล์ status endpoint (หรือรอรับ webhook) จนกว่าสถานะจะเปลี่ยนเป็น completed / failed
{
"detail": {
"code": 10000,
"status": "pending",
"msg": "Withdrawal request accepted, processing within 5 minutes.",
"data": {
"orderId": "EXAMPLEorderId0000000000000000000000000000Aa",
"amount": 15.0,
"fee": 1.0,
"net": 14.0,
"address": "TXXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
}
}ฟิลด์การตอบกลับ
| ฟิลด์ | ประเภท | คำอธิบาย |
|---|---|---|
| detail.code | จำนวนเต็ม | 10000 ยอมรับแล้ว |
| detail.status | สตริง | pending |
| detail.data.orderId | สตริง | หมายเลขคำสั่งซื้อ — สตริงรูปแบบ URL-safe ความยาว 43 ตัวอักษร ใช้สำหรับ status endpoint และใช้ระบุคำสั่งซื้อในเพย์โหลดของ webhook |
| detail.data.amount | ตัวเลข | จำนวนเงินรวมที่ส่งคำขอ (TRX) |
| detail.data.fee | ตัวเลข | ค่าธรรมเนียมที่หักไว้ (1 หรือ 2 TRX) |
| detail.data.net | ตัวเลข | จำนวนเงินที่ผู้รับจะได้รับ (amount − fee) |
| detail.data.address | สตริง | ที่อยู่ปลายทาง |
Endpoint สถานะ
GET https://netts.io/apiv2/withdraw/status/{orderId}ส่วนหัว: X-API-KEY + X-Real-IP (คำสั่งซื้อต้องเป็นของผู้ใช้ที่ได้รับการยืนยันตัวตนแล้ว) orderId เป็นรูปแบบ URL-safe — ส่งค่าได้โดยตรง ไม่จำเป็นต้องทำ URL-encoding
| สถานะคำสั่งซื้อ | HTTP | code | status |
|---|---|---|---|
| สำเร็จแล้ว (ส่ง TRX แล้ว) | 200 | 10000 | completed (พร้อม processed_at) |
| อยู่ในคิว / กำลังส่ง | 200 | 10001 | pending |
| ล้มเหลว | 200 | 5003 | failed (พร้อม error_message) |
| ไม่พบ / ไม่ใช่ของคุณ | 404 | -1 | — |
{
"detail": {
"code": 10000,
"status": "completed",
"data": {
"orderId": "EXAMPLEorderId0000000000000000000000000000Aa",
"amount": 15.0, "fee": 1.0, "net": 14.0,
"address": "TXXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"processed_at": "2026-01-01 00:00:00+00:00"
}
}
}บัญชีผู้ใช้ย่อย
การถอนเงินของผู้ใช้ย่อยทำงานเหมือนกับผู้ใช้ทั่วไปทุกประการ — เพียงแต่ใช้คีย์ API ของผู้ใช้ย่อยเอง ผู้ใช้ย่อยจะเรียกใช้ endpoint POST /apiv2/withdraw ตัวเดียวกันนี้ โดยยืนยันตัวตนด้วยคีย์ของตนเอง ยอดถอนจะถูกหักออกจากยอดคงเหลือของผู้ใช้ย่อยรายนั้นเอง และส่งไปยัง address ใดก็ตามที่ระบุไว้ในคำขอ มีขั้นต่ำเท่ากัน ค่าธรรมเนียมเท่ากัน (1 TRX) และขั้นตอนเหมือนกัน ไม่มี endpoint แยกต่างหากสำหรับผู้ใช้ย่อย — แต่ละบัญชี ไม่ว่าจะเป็นบัญชีหลักหรือบัญชีย่อย จะถอนได้เฉพาะยอดคงเหลือของตนเองด้วยคีย์ของตนเองเท่านั้น
Webhooks
แทนที่จะใช้วิธีการโพลล์ คุณสามารถกำหนดค่า webhook เพียงครั้งเดียว และ Netts จะส่งการแจ้งเตือนแบบลงนามผ่าน POST เมื่อการถอนเงินแต่ละรายการของคุณเข้าสู่สถานะสุดท้าย (completed / failed) การตั้งค่า webhook จะถูกบันทึกแยกตามผู้ใช้และมีผลกับการถอนเงินของบัญชีนั้นๆ หากไม่ได้กำหนดค่า webhook ไว้ ให้ใช้วิธีโพลล์ status endpoint แทน
กำหนดค่า / ดู / ลบ
POST https://netts.io/apiv2/withdraw/webhook # create or update
GET https://netts.io/apiv2/withdraw/webhook # view current config (secret is never returned)
DELETE https://netts.io/apiv2/withdraw/webhook # unsubscribeส่วนหัว: X-API-KEY + X-Real-IP
// POST body
{
"callback_url": "https://your-server.example/netts/withdraw-hook",
"secret": "your_shared_secret_min_8_chars",
"enabled": true
}| พารามิเตอร์ | ประเภท | จำเป็น | คำอธิบาย |
|---|---|---|---|
| callback_url | สตริง | ใช่ | URL แบบ http(s) (≤ 2048 ตัวอักษร) ที่จะรับคำขอ POST |
| secret | สตริง | ใช่ | ความลับร่วม (8…256 ตัวอักษร) ที่ใช้ในการลงนามแต่ละเพย์โหลด |
| enabled | บูลีน | ไม่ | เปิด/ปิดการนำส่งข้อมูลโดยไม่ต้องลบการกำหนดค่า ค่าเริ่มต้นคือ true |
GET จะส่งคืน { callback_url, enabled, secret_set, updated_at } — ตัว secret จะไม่มีการส่งกลับมาเด็ดขาด
เพย์โหลดการนำส่ง
Netts จะส่งคำขอ POST ไปยัง callback_url ของคุณพร้อมส่วนหัว X-Netts-Signature: base64( HMAC-SHA256( secret, raw_body ) ) และ JSON body ดังนี้:
{
"orderId": "EXAMPLEorderId0000000000000000000000000000Aa",
"status": "completed",
"amount": 15.0,
"fee": 1.0,
"net": 14.0,
"address": "TXXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"processed_at": "2026-01-01 00:00:00+00:00",
"error_message": null
}statusจะเป็นcompletedหรือfailed(หากเป็นfailedจะมีข้อมูลในerror_message)
การตรวจสอบยืนยันลายเซ็น
ลายเซ็นจะถูกคำนวณจาก canonical JSON ของเนื้อหา: คีย์ต้องเรียงลำดับ และไม่มีช่องว่าง (separators=(",", ":")) ให้คำนวณใหม่อีกครั้งด้วยวิธีเดียวกันแล้วนำมาเปรียบเทียบ
import hmac, hashlib, base64, json
def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = base64.b64encode(
hmac.new(secret.encode(), raw_body, hashlib.sha256).digest()
).decode()
return hmac.compare_digest(expected, signature_header)
# Flask example: verify against the EXACT bytes received, then parse.
# if verify(request.get_data(), request.headers["X-Netts-Signature"], SECRET): ...ตรวจสอบยืนยันเทียบกับไบต์ดิบที่ได้รับ (raw bytes) เสมอ หากคุณทำการ serialize ข้อมูล JSON ที่แยกวิเคราะห์แล้วใหม่ ให้จัดรูปแบบให้อยู่ในรูป canonical เสมอ:
json.dumps(payload, ensure_ascii=False, separators=(",",":"), sort_keys=True)
การรับประกันการนำส่ง
- ตอบกลับด้วยรหัส HTTP 2xx เพื่อยืนยันการรับ การตอบกลับอื่นๆ ทั้งหมด (หรือการหมดเวลา) จะถือว่าเป็นความพยายามที่ล้มเหลว
- พยายามส่งซ้ำสูงสุด 3 ครั้ง ต่อคำสั่งซื้อ ภายในช่วงเวลา 21 นาที นับจากเวลาที่สร้างคำสั่งซื้อ (ระยะเวลารอเพื่อลองใหม่ ≈ 5 นาที) หลังจากนั้นจะยกเลิกการนำส่ง — ให้กลับไปใช้วิธีดึงข้อมูลจาก status endpoint แทน
- การนำส่งมีการคัดกรองข้อมูลซ้ำ (de-duplicated): แต่ละคำสั่งซื้อจะถูกนำส่งสำเร็จอย่างมากที่สุดเพียงหนึ่งครั้ง
- ตรวจสอบให้แน่ใจว่าตัวจัดการของคุณทำงานแบบ idempotent โดยอิงตาม
orderId
การตอบกลับข้อผิดพลาด
ข้อผิดพลาดการยืนยันตัวตน (401)
{ "detail": { "code": -1, "msg": "Invalid API key or IP not in whitelist" } }ยอดคงเหลือไม่เพียงพอ (403)
{ "detail": { "code": 1004, "status": "failed", "msg": "Insufficient balance: 2.0 < 15 TRX" } }มีรายการถอนเงินที่รอดำเนินการอยู่ (409)
คุณสามารถมีรายการถอนเงินที่รอดำเนินการได้เพียงครั้งละหนึ่งรายการเท่านั้นบนยอดคงเหลือของคุณ โปรดรอจนกว่ารายการปัจจุบันจะได้รับการประมวลผลเสร็จสิ้น
{ "detail": { "code": 4090, "status": "failed", "msg": "You have a pending withdrawal. Wait until it is processed." } }ข้อผิดพลาดในการตรวจสอบความถูกต้อง (400)
{ "detail": { "code": 5004, "status": "failed", "msg": "Minimum withdrawal is 3 TRX" } }ข้อมูลอ้างอิงรหัสข้อผิดพลาด
| รหัส | คำอธิบาย | สถานะ HTTP |
|---|---|---|
10000 | ยอมรับแล้ว (การถอนเข้าคิวแล้ว) / สำเร็จแล้ว (status endpoint) | 202 / 200 |
10001 | รอดำเนินการ — อยู่ในคิวหรือกำลังส่ง (status endpoint) | 200 |
208 | คำขอซ้ำกับคำขอที่ยอมรับไปแล้ว — ส่งคืนการตอบกลับที่แคชไว้ | 208 |
- | คำขอเดิมยังอยู่ระหว่างการประมวลผล (อย่าเพิ่งลองส่งซ้ำ) | 409 |
4090 | คุณมีรายการถอนเงินที่รอดำเนินการอยู่แล้ว | 409 |
-1 | คีย์ API ไม่ถูกต้อง / IP ไม่อยู่ใน whitelist หรือไม่พบคำสั่งซื้อ | 401 / 404 |
1004 | ยอดคงเหลือไม่เพียงพอ | 403 |
5004 | ข้อผิดพลาดในการตรวจสอบความถูกต้อง (จำนวนเงิน < 3, ค่าธรรมเนียม ≥ จำนวนเงิน, ที่อยู่ไม่ถูกต้อง, idempotency key ไม่ถูกต้อง) | 400 |
5003 | การถอนเงินล้มเหลว / บริการไม่พร้อมใช้งาน | 200 (status) / 503 |
5000 | ข้อผิดพลาดภายในเซิร์ฟเวอร์ | 500 |
การจำกัดอัตราคำขอ
จำกัดต่อคีย์ API (ส่วนหัว X-API-KEY):
| ช่วงเวลา | ขีดจำกัด |
|---|---|
| 1 วินาที | 5 คำขอ |
| 1 นาที | 150 คำขอ |
เกินขีดจำกัดอัตราคำขอ (429)
{ "message": "API rate limit exceeded" }ความเป็นไอเดมโพเทนต์
ส่งส่วนหัวทางเลือก X-Idempotency-Key เพื่อให้แน่ใจว่าการส่งซ้ำโดยไม่ได้ตั้งใจจะไม่สร้างรายการถอนเงินซ้ำซ้อน — การตอบกลับเดิมจะถูกส่งคืนพร้อมกับ HTTP 208 หากคุณไม่ได้ส่งส่วนหัวนี้ เซิร์ฟเวอร์จะสร้างคีย์ขึ้นมาโดยอัตโนมัติจากพารามิเตอร์ของคำขอของคุณภายในช่วงเวลาสั้นๆ คีย์นี้ยังเป็น orderId ของคุณอีกด้วย
วิธีการสร้างคีย์
คีย์คือ base64url( HMAC-SHA256( secret, message ) ) โดยตัด padding = ออก — เป็นสตริง URL-safe ความยาว 43 ตัวอักษร โดยที่:
- secret = คีย์ API ของคุณ (
X-API-KEY) - message = ฟิลด์ต่างๆ ที่เชื่อมต่อกันด้วย
:—address:amount:nonce
nonce คือค่าใดๆ ที่มีความคงที่ตลอดการส่งซ้ำของคำสั่งซื้อเชิงตรรกะเดียวกัน แต่แตกต่างกันระหว่างคำสั่งซื้อคนละรายการ — เช่น UUID ที่คุณเก็บไว้สำหรับคำสั่งซื้อนั้น หรือช่วงเวลา timestamp แบบหยาบๆ สร้างคีย์ขึ้นมาหนึ่งครั้งต่อหนึ่งคำสั่งซื้อ และส่งค่าเดิมซ้ำทุกประการในทุกครั้งที่ลองใหม่
import hmac, hashlib, base64, time
def make_idempotency_key(api_key, address, amount, nonce=None):
if nonce is None:
nonce = str(int(time.time() // 2)) # 2-second bucket; or your own order UUID
message = f"{address}:{amount}:{nonce}"
digest = hmac.new(api_key.encode(), message.encode(), hashlib.sha256).digest()
return base64.urlsafe_b64encode(digest).decode().rstrip("=") # 43-char URL-safeการตรวจสอบความถูกต้อง
X-Idempotency-Keyที่ระบุจะต้องมีความยาว 16–64 ตัวอักษร จากชุดอักขระA–Z a–z 0–9 + / = _ -คีย์ที่มีรูปแบบไม่ถูกต้องหรือยาวเกินไปจะถูกปฏิเสธด้วย HTTP 400 (code 5004)
| รหัสสถานะ | ความหมาย |
|---|---|
| 202 | ยอมรับแล้ว (คำขอแรก) |
| 208 | ยอมรับไปแล้ว — ส่งคืนผลตอบกลับที่แคชไว้ (ไม่มีการถอนเงินครั้งที่สอง) |
| 409 | คำขอเดิมกำลังอยู่ระหว่างการประมวลผล — โปรดรอ อย่าเพิ่งลองใหม่ |
การลองใหม่หลังจากเกิดข้อผิดพลาด ระบบจะแคชเฉพาะผลลัพธ์ที่ยอมรับแล้วเท่านั้น หากความพยายามก่อนหน้า ล้มเหลว (เช่น ยอดคงเหลือไม่พอ, การตรวจสอบความถูกต้องไม่ผ่าน) คุณสามารถลองใหม่โดยใช้คีย์เดิมได้อย่างปลอดภัย — คำขอจะถูกดำเนินการใหม่อีกครั้งแทนที่จะส่งคืนข้อผิดพลาดเดิม ในระหว่างที่ความพยายามยังดำเนินอยู่ คุณจะได้รับ
409ให้รอแล้วจึงลองใหม่
หมายเหตุ
- การจ่ายเงินแบบอะซิงโครนัส การตอบกลับจะเป็นการรับทราบสถานะ
pendingเสมอ โดย daemon ของระบบเบื้องหลังจะส่ง TRX ให้ ซึ่งโดยทั่วไปจะใช้เวลาประมาณ 5 นาที ตรวจสอบผลลัพธ์ผ่าน status endpoint หรือ webhook - ยอดเงินจะถูกกันไว้ทันที เมื่อคำสั่งซื้อได้รับการยอมรับ (ไม่ใช่เมื่อส่ง TRX สำเร็จ)
- ขั้นต่ำ: 3 TRX ค่าธรรมเนียม: 1 TRX (หรือ 2 TRX กรณีเปิดใช้
sub_and_robot_out) ซึ่งจะถูกหักออกจากยอดamountรวม โดยผู้รับจะได้รับnet = amount − fee - รอดำเนินการได้ครั้งละหนึ่งรายการเท่านั้น บนยอดคงเหลือของคุณเอง (
code 4090) - ผู้ใช้ย่อยถอนเงินได้เหมือนกับผู้ใช้ทั่วไปทุกประการ — ใช้ endpoint
POST /apiv2/withdrawเดียวกัน กฎเดียวกัน แต่ยืนยันตัวตนด้วยคีย์ API ของผู้ใช้ย่อยเอง ผู้ใช้ย่อยจะถอนยอดคงเหลือของตนเองไปยังaddressใดๆ ที่ระบุ ไม่มี endpoint แยกต่างหากสำหรับผู้ใช้ย่อย - orderId เป็นสตริงรูปแบบ URL-safe ความยาว 43 ตัวอักษร ส่งค่าได้โดยตรงใน URL ตรวจสอบสถานะ (ไม่ต้องเข้ารหัส)
- Webhooks: แยกตามรายบัญชีผู้ใช้, ลงนามด้วย
X-Netts-Signature, ส่งซ้ำสูงสุด 3 ครั้งภายในหน้าต่างเวลา 21 นาที กำหนดค่าผ่านPOST /apiv2/withdraw/webhook