Skip to content

Webhooks — delegation confirmations

Register an HTTPS endpoint to receive a signed webhook the moment an energy-rental order (1h / 5m) is confirmed on-chain (delegation.confirmed). Once the delegation is confirmed, you can safely continue your flow (e.g. release USDT) knowing the obligation is fulfilled — instead of polling.

This page covers the management API (create / list / edit / rotate-secret / delete your endpoints) and the format of the webhook we deliver to you.

ℹ️ Roles. You manage your endpoints here. Delivery is performed by Netts asynchronously after a delegation is confirmed — there is nothing to poll. Only the success event delegation.confirmed is sent; failures/timeouts are not delivered.

Endpoint base URL

https://netts.io/apiv2/webhooks

Request Headers

HeaderRequiredDescription
Content-TypeYes (for POST/PATCH)application/json
X-API-KEYYesYour API key from the Netts dashboard
X-Real-IPYesIP address from your whitelist

Your user_id is derived from the API key — you never pass it. You can see and modify only your own endpoints.


Manage endpoints

Create — POST /apiv2/webhooks

Registers a new endpoint and returns a secret shown only once (store it — it signs every webhook you receive).

json
// request body
{ "url": "https://your-server.example/netts/delegation-hook" }
json
// response 201
{
    "detail": {
        "code": 10000,
        "status": "created",
        "data": {
            "id": 1,
            "url": "https://your-server.example/netts/delegation-hook",
            "secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
            "is_active": true,
            "created_at": "2026-01-01T00:00:00"
        }
    }
}

URL requirements (validated on create and on every edit):

  • must be https;
  • must resolve to a public address — loopback, private (RFC1918), link-local (incl. 169.254.169.254), and other non-routable ranges are rejected;
  • no credentials in the URL (user:pass@…);
  • length up to 2048 chars.

A rejected URL returns 400. You may have up to 5 active endpoints — the 6th returns 409.

bash
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"}'

List — GET /apiv2/webhooks

Returns your endpoints (the secret is never returned here).

json
{
    "detail": {
        "code": 10000,
        "status": "ok",
        "data": {
            "endpoints": [
                {
                    "id": 1,
                    "url": "https://your-server.example/netts/delegation-hook",
                    "is_active": true,
                    "created_at": "2026-01-01T00:00:00",
                    "updated_at": "2026-01-01T00:00:00"
                }
            ],
            "count": 1,
            "max_active": 5
        }
    }
}

Get one — GET /apiv2/webhooks/{id}

Same shape as a list item (no secret). A foreign or non-existent id returns 404.

Edit — PATCH /apiv2/webhooks/{id}

Change the url and/or is_active. Send any subset; an empty body returns 422. A changed url is re-validated (https / SSRF). A foreign or non-existent id returns 404.

json
// request body (any subset)
{ "url": "https://your-server.example/netts/new-hook", "is_active": false }
json
// response 200
{
    "detail": {
        "code": 10000,
        "status": "updated",
        "data": {
            "id": 1,
            "url": "https://your-server.example/netts/new-hook",
            "is_active": false,
            "created_at": "2026-01-01T00:00:00",
            "updated_at": "2026-01-01T00:00:01"
        }
    }
}

Set is_active: false to pause delivery without deleting the endpoint; true to resume.

Rotate secret — POST /apiv2/webhooks/{id}/rotate-secret

Generates a new secret and returns it once. The new secret takes effect immediately for subsequent deliveries — no further action needed.

json
// response 200
{
    "detail": {
        "code": 10000,
        "status": "rotated",
        "data": {
            "id": 1,
            "secret": "whsec_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
        }
    }
}

Delete — DELETE /apiv2/webhooks/{id}

Hard-deletes the endpoint. Returns 204 (no body); a foreign or non-existent id returns 404.

bash
curl -X DELETE https://netts.io/apiv2/webhooks/1 \
  -H "X-API-KEY: your_api_key" -H "X-Real-IP: your_whitelisted_ip"

The webhook we deliver

When one of your rental orders is confirmed, Netts sends a POST to each of your active endpoints.

Body (application/json, UTF-8). Addresses and hash are full values:

json
{
    "event": "delegation.confirmed",
    "delivery_id": 1,
    "order_id": "your_order_id",
    "order_type": "1h",
    "receive_address": "TXXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "energy_amount": 65000,
    "tx_hash": "0000000000000000000000000000000000000000000000000000000000000000",
    "delegation_timestamp": 1700000000000,
    "confirmed_at": "2026-01-01T00:00:00Z"
}
FieldTypeDescription
eventstringAlways delegation.confirmed (success only)
delivery_idintDelivery ID — dedup key on your side
order_idstringYour order ID
order_typestring1h or 5m
receive_addressstringFull TRON address that received the energy
energy_amountintEnergy amount delegated
tx_hashstringFull delegation transaction hash
delegation_timestampint?Optional — present only when confirmed via the Mongo path; may be absent
confirmed_atstringUTC ISO-8601, confirmation moment

Headers we send:

HeaderValue
X-Netts-Eventdelegation.confirmed
X-Netts-Deliverydelivery_id (dedup)
X-Netts-Timestampunix seconds at send time
X-Netts-Signaturesha256=<hex>, hex = HMAC_SHA256(secret, "<timestamp>." + raw_body)
User-Agentnetts-webhook/1.0

Verifying the signature

The signature follows the Stripe scheme (timestamp.body), computed over the raw bytes we send. Recompute it with your secret, compare constant-time, and reject if X-Netts-Timestamp is outside a ±5 minute window (replay protection).

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

Delivery semantics (important — at-least-once)

Delivery is at-least-once: a dropped response can cause a retry, so you may receive the same event twice. Because the business action (releasing USDT) is money-sensitive:

  1. Dedup is mandatory — process each event idempotently by delivery_id (and/or order_id); a repeat is a no-op.
  2. Verify HMAC before any money action — don't trust the body until the signature matches and X-Netts-Timestamp is fresh.
  3. Return 2xx only after you've durably stored the event — otherwise we (correctly) retry.

Retry window depends on order type: 5m orders retry for ~1 minute, 1h orders for ~10 minutes, then the delivery is marked dead. Respond 2xx to acknowledge; any non-2xx / timeout triggers a retry.


Error Code Reference

CodeDescriptionHTTP Status
10000Success (created / ok / updated / rotated)200 / 201
-Deleted (no body)204
4000Invalid / unsafe webhook URL (not https, private/loopback, credentials, too long)400
-1Invalid API key / IP not in whitelist401
-1Endpoint not found (or not yours)404
4090Active endpoint limit reached (max 5)409
4220Nothing to update (PATCH with empty body)422
5003Failed to create endpoint (try again)503

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

Notes

  • Secret is shown once — on create and on rotate. It is never returned by GET/LIST. Lost it? rotate to get a new one.
  • Multiple integrations: up to 5 active endpoints; every confirmed order is delivered to all of them.
  • Pausing: PATCH … {"is_active": false} stops delivery without losing the endpoint.
  • Only delegation.confirmed is delivered (success). There is no failure event.
  • URLs are validated for SSRF safety at registration and on every edit; the delivery side re-validates at send time.