Skip to content

POST /apiv2/withdraw ​

Withdraw TRX from your Netts balance to a TRON address. The request returns an order number immediately; the actual on-chain payout is performed asynchronously by the backend (within ~5 minutes). Track the result by polling the status endpoint or by configuring a webhook.

ℹ️ How it works. Placing a withdrawal reserves the amount from your balance right away (balance is debited the moment the order is accepted). A backend daemon then sends the TRX and marks the order completed or failed. There is no synchronous on-chain result in the initial response — you always get a pending acknowledgement first.

Before the order is accepted, the destination address is checked. If withdrawals to that address are not available, or the check cannot be completed at the moment, the request is rejected: no order is created and nothing is reserved from your balance.

Endpoint URL ​

POST https://netts.io/apiv2/withdraw

Request Headers ​

HeaderRequiredDescription
Content-TypeYesapplication/json
X-API-KEYYesYour API key from the Netts dashboard
X-Idempotency-KeyNoOptional client-generated key (base64) to safely retry without a double withdrawal. If omitted, the server derives one automatically. This value becomes your orderId.

Request Body ​

json
{
    "amount": 15,
    "address": "TXXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}

Parameters ​

ParameterTypeRequiredDescription
amountnumberYesGross amount in TRX (minimum 3). The fee is deducted from this amount — the recipient gets amount − fee (net).
addressstringYesDestination TRON address (T…, 34 chars, base58). Checked before the order is accepted.
sub_and_robot_outbooleanNoRobot/sub payout mode: applies the 2 TRX fee instead of 1 TRX. Default false.

Fee. A flat fee is withheld from the gross amount: 1 TRX normally, or 2 TRX when sub_and_robot_out = true. The order is rejected if amount − fee ≤ 0.

Example Requests ​

The examples below also build and send the X-Idempotency-Key so an accidental repeat does not create a second withdrawal. See Idempotency for the full rules.

cURL ​

bash
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-Idempotency-Key: $IDEMP" \
  -d "{\"amount\": $AMOUNT, \"address\": \"$ADDR\"}"

Python ​

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-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)}")

Response ​

Accepted — withdrawal queued (202 Accepted) ​

The amount is reserved from your balance and the payout is scheduled. Poll the status endpoint (or wait for the webhook) until it becomes completed / failed.

json
{
    "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"
        }
    }
}

Response Fields ​

FieldTypeDescription
detail.codeinteger10000 accepted
detail.statusstringpending
detail.data.orderIdstringOrder number — a 43-char URL-safe string. Use it for the status endpoint and it identifies the order in webhook payloads.
detail.data.amountnumberGross amount requested (TRX)
detail.data.feenumberFee withheld (1 or 2 TRX)
detail.data.netnumberAmount the recipient receives (amount − fee)
detail.data.addressstringDestination address

Status Endpoint ​

GET https://netts.io/apiv2/withdraw/status/{orderId}

Headers: X-API-KEY (the order must belong to the authenticated user). orderId is URL-safe — pass it as-is, no URL-encoding needed.

Order stateHTTPcodestatus
Completed (TRX sent)20010000completed (with processed_at)
Queued / sending20010001pending
Failed2005003failed (with error_message)
Not found / not yours404-1—
json
{
    "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"
        }
    }
}

Sub-user accounts ​

Sub-user withdrawals work exactly the same as for regular users — just with the sub-user's own API key. A sub-user calls this same POST /apiv2/withdraw endpoint, authenticated with its own key; the withdrawal is debited from that sub-user's own balance and sent to the address the request specifies. Same minimum, same fee (1 TRX), same address check, same flow. There is no separate sub-user endpoint — each account, parent or sub-user, only ever withdraws its own balance with its own key.

Webhooks ​

Instead of polling, configure a webhook once and Netts will POST a signed notification when each of your withdrawals reaches a terminal state (completed / failed). The webhook is stored per user and applies to that account's withdrawals. If no webhook is configured, just poll the status endpoint.

Configure / view / remove ​

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

Headers: X-API-KEY.

json
// POST body
{
    "callback_url": "https://your-server.example/netts/withdraw-hook",
    "secret": "your_shared_secret_min_8_chars",
    "enabled": true
}
ParameterTypeRequiredDescription
callback_urlstringYeshttp(s) URL (≤ 2048 chars) that receives the POST
secretstringYesShared secret (8…256 chars) used to sign each payload
enabledbooleanNoTurn delivery on/off without deleting the config. Default true

GET returns { callback_url, enabled, secret_set, updated_at } — the secret itself is never echoed back.

Delivery payload ​

Netts sends a POST to your callback_url with header X-Netts-Signature: base64( HMAC-SHA256( secret, raw_body ) ) and this JSON body:

json
{
    "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 is completed or failed (on failed, error_message is populated).

Verifying the signature ​

The signature is computed over the canonical JSON of the body: keys sorted, no spaces (separators=(",", ":")). Recompute it the same way and compare.

python
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): ...

Always verify against the raw received bytes. If you re-serialize the parsed JSON, reproduce the canonical form: json.dumps(payload, ensure_ascii=False, separators=(",",":"), sort_keys=True).

