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 polling GET /api/v1/energy/orders/:order_id.

Enabling webhooks

  1. In the portal, open API keys and create (or re-issue) a key.
  2. 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.
  3. 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
  }
}
  • event is always order.status_changed.
  • data is exactly the shape returned by GET /api/v1/energy/orders/:order_id; data.status is one of PENDING, COMPLETED, FAILED, RECONCILIATION_REQUIRED.
  • event_id is ${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:

  1. Reject if |now − t| > 300 seconds.
  2. Recompute the HMAC over ${t}.${rawBody} and constant-time-compare it to the v1 value.

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 on event_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 2xx response acknowledges the delivery. 3xx, 429 and 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.