Webhooks — notificaciones de pedidos
Registre un endpoint HTTPS para recibir un webhook firmado en el momento exacto en que uno de sus pedidos sea completado y verificado en la cadena (on-chain). En lugar de hacer sondeos (polling), continúe con su flujo (p. ej., liberar USDT) tan pronto como llegue la notificación.
Se entregan tres eventos:
| Evento | Enviado cuando |
|---|---|
delegation.confirmed | Un alquiler de energy (1h / 5m) se confirma on-chain |
bandwidth.delegated | Se completa un pedido de bandwidth |
activation.confirmed | Se ejecuta una activación de dirección on-chain |
Esta página cubre la API de gestión (crear / listar / editar / rotar secreto / eliminar sus endpoints) y el formato de los webhooks que le enviamos.
ℹ️ Roles. Usted gestiona sus endpoints aquí. Netts realiza la entrega de forma asíncrona una vez que el pedido se verifica; no hay nada que sondear. Solo se envían eventos de éxito; los fallos y tiempos de espera agotados nunca se entregan.
🔒 Cada hash que enviamos se verifica primero on-chain. Un webhook se despacha únicamente después de que cada hash de transacción incluido en él se encuentre en un bloque. Si un hash aún no está en un bloque, la entrega se retiene y se vuelve a comprobar cada 30 segundos durante un máximo de 5 minutos; si nunca llega a confirmarse, no se envía nada para ese pedido. Nunca recibirá un hash que no exista on-chain.
URL base del endpoint
https://netts.io/apiv2/webhooksEncabezados de solicitud
| Encabezado | Requerido | Descripción |
|---|---|---|
| Content-Type | Sí (para POST/PATCH) | application/json |
| X-API-KEY | Sí | Su clave API del panel de Netts |
| X-Real-IP | Sí | Dirección IP de su lista blanca |
Su user_id se deriva de la clave API; nunca debe enviarlo. Puede ver y modificar únicamente sus propios endpoints.
Endpoint principal y de respaldo
Puede registrar como máximo dos endpoints, y cada uno tiene un role:
| Rol | Propósito |
|---|---|
primary | La dirección a la que se entrega cada webhook. |
backup | Alternativa de respaldo. Se utiliza únicamente cuando la entrega al primary falla tras agotar sus reintentos. |
Un único pedido confirmado genera un único webhook. No es una difusión en abanico (fan-out): el mismo evento nunca se envía a ambas direcciones a la vez. El endpoint backup existe por motivos de resiliencia: si su host principal es inaccesible o sigue respondiendo con estados distintos de 2xx, la entrega pasa al de respaldo en lugar de descartarse.
El primer endpoint que cree se convierte en primary; el segundo se convierte en backup. Puede especificar el role explícitamente, o intercambiarlos más tarde mediante PATCH.
¿Por qué no una URL independiente por cada tipo de operación? Porque el tipo de evento viaja dentro del cuerpo, en el campo
event. Un solo manejador, una sola verificación de firma, y los nuevos tipos de eventos comenzarán a llegar sin necesidad de registrar nada nuevo.
Gestionar endpoints
Crear — POST /apiv2/webhooks
Registra un nuevo endpoint y devuelve un secret que se muestra solo una vez (guárdelo: firma cada webhook que recibe).
// request body — role is optional
{
"url": "https://your-server.example/netts/delegation-hook",
"role": "primary"
}Si omite role, se asigna el primero disponible: primary, luego backup.
// response 201
{
"detail": {
"code": 10000,
"status": "created",
"data": {
"id": 1,
"url": "https://your-server.example/netts/delegation-hook",
"secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"role": "primary",
"is_active": true,
"created_at": "2026-01-01T00:00:00"
}
}
}Requisitos de la URL (validados al crear y en cada edición):
- debe ser
https; - debe resolverse en una dirección pública: las direcciones de bucle invertido (loopback), privadas (RFC1918), de enlace local (incl.
169.254.169.254) y otros rangos no enrutables son rechazados; - sin credenciales en la URL (
user:pass@…); - longitud de hasta 2048 caracteres.
Una URL rechazada devuelve 400.
Puede tener dos endpoints: uno primary y uno backup. Un tercero devuelve 409 (4090). Solicitar un role que ya esté ocupado devuelve 409 (4091): intercambie los roles con PATCH o elimine primero el existente.
curl -X POST https://netts.io/apiv2/webhooks \
-H "Content-Type: application/json" \
-H "X-API-KEY: your_api_key" \
-H "X-Real-IP: your_whitelisted_ip" \
-d '{"url": "https://your-server.example/netts/delegation-hook", "role": "primary"}'Listar — GET /apiv2/webhooks
Devuelve sus endpoints (el secret nunca se devuelve aquí).
{
"detail": {
"code": 10000,
"status": "ok",
"data": {
"endpoints": [
{
"id": 1,
"url": "https://your-server.example/netts/delegation-hook",
"role": "primary",
"is_active": true,
"created_at": "2026-01-01T00:00:00",
"updated_at": "2026-01-01T00:00:00"
},
{
"id": 2,
"url": "https://backup.example/netts/delegation-hook",
"role": "backup",
"is_active": true,
"created_at": "2026-01-01T00:00:00",
"updated_at": "2026-01-01T00:00:00"
}
],
"count": 2,
"max_endpoints": 2,
"roles": ["primary", "backup"]
}
}
}Obtener uno — GET /apiv2/webhooks/{id}
Misma estructura que un elemento de la lista (sin secret). Un id ajeno o inexistente devuelve 404.
Editar — PATCH /apiv2/webhooks/{id}
Cambia la url, is_active y/o role. Envíe cualquier subconjunto; un cuerpo vacío devuelve 422. Si se cambia la url, se vuelve a validar (https / SSRF). Un id ajeno o inexistente devuelve 404.
// request body (any subset)
{ "url": "https://your-server.example/netts/new-hook", "is_active": false }// response 200
{
"detail": {
"code": 10000,
"status": "updated",
"data": {
"id": 1,
"url": "https://your-server.example/netts/new-hook",
"role": "primary",
"is_active": false,
"created_at": "2026-01-01T00:00:00",
"updated_at": "2026-01-01T00:00:01"
}
}
}Promover el respaldo. Enviar {"role": "primary"} a su endpoint de respaldo intercambia los dos roles en una única transacción: el antiguo principal pasa a ser el de respaldo. Nunca se quedará sin una dirección principal, y no se requiere ninguna llamada adicional para el otro endpoint.
curl -X PATCH https://netts.io/apiv2/webhooks/2 \
-H "Content-Type: application/json" \
-H "X-API-KEY: your_api_key" \
-H "X-Real-IP: your_whitelisted_ip" \
-d '{"role": "primary"}'Establezca
is_active: falsepara pausar la entrega sin eliminar el endpoint;truepara reanudarla. Pausar suprimaryno promueve el de respaldo: la entrega seguirá intentándose hacia el principal. Intercambie los roles si desea que el de respaldo tome el control.
Rotar secreto — POST /apiv2/webhooks/{id}/rotate-secret
Genera un nuevo secret y lo devuelve una sola vez. El nuevo secreto entra en vigor de forma inmediata para las entregas posteriores, sin necesidad de acciones adicionales. Cada endpoint tiene su propio secreto: rotar el secreto del principal no modifica el del respaldo.
// response 200
{
"detail": {
"code": 10000,
"status": "rotated",
"data": {
"id": 1,
"secret": "whsec_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
}
}
}Eliminar — DELETE /apiv2/webhooks/{id}
Elimina de forma permanente el endpoint y libera su rol. Devuelve 204 (sin cuerpo); un id ajeno o inexistente devuelve 404.
curl -X DELETE https://netts.io/apiv2/webhooks/1 \
-H "X-API-KEY: your_api_key" -H "X-Real-IP: your_whitelisted_ip"Los webhooks que entregamos
Cuando se completa uno de sus pedidos, Netts envía una solicitud POST a su endpoint primary. Cada cuerpo es application/json (UTF-8); las direcciones y los hashes siempre se entregan como valores completos.
Campos comunes a todos los eventos:
| Campo | Tipo | Descripción |
|---|---|---|
event | string | Tipo de evento: clave de enrutamiento para su manejador |
delivery_id | int | ID de entrega: clave de desduplicación de su lado. También se envía en el encabezado X-Netts-Delivery. |
order_id | string | Su ID de pedido |
order_type | string | 1h, 5m, bandwidth o activation |
tx_hashes | string[] | Todos los hashes de transacción de la operación, cada uno verificado on-chain |
confirmed_at | string | ISO-8601 en formato UTC |
delegation.confirmed — alquiler de energy
{
"event": "delegation.confirmed",
"delivery_id": 1,
"order_id": "1Hxxxxxxxxxx",
"order_type": "1h",
"receive_address": "TXXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"energy_amount": 65000,
"tx_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"tx_hashes": ["0000000000000000000000000000000000000000000000000000000000000000"],
"delegation_timestamp": 1700000000000,
"confirmed_at": "2026-01-01T00:00:00Z"
}| Campo | Tipo | Descripción |
|---|---|---|
order_type | string | 1h o 5m |
receive_address | string | Dirección TRON que recibió la energy |
energy_amount | int | Cantidad de Energy delegada |
tx_hash | string | Campo heredado, conservado por compatibilidad: idéntico a tx_hashes[0] |
delegation_timestamp | int? | Opcional — presente solo cuando se confirma a través de la ruta Mongo |
Se recomienda utilizar
tx_hashesen nuevas integraciones: un pedido puede, en principio, completarse mediante más de una transacción.tx_hashseguirá funcionando.
bandwidth.delegated — pedido de bandwidth
{
"event": "bandwidth.delegated",
"delivery_id": 2,
"order_id": "B1Hxxxxxxxxxxxxx",
"order_type": "bandwidth",
"rental_label": "1h",
"rental_seconds": 3600,
"receive_address": "TXXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"bandwidth_amount": 400,
"fulfillment": "delegated",
"tx_hashes": ["0000000000000000000000000000000000000000000000000000000000000000"],
"confirmed_at": "2026-01-01T00:00:00Z"
}| Campo | Tipo | Descripción |
|---|---|---|
rental_label / rental_seconds | string / int | Duración del alquiler, p. ej., 1h / 3600 |
receive_address | string | Dirección TRON que recibió el bandwidth |
bandwidth_amount | int | Unidades de Bandwidth (netas) |
fulfillment | string | Cómo se completó el pedido — ver más abajo |
Valores de fulfillment:
| Valor | Significado | tx_hashes |
|---|---|---|
delegated | Bandwidth delegado desde nuestro pool | 1+ hashes |
trx_send | Completado mediante el envío de TRX a la dirección en lugar de delegar | 1+ hashes |
already_enough | La dirección ya tenía suficiente bandwidth libre; no se envió nada on-chain | vacío |
already_enough es el único caso en el que tx_hashes está vacío: el pedido se cierra con éxito, pero no hay transacción porque no fue necesaria ninguna.
activation.confirmed — activación de dirección
{
"event": "activation.confirmed",
"delivery_id": 3,
"order_id": "123456",
"order_type": "activation",
"address": "TXXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"activation_type": "ACC_CREATE",
"source": "telegram_bot",
"tx_hashes": ["0000000000000000000000000000000000000000000000000000000000000000"],
"confirmed_at": "2026-01-01T00:00:00Z"
}| Campo | Tipo | Descripción |
|---|---|---|
order_id | string | ID del pedido de activación (cadena numérica) |
address | string | Dirección TRON que fue activada |
activation_type | string | ACC_CREATE (AccountCreateContract) o DIRECT (transferencia TRX) |
source | string | Marcador de origen. Ya sea una etiqueta de servicio o el ID del pedido de energy que requirió la activación |
Solo se entregan activaciones reales. Si la dirección resultó estar ya activa y no se realizó ninguna transacción, no se envía ningún webhook.
Un pedido de energy que también haya requerido una activación genera dos webhooks: uno
activation.confirmedy unodelegation.confirmed. Son eventos independientes condelivery_iddistintos; enrútelos mediante el campoevent.
Encabezados que enviamos:
| Encabezado | Valor |
|---|---|
X-Netts-Event | Tipo de evento: delegation.confirmed, bandwidth.delegated o activation.confirmed |
X-Netts-Delivery | delivery_id (desduplicación) |
X-Netts-Timestamp | Segundos unix al momento del envío |
X-Netts-Signature | sha256=<hex>, hex = HMAC_SHA256(secret, "<timestamp>." + raw_body) |
User-Agent | netts-webhook/1.0 |
Verificar la firma
La firma sigue el esquema de Stripe (timestamp.body), calculado sobre los bytes en crudo que enviamos. Vuelva a calcularla con su secret, compárela en tiempo constante y rechácela si X-Netts-Timestamp se encuentra fuera de una ventana de ±5 minutos (protección contra ataques de repetición).
Firme con el secreto del endpoint que recibió la solicitud: el principal y el de respaldo tienen secretos independientes. Si ambas direcciones son atendidas por el mismo manejador, seleccione el secreto en función de la URL a la que llegó la solicitud.
import hmac, hashlib, time
def verify(raw_body: bytes, sig_header: str, ts_header: str, secret: str) -> bool:
# freshness (anti-replay)
if abs(time.time() - int(ts_header)) > 300:
return False
signed = f"{ts_header}.".encode() + raw_body
expected = "sha256=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig_header)
# Flask example:
# ok = verify(request.get_data(),
# request.headers["X-Netts-Signature"],
# request.headers["X-Netts-Timestamp"], SECRET)Semántica de entrega (importante: al menos una vez / at-least-once)
La entrega es al menos una vez: una respuesta perdida puede provocar un reintento, por lo que podría recibir el mismo evento dos veces. Dado que la acción comercial (liberar USDT) involucra dinero sensible:
- La desduplicación es obligatoria: procese cada evento de forma idempotente según
delivery_id(y/oorder_id); una repetición no debe realizar ninguna operación (no-op). - Verifique el HMAC antes de cualquier acción monetaria: no confíe en el cuerpo hasta que la firma coincida y
X-Netts-Timestampsea reciente. - Devuelva 2xx solo después de haber almacenado el evento de forma duradera: de lo contrario, (correctamente) reintentaremos el envío.
Responda con 2xx para confirmar la recepción; cualquier código distinto de 2xx o tiempo de espera agotado activará un reintento.
Orden de los intentos:
- Los reintentos se dirigen a su endpoint
primary. La ventana depende del tipo de pedido: los pedidos5mreintentan durante ~1 minuto; todos los demás tipos durante ~10 minutos. - Si la ventana se agota y usted registró un
backup, la entrega se traslada allí y el cronograma de reintentos comienza de nuevo, firmado con el propio secreto del respaldo. - Únicamente después de que el endpoint de respaldo también se agote, la entrega se marca como fallida definitivamente (dead).
Se utiliza el mismo delivery_id en todo momento, por lo que un mensaje que primero falló en el principal y luego tuvo éxito en el de respaldo sigue siendo un único evento para su lógica de desduplicación.
Referencia de códigos de error
| Código | Descripción | Estado HTTP |
|---|---|---|
10000 | Éxito (created / ok / updated / rotated) | 200 / 201 |
- | Eliminado (sin cuerpo) | 204 |
4000 | URL de webhook inválida o insegura (no es https, privada/loopback, credenciales, demasiado larga) | 400 |
-1 | Clave API inválida / IP fuera de la lista blanca | 401 |
-1 | Endpoint no encontrado (o no le pertenece) | 404 |
4090 | Límite de endpoints alcanzado (máx. 2: primary, backup) | 409 |
4091 | El rol solicitado ya está ocupado: intercámbielo con PATCH o elimine primero el endpoint existente | 409 |
4220 | Nada que actualizar (PATCH con cuerpo vacío) | 422 |
5003 | Error al crear el endpoint (inténtelo de nuevo) | 503 |
Límites de velocidad (Rate Limits)
Limitado por clave API (encabezado X-API-KEY):
| Período | Límite |
|---|---|
| 1 segundo | 5 solicitudes |
| 1 minuto | 150 solicitudes |
Límite de velocidad excedido (429)
{ "message": "API rate limit exceeded" }Notas
- El secreto se muestra una sola vez: al crear y al rotar. Nunca se devuelve mediante
GET/LIST. ¿Lo perdió? Rótelo para obtener uno nuevo. - Dos endpoints, no difusión en abanico: uno
primaryy unobackup. Cada pedido confirmado genera un webhook, entregado al principal; el de respaldo se usa solo si el principal se agota. - Cambio de URL sin tiempo de inactividad: registre la nueva dirección como
backup, verifíquela y luego apliquePATCHpara convertirla enprimary: el intercambio es atómico. - Pausar:
PATCH … {"is_active": false}detiene la entrega sin perder el endpoint. - Solo eventos de éxito:
delegation.confirmed,bandwidth.delegated,activation.confirmed. No existe ningún evento de fallo: un pedido fallido o con tiempo de espera agotado no genera ningún webhook. - Se pueden agregar nuevos tipos de eventos con el tiempo. Enrute mediante el campo
evente ignore los tipos que aún no maneje: nunca necesitará registrar nada nuevo para comenzar a recibirlos. - Los hashes se verifican on-chain antes de la entrega (consulte la nota al principio): un webhook contiene hashes que ya están todos incluidos en un bloque, o no se envía en absoluto.
- Las URL se validan para garantizar la seguridad frente a SSRF en el registro y en cada edición; el sistema de entrega vuelve a validarlas en el momento del envío.