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.confirmedis sent; failures/timeouts are not delivered.
Endpoint base URL
https://netts.io/apiv2/webhooksRequest Headers
| Header | Required | Description |
|---|---|---|
| Content-Type | Yes (for POST/PATCH) | application/json |
| X-API-KEY | Yes | Your API key from the Netts dashboard |
| X-Real-IP | Yes | IP 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).
// request body
{ "url": "https://your-server.example/netts/delegation-hook" }// 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.
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).
{
"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.
// 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",
"is_active": false,
"created_at": "2026-01-01T00:00:00",
"updated_at": "2026-01-01T00:00:01"
}
}
}Set
is_active: falseto pause delivery without deleting the endpoint;trueto 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.
// 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.
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:
{
"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"
}| Field | Type | Description |
|---|---|---|
event | string | Always delegation.confirmed (success only) |
delivery_id | int | Delivery ID — dedup key on your side |
order_id | string | Your order ID |
order_type | string | 1h or 5m |
receive_address | string | Full TRON address that received the energy |
energy_amount | int | Energy amount delegated |
tx_hash | string | Full delegation transaction hash |
delegation_timestamp | int? | Optional — present only when confirmed via the Mongo path; may be absent |
confirmed_at | string | UTC ISO-8601, confirmation moment |
Headers we send:
| Header | Value |
|---|---|
X-Netts-Event | delegation.confirmed |
X-Netts-Delivery | delivery_id (dedup) |
X-Netts-Timestamp | unix seconds at send time |
X-Netts-Signature | sha256=<hex>, hex = HMAC_SHA256(secret, "<timestamp>." + raw_body) |
User-Agent | netts-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).
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:
- Dedup is mandatory — process each event idempotently by
delivery_id(and/ororder_id); a repeat is a no-op. - Verify HMAC before any money action — don't trust the body until the signature matches and
X-Netts-Timestampis fresh. - 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
| Code | Description | HTTP Status |
|---|---|---|
10000 | Success (created / ok / updated / rotated) | 200 / 201 |
- | Deleted (no body) | 204 |
4000 | Invalid / unsafe webhook URL (not https, private/loopback, credentials, too long) | 400 |
-1 | Invalid API key / IP not in whitelist | 401 |
-1 | Endpoint not found (or not yours) | 404 |
4090 | Active endpoint limit reached (max 5) | 409 |
4220 | Nothing to update (PATCH with empty body) | 422 |
5003 | Failed to create endpoint (try again) | 503 |
Rate Limits
Limited per API key (header X-API-KEY):
| Period | Limit |
|---|---|
| 1 second | 5 requests |
| 1 minute | 150 requests |
Rate Limit Exceeded (429)
{ "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.confirmedis 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.