Delivery guarantees ​

  • Respond with HTTP 2xx to acknowledge. Any other response (or a timeout) is treated as a failed attempt.
  • Up to 3 attempts per order, within a 21-minute window from the moment the order was created (retry backoff ≈ 5 minutes). After that, delivery is given up — fall back to the status endpoint.
  • Deliveries are de-duplicated: each order is delivered at most once successfully.
  • Make your handler idempotent on orderId.

Error Responses ​

Authentication Error (401) ​

json
{ "detail": { "code": -1, "msg": "Invalid API key or IP not in whitelist" } }

Insufficient Balance (403) ​

json
{ "detail": { "code": 1004, "status": "failed", "msg": "Insufficient balance: 2.0 < 15 TRX" } }

Address Not Available (403) ​

Withdrawals to this destination address are not accepted. No order is created and nothing is reserved. Use a different address or contact support.

json
{ "detail": { "code": 1006, "status": "failed", "msg": "Withdrawals to this address are not available. Contact support." } }

Address Check Unavailable (503) ​

The destination address could not be checked right now. No order is created and nothing is reserved. Retry shortly — the same X-Idempotency-Key may be reused.

json
{ "detail": { "code": 1009, "status": "failed", "msg": "Address check is temporarily unavailable. No request was created. Retry shortly." } }

Pending Withdrawal Exists (409) ​

You may have only one pending withdrawal at a time on your own balance. Wait until the current one is processed.

json
{ "detail": { "code": 4090, "status": "failed", "msg": "You have a pending withdrawal. Wait until it is processed." } }

Validation Error (400) ​

json
{ "detail": { "code": 5004, "status": "failed", "msg": "Minimum withdrawal is 3 TRX" } }

Error Code Reference ​

CodeDescriptionHTTP Status
10000Accepted (withdrawal queued) / Completed (status endpoint)202 / 200
10001Pending — queued or sending (status endpoint)200
208Duplicate of an already-accepted request — cached response208
-Same request still being processed (do not retry yet)409
4090You already have a pending withdrawal409
-1Invalid API key / IP not in whitelist, or order not found401 / 404
1004Insufficient balance403
1006Withdrawals to this address are not available403
1009Address check temporarily unavailable — no order created, retry shortly503
5004Validation error (amount < 3, fee ≥ amount, bad address, bad idempotency key)400
5003Withdrawal failed / service unavailable200 (status) / 503
5000Internal server error500

Rate Limits ​

Limited per API key (header X-API-KEY):

PeriodLimit
1 second5 requests
1 minute150 requests

Rate Limit Exceeded (429) ​

json
{ "message": "API rate limit exceeded" }

Idempotency ​

Send the optional X-Idempotency-Key header so an accidental repeat does not create a second withdrawal — the original response is returned with HTTP 208. If you do not send the header, the server derives a key automatically from your request parameters within a short time window. The key is also your orderId.

How to form the key ​

The key is base64url( HMAC-SHA256( secret, message ) ) with = padding removed — a 43-char URL-safe string, where:

  • secret = your API key (X-API-KEY);
  • message = the fields joined with : — address:amount:nonce.

nonce is any value that is stable across retries of the same logical order but different between distinct orders — e.g. a UUID you keep for that order, or a coarse timestamp bucket. Generate the key once per order and resend the exact same value on every retry.

python
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

Validation. A supplied X-Idempotency-Key must be 16–64 characters from the charset A–Z a–z 0–9 + / = _ -. A malformed or over-long key is rejected with HTTP 400 (code 5004).

Status CodeMeaning
202Accepted (first request)
208Already accepted — cached response returned (no second withdrawal)
409The same request is currently being processed — wait, do not retry yet

Retrying after a failure. Only accepted results are cached. If the previous attempt failed (e.g. insufficient balance, validation), you may safely retry with the same key — the request is attempted again rather than returning the old error. While an attempt is still in progress you get 409; wait and retry.

Notes ​

  • Asynchronous payout. The response is always a pending acknowledgement; the TRX is sent by a backend daemon, typically within ~5 minutes. Use the status endpoint or a webhook for the result.
  • Balance is reserved immediately when the order is accepted (not when the TRX is finally sent).
  • Minimum: 3 TRX. Fee: 1 TRX (or 2 TRX with sub_and_robot_out), withheld from the gross amount; the recipient receives net = amount − fee.
  • One pending at a time on your own balance (code 4090).
  • Destination address check. Every request is checked before the order is accepted: code 1006 (HTTP 403) — withdrawals to this address are not available; code 1009 (HTTP 503) — the check is temporarily unavailable, retry shortly. In both cases no order is created.
  • Sub-users withdraw exactly like regular users — same POST /apiv2/withdraw endpoint, same rules, but authenticated with the sub-user's own API key. A sub-user withdraws its own balance to the address it specifies. There is no separate sub-user endpoint.
  • orderId is a 43-char URL-safe string; pass it as-is in the status URL (no encoding required).
  • Webhooks: per-user, signed with X-Netts-Signature; up to 3 attempts within a 21-minute window. Configure via POST /apiv2/withdraw/webhook.