POST /apiv2/withdraw
Rút TRX từ số dư Netts của bạn về bất kỳ địa chỉ TRON nào. Yêu cầu sẽ trả về mã đơn hàng ngay lập tức; việc thanh toán on-chain thực tế được thực hiện bất đồng bộ bởi backend (trong vòng ~5 phút). Theo dõi kết quả bằng cách thăm dò endpoint trạng thái hoặc bằng cách cấu hình một webhook.
ℹ️ Cách thức hoạt động. Việc tạo một yêu cầu rút tiền sẽ giữ trước số tiền từ số dư của bạn ngay lập tức (số dư bị trừ ngay thời điểm đơn hàng được chấp nhận). Sau đó, một tiến trình nền (daemon) của backend sẽ gửi TRX và đánh dấu đơn hàng là
completedhoặcfailed. Không có kết quả on-chain đồng bộ trong phản hồi ban đầu — bạn luôn nhận được xác nhậnpendingtrước.
URL Endpoint
POST https://netts.io/apiv2/withdrawTiêu đề Yêu cầu
| Tiêu đề | Bắt buộc | Mô tả |
|---|---|---|
| Content-Type | Có | application/json |
| X-API-KEY | Có | Khóa API của bạn từ bảng điều khiển Netts |
| X-Real-IP | Có | Địa chỉ IP từ danh sách trắng của bạn |
| X-Idempotency-Key | Không | Khóa tùy chọn do client tạo (base64) để thử lại một cách an toàn mà không bị rút tiền hai lần. Nếu bỏ qua, máy chủ sẽ tự động tạo một khóa. Giá trị này trở thành orderId của bạn. |
Thân Yêu cầu
{
"amount": 15,
"address": "TXXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}Tham số
| Tham số | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
| amount | number | Có | Số tiền tổng (gross) bằng TRX (tối thiểu 3). Phí sẽ được trừ vào số tiền này — người nhận sẽ nhận được amount − fee (net). |
| address | string | Có | Địa chỉ TRON đích (T…, 34 ký tự, base58). |
| sub_and_robot_out | boolean | Không | Chế độ thanh toán robot/sub: áp dụng mức phí 2 TRX thay vì 1 TRX. Mặc định là false. |
Phí. Một khoản phí cố định được khấu trừ từ
amounttổng: 1 TRX thông thường, hoặc 2 TRX khisub_and_robot_out = true. Đơn hàng sẽ bị từ chối nếuamount − fee ≤ 0.
Yêu cầu Mẫu
Các ví dụ dưới đây cũng xây dựng và gửi
X-Idempotency-Keyđể việc gửi lặp lại ngoài ý muốn không tạo ra lần rút tiền thứ hai. Xem Tính lũy đẳng để biết đầy đủ các quy tắc.
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)}")Phản hồi
Đã chấp nhận — lệnh rút tiền đã vào hàng đợi (202 Accepted)
Số tiền được giữ lại từ số dư của bạn và kế hoạch thanh toán đã được lên lịch. Hãy thăm dò endpoint trạng thái (hoặc chờ webhook) cho đến khi nó chuyển thành 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"
}
}
}Các Trường Phản hồi
| Trường | Kiểu | Mô tả |
|---|---|---|
| detail.code | integer | 10000 đã được chấp nhận |
| detail.status | string | pending |
| detail.data.orderId | string | Mã đơn hàng — chuỗi 43 ký tự an toàn với URL. Sử dụng mã này cho endpoint trạng thái và nó dùng để xác định đơn hàng trong payload của webhook. |
| detail.data.amount | number | Số tiền tổng được yêu cầu (TRX) |
| detail.data.fee | number | Phí được giữ lại (1 hoặc 2 TRX) |
| detail.data.net | number | Số tiền người nhận thực nhận (amount − fee) |
| detail.data.address | string | Địa chỉ đích |
Endpoint Trạng thái
GET https://netts.io/apiv2/withdraw/status/{orderId}Tiêu đề: X-API-KEY + X-Real-IP (đơn hàng phải thuộc về người dùng đã được xác thực). orderId là chuỗi an toàn với URL — truyền nguyên trạng, không cần mã hóa URL.
| Trạng thái đơn hàng | HTTP | code | status |
|---|---|---|---|
| Đã hoàn thành (đã gửi TRX) | 200 | 10000 | completed (kèm processed_at) |
| Trong hàng đợi / đang gửi | 200 | 10001 | pending |
| Thất bại | 200 | 5003 | failed (kèm error_message) |
| Không tìm thấy / không phải của bạn | 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"
}
}
}Tài khoản người dùng phụ (sub-user)
Việc rút tiền của người dùng phụ hoạt động hoàn toàn giống như người dùng thông thường — chỉ khác là sử dụng khóa API của chính người dùng phụ đó. Người dùng phụ gọi cùng endpoint POST /apiv2/withdraw này, được xác thực bằng khóa của chính họ; khoản tiền rút sẽ được trừ vào số dư của chính người dùng phụ đó và gửi đến bất kỳ address nào được chỉ định trong yêu cầu. Cùng mức tối thiểu, cùng mức phí (1 TRX), cùng quy trình. Hoàn toàn không có endpoint riêng cho người dùng phụ — mỗi tài khoản, dù là tài khoản cha hay người dùng phụ, chỉ rút số dư của chính mình bằng khóa riêng của mình.
Webhook
Thay vì thăm dò, hãy cấu hình webhook một lần và Netts sẽ POST một thông báo có chữ ký khi mỗi lệnh rút tiền của bạn đạt đến trạng thái cuối cùng (completed / failed). Webhook được lưu theo từng người dùng và áp dụng cho các lượt rút tiền của tài khoản đó. Nếu không có webhook nào được cấu hình, bạn chỉ cần thăm dò endpoint trạng thái.
Cấu hình / xem / xóa
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 # unsubscribeTiêu đề: 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
}| Tham số | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
| callback_url | string | Có | URL http(s) (≤ 2048 ký tự) nhận yêu cầu POST |
| secret | string | Có | Khóa bí mật dùng chung (8…256 ký tự) dùng để ký từng payload |
| enabled | boolean | Không | Bật/tắt việc phân phối mà không cần xóa cấu hình. Mặc định là true |
GET trả về { callback_url, enabled, secret_set, updated_at } — chính khóa bí mật không bao giờ được trả lại.
Payload phân phối
Netts gửi một yêu cầu POST đến callback_url của bạn với tiêu đề X-Netts-Signature: base64( HMAC-SHA256( secret, raw_body ) ) cùng với thân JSON sau:
{
"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
}statuscó giá trịcompletedhoặcfailed(khifailed,error_messagesẽ có nội dung).
Xác minh chữ ký
Chữ ký được tính toán dựa trên chuỗi JSON chuẩn tắc (canonical JSON) của phần thân: các khóa đã được sắp xếp, không có khoảng trắng (separators=(",", ":")). Hãy tính toán lại theo cùng cách đó và so sánh.
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): ...Luôn xác minh dựa trên các byte thô đã nhận được (raw received bytes). Nếu bạn tuần tự hóa lại JSON đã phân tích cú pháp, hãy tái tạo dạng chuẩn tắc:
json.dumps(payload, ensure_ascii=False, separators=(",",":"), sort_keys=True).
Đảm bảo phân phối
- Phản hồi bằng HTTP 2xx để xác nhận. Mọi phản hồi khác (hoặc hết thời gian chờ) đều được coi là một lần thử thất bại.
- Tối đa 3 lần thử cho mỗi đơn hàng, trong khoảng thời gian 21 phút kể từ thời điểm đơn hàng được tạo (thời gian chờ thử lại ≈ 5 phút). Sau thời gian đó, việc phân phối sẽ bị hủy bỏ — hãy chuyển sang sử dụng endpoint trạng thái.
- Các lần phân phối được chống trùng lặp: mỗi đơn hàng được phân phối thành công tối đa một lần.
- Đảm bảo trình xử lý của bạn có tính lũy đẳng đối với
orderId.
Phản hồi Lỗi
Lỗi Xác thực (401)
{ "detail": { "code": -1, "msg": "Invalid API key or IP not in whitelist" } }Không đủ Số dư (403)
{ "detail": { "code": 1004, "status": "failed", "msg": "Insufficient balance: 2.0 < 15 TRX" } }Đang có Lệnh Rút tiền Chờ xử lý (409)
Bạn chỉ có thể có duy nhất một lệnh rút tiền chờ xử lý tại một thời điểm trên số dư của chính mình. Hãy đợi cho đến khi lệnh hiện tại được xử lý xong.
{ "detail": { "code": 4090, "status": "failed", "msg": "You have a pending withdrawal. Wait until it is processed." } }Lỗi Xác thực Dữ liệu (400)
{ "detail": { "code": 5004, "status": "failed", "msg": "Minimum withdrawal is 3 TRX" } }Bảng Tra cứu Mã Lỗi
| Mã | Mô tả | Trạng thái HTTP |
|---|---|---|
10000 | Đã chấp nhận (lệnh rút đã vào hàng đợi) / Đã hoàn thành (endpoint trạng thái) | 202 / 200 |
10001 | Đang chờ xử lý — đã vào hàng đợi hoặc đang gửi (endpoint trạng thái) | 200 |
208 | Trùng lặp với một yêu cầu đã được chấp nhận — trả về phản hồi đã lưu trong bộ nhớ đệm | 208 |
- | Yêu cầu tương tự vẫn đang được xử lý (chưa nên thử lại) | 409 |
4090 | Bạn đã có một lệnh rút tiền đang chờ xử lý | 409 |
-1 | Khóa API không hợp lệ / IP không nằm trong danh sách trắng, hoặc không tìm thấy đơn hàng | 401 / 404 |
1004 | Số dư không đủ | 403 |
5004 | Lỗi xác thực dữ liệu (số tiền < 3, phí ≥ số tiền, sai địa chỉ, sai khóa lũy đẳng) | 400 |
5003 | Rút tiền thất bại / dịch vụ không khả dụng | 200 (trạng thái) / 503 |
5000 | Lỗi máy chủ nội bộ | 500 |
Giới hạn Tốc độ
Được giới hạn theo từng khóa API (tiêu đề X-API-KEY):
| Khoảng thời gian | Giới hạn |
|---|---|
| 1 giây | 5 yêu cầu |
| 1 phút | 150 yêu cầu |
Vượt quá Giới hạn Tốc độ (429)
{ "message": "API rate limit exceeded" }Tính lũy đẳng
Hãy gửi tiêu đề tùy chọn X-Idempotency-Key để việc gửi lặp lại ngoài ý muốn không tạo ra lần rút tiền thứ hai — phản hồi ban đầu sẽ được trả về với HTTP 208. Nếu bạn không gửi tiêu đề này, máy chủ sẽ tự động tạo một khóa từ các tham số yêu cầu của bạn trong một khoảng thời gian ngắn. Khóa này cũng chính là orderId của bạn.
Cách tạo khóa
Khóa là base64url( HMAC-SHA256( secret, message ) ) đã loại bỏ phần đệm = — một chuỗi 43 ký tự an toàn với URL, trong đó:
- secret = khóa API của bạn (
X-API-KEY); - message = các trường được nối với nhau bằng dấu
:—address:amount:nonce.
nonce là bất kỳ giá trị nào giữ nguyên giữa các lần thử lại của cùng một đơn hàng logic nhưng khác nhau giữa các đơn hàng riêng biệt — ví dụ: một UUID bạn lưu cho đơn hàng đó, hoặc một nhóm dấu thời gian khái lược. Hãy tạo khóa một lần cho mỗi đơn hàng và gửi lại chính xác cùng một giá trị đó trong mỗi lần thử lại.
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-safeXác thực. Một giá trị
X-Idempotency-Keyđược cung cấp phải có từ 16–64 ký tự thuộc tập ký tựA–Z a–z 0–9 + / = _ -. Một khóa sai định dạng hoặc quá dài sẽ bị từ chối với HTTP 400 (code 5004).
| Mã Trạng thái | Ý nghĩa |
|---|---|
| 202 | Đã chấp nhận (yêu cầu đầu tiên) |
| 208 | Đã được chấp nhận trước đó — trả về phản hồi từ bộ nhớ đệm (không tạo lệnh rút tiền thứ hai) |
| 409 | Yêu cầu tương tự hiện đang được xử lý — hãy đợi, chưa nên thử lại vội |
Thử lại sau khi thất bại. Chỉ các kết quả được chấp nhận mới được lưu trong bộ nhớ đệm. Nếu lần thử trước thất bại (ví dụ: không đủ số dư, lỗi xác thực), bạn có thể yên tâm thử lại với cùng một khóa — yêu cầu sẽ được thực hiện lại thay vì trả về lỗi cũ. Trong khi một lần thử vẫn đang được xử lý, bạn sẽ nhận được
409; hãy đợi và thử lại.
Ghi chú
- Thanh toán bất đồng bộ. Phản hồi luôn là một xác nhận
pending; TRX được gửi bởi một tiến trình nền của backend, thường trong vòng ~5 phút. Sử dụng endpoint trạng thái hoặc webhook để nhận kết quả. - Số dư được giữ ngay lập tức khi đơn hàng được chấp nhận (không phải khi TRX được gửi đi thực tế).
- Tối thiểu: 3 TRX. Phí: 1 TRX (hoặc 2 TRX với
sub_and_robot_out), được khấu trừ từamounttổng; người nhận sẽ nhận đượcnet = amount − fee. - Chỉ một lệnh chờ xử lý tại một thời điểm trên số dư của chính bạn (
code 4090). - Người dùng phụ rút tiền hoàn toàn giống người dùng thông thường — cùng endpoint
POST /apiv2/withdraw, cùng các quy tắc, nhưng được xác thực bằng khóa API của chính người dùng phụ đó. Người dùng phụ rút số dư của chính mình về bất kỳaddressnào được chỉ định. Không có endpoint riêng cho người dùng phụ. - orderId là chuỗi 43 ký tự an toàn với URL; truyền nguyên trạng trong URL trạng thái (không yêu cầu mã hóa).
- Webhook: theo từng người dùng, được ký bằng
X-Netts-Signature; tối đa 3 lần thử trong khoảng thời gian 21 phút. Cấu hình thông quaPOST /apiv2/withdraw/webhook.