POST /apiv2/withdraw
Retira TRX de tu saldo de Netts a cualquier dirección TRON. La solicitud devuelve un número de orden inmediatamente; el pago real on-chain es realizado de forma asíncrona por el backend (en ~5 minutos). Monitorea el resultado mediante sondeo al endpoint de estado o configurando un webhook.
ℹ️ Cómo funciona. Realizar un retiro reserva el monto de tu saldo de inmediato (el saldo se debita en el momento en que se acepta la orden). Luego, un demonio del backend envía el TRX y marca la orden como
completedofailed. No hay resultado on-chain sincrónico en la respuesta inicial; siempre obtendrás primero una confirmaciónpending.
URL del Endpoint
POST https://netts.io/apiv2/withdrawEncabezados de la Solicitud
| Encabezado | Requerido | Descripción |
|---|---|---|
| Content-Type | Sí | application/json |
| X-API-KEY | Sí | Tu clave API del panel de Netts |
| X-Real-IP | Sí | Dirección IP de tu lista blanca |
| X-Idempotency-Key | No | Clave opcional generada por el cliente (base64) para reintentar de forma segura sin un doble retiro. Si se omite, el servidor genera una automáticamente. Este valor se convierte en tu orderId. |
Cuerpo de la Solicitud
{
"amount": 15,
"address": "TXXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}Parámetros
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
| amount | number | Sí | Monto bruto en TRX (mínimo 3). La tarifa se deduce de este monto; el destinatario recibe amount − fee (net). |
| address | string | Sí | Dirección TRON de destino (T…, 34 caracteres, base58). |
| sub_and_robot_out | boolean | No | Modo de pago de robot/subcuenta: aplica la tarifa de 2 TRX en lugar de 1 TRX. Valor predeterminado false. |
Tarifa. Se retiene una tarifa fija del
amountbruto: 1 TRX normalmente, o 2 TRX cuandosub_and_robot_out = true. La orden se rechaza siamount − fee ≤ 0.
Solicitudes de Ejemplo
Los ejemplos a continuación también construyen y envían el
X-Idempotency-Keypara que una repetición accidental no cree un segundo retiro. Consulta Idempotencia para ver las reglas completas.
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)}")Respuesta
Aceptado — retiro en cola (202 Accepted)
El monto se reserva de tu saldo y se programa el pago. Consulta periódicamente el endpoint de estado (o espera al webhook) hasta que pase a 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"
}
}
}Campos de Respuesta
| Campo | Tipo | Descripción |
|---|---|---|
| detail.code | integer | 10000 aceptado |
| detail.status | string | pending |
| detail.data.orderId | string | Número de orden: una cadena de 43 caracteres segura para URL. Úsala para el endpoint de estado e identifica la orden en las cargas útiles de webhooks. |
| detail.data.amount | number | Monto bruto solicitado (TRX) |
| detail.data.fee | number | Tarifa retenida (1 o 2 TRX) |
| detail.data.net | number | Monto que recibe el destinatario (amount − fee) |
| detail.data.address | string | Dirección de destino |
Endpoint de Estado
GET https://netts.io/apiv2/withdraw/status/{orderId}Encabezados: X-API-KEY + X-Real-IP (la orden debe pertenecer al usuario autenticado). orderId es seguro para URL: pásalo tal cual, no es necesaria la codificación URL.
| Estado de la orden | HTTP | code | status |
|---|---|---|---|
| Completada (TRX enviado) | 200 | 10000 | completed (con processed_at) |
| En cola / enviando | 200 | 10001 | pending |
| Fallida | 200 | 5003 | failed (con error_message) |
| No encontrada / no te pertenece | 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"
}
}
}Cuentas de subusuarios
Los retiros de subusuarios funcionan exactamente igual que para los usuarios regulares, solo que con la propia clave API del subusuario. Un subusuario llama a este mismo endpoint POST /apiv2/withdraw, autenticado con su propia clave; el retiro se debita del propio saldo de ese subusuario y se envía a cualquier address que especifique la solicitud. Mismo mínimo, misma tarifa (1 TRX), mismo flujo. No existe ningún endpoint separado para subusuarios: cada cuenta, principal o subusuario, solo retira su propio saldo con su propia clave.
Webhooks
En lugar de consultar periódicamente, configura un webhook una vez y Netts enviará por POST una notificación firmada cuando cada uno de tus retiros alcance un estado terminal (completed / failed). El webhook se almacena por usuario y se aplica a los retiros de esa cuenta. Si no hay ningún webhook configurado, simplemente consulta el endpoint de estado.
Configurar / ver / eliminar
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 # unsubscribeEncabezados: 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
}| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
| callback_url | string | Sí | URL http(s) (≤ 2048 caracteres) que recibe el POST |
| secret | string | Sí | Secreto compartido (8…256 caracteres) utilizado para firmar cada carga útil |
| enabled | boolean | No | Activa/desactiva la entrega sin eliminar la configuración. Valor predeterminado true |
GET devuelve { callback_url, enabled, secret_set, updated_at }; el secreto en sí nunca se devuelve.
Carga útil de entrega
Netts envía un POST a tu callback_url con el encabezado X-Netts-Signature: base64( HMAC-SHA256( secret, raw_body ) ) y este cuerpo 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
}statusescompletedofailed(enfailed,error_messagese encuentra completado).
Verificación de la firma
La firma se calcula sobre el JSON canónico del cuerpo: claves ordenadas, sin espacios (separators=(",", ":")). Recalcúlala de la misma manera y compárala.
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): ...Siempre verifica con los bytes sin procesar recibidos. Si vuelves a serializar el JSON analizado, reproduce la forma canónica:
json.dumps(payload, ensure_ascii=False, separators=(",",":"), sort_keys=True).
Garantías de entrega
- Responde con HTTP 2xx para confirmar la recepción. Cualquier otra respuesta (o un tiempo de espera agotado) se trata como un intento fallido.
- Hasta 3 intentos por orden, dentro de un período de 21 minutos desde el momento en que se creó la orden (tiempo de espera entre reintentos ≈ 5 minutos). Después de eso, se desiste de la entrega; recurre al endpoint de estado.
- Las entregas están desduplicadas: cada orden se entrega como máximo una vez con éxito.
- Haz que tu controlador sea idempotente según el
orderId.
Respuestas de Error
Error de Autenticación (401)
{ "detail": { "code": -1, "msg": "Invalid API key or IP not in whitelist" } }Saldo Insuficiente (403)
{ "detail": { "code": 1004, "status": "failed", "msg": "Insufficient balance: 2.0 < 15 TRX" } }Retiro Pendiente Existente (409)
Puedes tener solo un retiro pendiente a la vez sobre tu propio saldo. Espera hasta que el actual sea procesado.
{ "detail": { "code": 4090, "status": "failed", "msg": "You have a pending withdrawal. Wait until it is processed." } }Error de Validación (400)
{ "detail": { "code": 5004, "status": "failed", "msg": "Minimum withdrawal is 3 TRX" } }Referencia de Códigos de Error
| Código | Descripción | Estado HTTP |
|---|---|---|
10000 | Aceptado (retiro en cola) / Completado (endpoint de estado) | 202 / 200 |
10001 | Pendiente — en cola o enviando (endpoint de estado) | 200 |
208 | Duplicado de una solicitud ya aceptada — respuesta en caché | 208 |
- | La misma solicitud aún se está procesando (no reintentar todavía) | 409 |
4090 | Ya tienes un retiro pendiente | 409 |
-1 | Clave API inválida / IP no en lista blanca, u orden no encontrada | 401 / 404 |
1004 | Saldo insuficiente | 403 |
5004 | Error de validación (monto < 3, tarifa ≥ monto, dirección incorrecta, clave de idempotencia incorrecta) | 400 |
5003 | Retiro fallido / servicio no disponible | 200 (estado) / 503 |
5000 | Error interno del servidor | 500 |
Límites de Tasa
Limitado por clave API (encabezado X-API-KEY):
| Período | Límite |
|---|---|
| 1 segundo | 5 solicitudes |
| 1 minuto | 150 solicitudes |
Límite de Tasa Excedido (429)
{ "message": "API rate limit exceeded" }Idempotencia
Envía el encabezado opcional X-Idempotency-Key para que una repetición accidental no cree un segundo retiro; se devolverá la respuesta original con HTTP 208. Si no envías el encabezado, el servidor deduce una clave automáticamente a partir de los parámetros de tu solicitud dentro de una ventana de tiempo corta. La clave también es tu orderId.
Cómo formar la clave
La clave es base64url( HMAC-SHA256( secret, message ) ) sin el relleno =: una cadena de 43 caracteres segura para URL, donde:
- secret = tu clave API (
X-API-KEY); - message = los campos unidos con
:—address:amount:nonce.
nonce es cualquier valor que sea estable entre reintentos de la misma orden lógica pero diferente entre órdenes distintas; p. ej., un UUID que guardes para esa orden o un bloque temporal aproximado. Genera la clave una vez por orden y vuelve a enviar exactamente el mismo valor en cada reintento.
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-safeValidación. Un
X-Idempotency-Keyproporcionado debe tener entre 16 y 64 caracteres del conjuntoA–Z a–z 0–9 + / = _ -. Una clave malformada o demasiado larga se rechaza con HTTP 400 (code 5004).
| Código de Estado | Significado |
|---|---|
| 202 | Aceptado (primera solicitud) |
| 208 | Ya aceptado — se devuelve la respuesta en caché (sin segundo retiro) |
| 409 | La misma solicitud se está procesando actualmente — espera, no reintentes todavía |
Reintentar tras un fallo. Solo se almacenan en caché los resultados aceptados. Si el intento anterior falló (p. ej., saldo insuficiente, validación), puedes reintentar de forma segura con la misma clave; la solicitud se intentará de nuevo en lugar de devolver el error anterior. Mientras un intento siga en curso obtendrás
409; espera y vuelve a intentarlo.
Notas
- Pago asíncrono. La respuesta siempre es una confirmación
pending; el TRX es enviado por un demonio del backend, normalmente en ~5 minutos. Usa el endpoint de estado o un webhook para ver el resultado. - El saldo se reserva inmediatamente cuando se acepta la orden (no cuando finalmente se envía el TRX).
- Mínimo: 3 TRX. Tarifa: 1 TRX (o 2 TRX con
sub_and_robot_out), retenida delamountbruto; el destinatario recibenet = amount − fee. - Uno pendiente a la vez sobre tu propio saldo (
code 4090). - Los subusuarios retiran exactamente igual que los usuarios regulares: el mismo endpoint
POST /apiv2/withdraw, mismas reglas, pero autenticados con la propia clave API del subusuario. Un subusuario retira su propio saldo a cualquieraddressque especifique. No hay ningún endpoint separado para subusuarios. - orderId es una cadena de 43 caracteres segura para URL; pásala tal cual en la URL de estado (no requiere codificación).
- Webhooks: por usuario, firmados con
X-Netts-Signature; hasta 3 intentos dentro de una ventana de 21 minutos. Se configuran mediantePOST /apiv2/withdraw/webhook.