Webhooks
Webhooks push order status changes to your server, so you don't have to poll. When you create an API key you may supply a callback_url; if present, every status change for orders created with that key is POSTed to it, signed with the key's secret.
Webhooks are optional. If you don't set a
callback_url, keep pollingGET /api/v1/energy/orders/:order_id.
Enabling webhooks
- In the portal, open API keys and create (or re-issue) a key.
- Enter a Webhook URL — an
https://endpoint you control. The URL is validated to be publicly reachable; private, loopback and link-local addresses are rejected. - Orders created with that key are delivered to the URL.
The callback URL is per API key. The key that created an order determines where its notifications go.
What you receive
A POST to your callback_url:
content-type: application/json
user-agent: tron-energy-aggregator-webhook/1.0
x-signature: t=1727520000,v1=<lowercase-hex>
Body:
{
"event": "order.status_changed",
"event_id": "ord_9c2b7f0a:COMPLETED",
"created_at": 1727520000,
"data": {
"order_id": "ord_9c2b7f0a",
"status": "COMPLETED",
"product": "TRON_ENERGY",
"order_type": "RENTAL",
"amount": 64400,
"duration_minutes": 60,
"target_address": "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
"customer_price": "2.093",
"order_total": "2.093",
"actual_consumed": "2.093",
"currency": "TRX",
"created_at": "2026-09-22T12:00:00.000Z",
"completed_at": "2026-09-22T12:00:05.000Z",
"failure_reason": null
}
}
eventis alwaysorder.status_changed.datais exactly the shape returned byGET /api/v1/energy/orders/:order_id;data.statusis one ofPENDING,COMPLETED,FAILED,RECONCILIATION_REQUIRED.event_idis${order_id}:${status}— stable across retries of the same transition. Deduplicate on it.
Verifying a delivery
The HMAC key is your API key's sk_… secret (the same string you sign requests with). The signature is:
v1 = hex(HMAC-SHA256(secret, `${t}.${rawBody}`))
where t is the t= component of x-signature (unix seconds) and rawBody is the exact JSON bytes received (unparsed). To verify:
- Reject if
|now − t| > 300seconds. - Recompute the HMAC over
${t}.${rawBody}and constant-time-compare it to thev1value.
Python and Go use only the standard library; the Rust example needs hmac, sha2 and hex.
import hashlib, hmac, re, time def verify(raw_body: bytes, x_signature: str, secret: str) -> bool: t_match = re.search(r"(?:^|,)t=(\d+)", x_signature) v_match = re.search(r"(?:^|,)v1=([0-9a-f]+)", x_signature) if not t_match or not v_match: return False t = int(t_match.group(1)) if abs(int(time.time()) - t) > 300: return False expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, v_match.group(1))
Always read the raw body before parsing JSON — the signature covers the exact bytes, and re-serializing the parsed object may change them.
Delivery semantics
- At-least-once. A delivery that fails (network error,
429,5xx) is retried with exponential backoff up to 5 attempts. - Duplicate handling. Retries use the same
event_id, so treat your handler as idempotent keyed onevent_id. - Ordering is not guaranteed. A final status change is always the authoritative state — read
data.status, never assume a sequence. - 2xx means success. Any
2xxresponse acknowledges the delivery.3xx,429and other non-2xx are treated as failures as described above. - Exhausted retries are dropped; polling remains the fallback for reading order state.
If your endpoint is down for longer than the retry window, poll GET /api/v1/energy/orders/:order_id to reconcile.
Security
- The URL must be
https://and publicly routable; redirects are never followed. - Only the API key that created an order receives its webhooks.
- Revoking a key stops further deliveries from it immediately.
