Skip to main content

Webhook events

Webhooks are the optional push channel: instead of polling, TRONAgg POSTs a signed JSON event to your URL when something happens. Polling (GET /order/{id}, GET /deposits) always works without any of this — set up a webhook only if you want push.

Create one in Workspace → Notifications or with POST /webhooks.

The delivery

Every delivery is one HTTPS POST with one event:

POST https://your-service.example.com/tronagg/webhook
{
"id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "order.status_changed",
"created_at": "2026-07-12T10:30:00Z",
"audience": "customer",
"data": { }
}
FieldMeaning
idUnique event id — de-duplicate on this.
typeEvent type, also in the X-TronAgg-Event header.
created_atWhen it happened (ISO-8601, UTC).
audienceAlways customer for your webhooks.
dataEvent-specific payload — see the catalog.
HeaderValue
Content-Typeapplication/json
X-TronAgg-EventThe event type.
X-TronAgg-DeliveryDelivery id, stable across retries.
X-TronAgg-TimestampUnix seconds when the delivery was signed.
X-TronAgg-Signaturev1=<hex>verify it.
Delivery is at-least-once

Answer 2xx within a few seconds and do heavy work off the request path. Failed deliveries are retried with backoff, so the same event can arrive twice — de-duplicate on id.

Verify the signature

The signature is HMAC-SHA256 over "{timestamp}.{raw_body}" with your webhook's signing secret (shown once, at creation), hex-encoded as v1=<hex>. Verify over the raw request bytes, compare in constant time, and reject stale timestamps:

verify.py
import hashlib, hmac

def verify(secret: str, timestamp: str, body: bytes, signature_header: str) -> bool:
expected = hmac.new(
secret.encode(), timestamp.encode() + b"." + body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(f"v1={expected}", signature_header)
The same check in JavaScript (Node)
verify.js
const crypto = require("node:crypto");

function verify(secret, timestamp, rawBody, signatureHeader) {
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.`)
.update(rawBody) // Buffer of the exact bytes received
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(`v1=${expected}`),
Buffer.from(signatureHeader)
);
}

Events

On events fire out of the box on a webhook created via POST /webhooks; Off events start disabled — toggle any of them per webhook in Workspace → Notifications.

EventFires whenDefault
order.status_changedAn order finishes — COMPLETED or FAILED.✅ On
balance.creditedA deposit or credit lands on your balance.✅ On
autoenergy.delegatedAutoEnergy covers one transfer.✅ On
autoenergy.closedAn AutoEnergy subscription stops.✅ On
autoenergy.activatedAn AutoEnergy subscription becomes active.— Off
balance.lowBalance drops below your threshold.— Off
account.daily_summaryOnce-a-day account digest.— Off

order.status_changed

The push alternative to polling GET /order/{id}. Fires once the order is final.

{
"order_id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"status": "COMPLETED",
"resource_type": "ENERGY",
"resource_amount": "65000",
"receiver_address": "TC7UXt7t2WLGbFasqvErydRQZ7bnGbQs",
"total_price_sun": 3500000,
"error_message": null
}
FieldMeaning
statusCOMPLETED or FAILED (upper-case, matching the API).
resource_amountEnergy amount, as a string.
total_price_sunWhat you were charged, in SUN.
error_messageSet only when status is FAILED.

balance.credited

{
"entry_id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"entry_type": "deposit",
"amount_sun": 100000000,
"balance_sun": 250000000,
"reference_id": "a1b2c3d4e5f6..."
}
FieldMeaning
amount_sunAmount credited (SUN).
balance_sunYour balance after the credit (SUN).
entry_typedeposit for on-chain deposits.
reference_idSource of the credit, e.g. the deposit transaction.

autoenergy.delegated

Fires per covered transfer on a subscribed address.

{
"subscription_id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"address": "TC7UXt7t2WLGbFasqvErydRQZ7bnGbQs",
"energy_class": "131k",
"billed_sun": 2100000,
"used_tx_id": "a1b2c3d4e5f6...",
"tx_used_total": 5,
"quota_max_tx": 100
}
FieldMeaning
energy_classEnergy covered for this transfer — 65k or 131k.
billed_sunWhat was billed for it (SUN).
used_tx_idThe transfer that consumed the energy.
tx_used_total / quota_max_txTransfers covered so far / quota (null = uncapped).

autoenergy.closed

Fires when a subscription stops, for any reason.

{
"subscription_id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"address": "TC7UXt7t2WLGbFasqvErydRQZ7bnGbQs",
"close_reason": "quota",
"tx_used_total": 42
}
FieldMeaning
close_reasonWhy it stopped — quota, expired, idle, insufficient_balance, manual, … Treat as an open set.
tx_used_totalTotal transfers the subscription covered.

autoenergy.activated

Fires when a subscription becomes active on an address.

{
"subscription_id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"address": "TC7UXt7t2WLGbFasqvErydRQZ7bnGbQs"
}

balance.low

Fires once per crossing of the threshold you set when enabling it.

{
"entry_id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"entry_type": "purchase",
"amount_sun": -3500000,
"balance_sun": 12000000,
"reference_id": "a1b2c3d4e5f6...",
"threshold_sun": 50000000
}
FieldMeaning
balance_sunBalance after the change that crossed the threshold (SUN).
threshold_sunThe level you configured (SUN).
amount_sunThe change itself — negative for a debit.

account.daily_summary

{
"date": "2026-07-23",
"completed_orders": 12,
"energy_received": "780000",
"spent_sun": 42000000,
"balance_sun": 250000000
}
FieldMeaning
completed_ordersOrders completed that day.
energy_receivedTotal energy delegated to you (string).
spent_sun / balance_sunSpend that day / end-of-day balance (SUN).

Test your endpoint

POST /webhooks/{id}/test sends a notification.test event, signed exactly like a real delivery — use it to confirm your endpoint and signature check end to end:

{
"id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "notification.test",
"created_at": "2026-07-12T10:30:00Z",
"data": { "message": "TRONAgg notifications are connected." }
}

Next steps