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:
{
"id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "order.status_changed",
"created_at": "2026-07-12T10:30:00.123456+00:00",
"audience": "customer",
"data": { }
}
| Field | Meaning |
|---|---|
id | Unique event id — de-duplicate on this. |
type | Event type, also in the X-TronAgg-Event header. |
created_at | When it happened: ISO-8601 with a UTC offset and microseconds, e.g. 2026-07-12T10:30:00.123456+00:00. |
audience | customer for events about your own account. Reseller accounts also receive reseller events about their customers' orders and their reseller balance — see Reseller events. Absent on notification.test. |
data | Event-specific payload — see the catalog. |
| Header | Value |
|---|---|
Content-Type | application/json |
X-TronAgg-Event | The event type. |
X-TronAgg-Delivery | Delivery id, stable across retries. |
X-TronAgg-Timestamp | Unix seconds when this attempt was signed. |
X-TronAgg-Signature | v1=<hex> — verify it. |
Answer 2xx within 10 seconds and do heavy work off the request path. Redirects are not followed: a 3xx counts as a failure, like any other non-2xx response, a timeout or a connection error.
A failed delivery is retried up to 4 more times — about 1 minute, 5 minutes, 30 minutes and 2 hours after each failed attempt — and then dropped. The same event can arrive more than once, so de-duplicate on id. Events are not guaranteed to arrive in order. If you must not miss a state, reconcile with GET /order/{id} or GET /orders.
Verify the signature
The signature is HMAC-SHA256 over "{timestamp}.{raw_body}", keyed with your webhook's signing secret (shown once, at creation), hex-encoded and sent as v1=<hex>. {timestamp} is the X-TronAgg-Timestamp header exactly as received.
- Verify over the raw request bytes, before any JSON parsing.
- Compare in constant time.
- Reject deliveries whose
X-TronAgg-Timestampis more than 5 minutes from your clock. Every retry is re-signed with a fresh timestamp, so a genuine delivery always passes this check.
import hashlib
import hmac
import time
TOLERANCE_SECONDS = 300
def verify(secret: str, timestamp: str | None, body: bytes, signature_header: str | None) -> bool:
"""timestamp = X-TronAgg-Timestamp, signature_header = X-TronAgg-Signature, body = raw bytes."""
if not timestamp or not signature_header:
return False
try:
signed_at = int(timestamp)
except ValueError:
return False
if abs(time.time() - signed_at) > TOLERANCE_SECONDS:
return False
expected = "v1=" + hmac.new(
secret.encode(), timestamp.encode() + b"." + body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected.encode(), signature_header.encode())
The same check in JavaScript (Node)
const crypto = require("node:crypto");
const TOLERANCE_SECONDS = 300;
// timestamp = X-TronAgg-Timestamp, signatureHeader = X-TronAgg-Signature,
// rawBody = Buffer of the exact bytes received.
function verify(secret, timestamp, rawBody, signatureHeader) {
if (typeof timestamp !== "string" || typeof signatureHeader !== "string") return false;
const signedAt = Number(timestamp);
if (!Number.isInteger(signedAt)) return false;
if (Math.abs(Date.now() / 1000 - signedAt) > TOLERANCE_SECONDS) return false;
const expected = Buffer.from(
"v1=" +
crypto
.createHmac("sha256", secret)
.update(`${timestamp}.`)
.update(rawBody)
.digest("hex")
);
const received = Buffer.from(signatureHeader);
// timingSafeEqual throws on buffers of different length.
if (received.length !== expected.length) return false;
return crypto.timingSafeEqual(expected, received);
}
Events
On events fire out of the box on a new webhook; Off events start disabled — toggle any of them per webhook in Workspace → Notifications. A webhook created with POST /webhooks also has order.status_changed switched on; one created in the workspace starts with it off.
Defaults apply when the webhook is created. Events added to TRONAgg later start disabled on existing webhooks — enable them in Workspace → Notifications.
Ignore event types you don't recognise and still answer 2xx: new events may be added.
| Event | Fires when | Default |
|---|---|---|
order.status_changed | An order finishes — COMPLETED or FAILED. | ✅ On (API) |
balance.credited | A deposit, or a transfer in from a reseller balance, is credited to your balance. | ✅ On |
balance.changed | Every balance movement — debits included. | — Off |
autoenergy.delegated | AutoEnergy covers one transfer. | ✅ On |
autoenergy.closed | TRONAgg stops an AutoEnergy subscription automatically. | ✅ On |
agent.sale | An Energy Agent payment turns into an Energy order. | ✅ On |
agent.payment_skipped | An Energy Agent payment is skipped. | ✅ On |
energy_pass.renewed | Your Energy Pass renews for another period. | ✅ On |
energy_pass.expired | Your Energy Pass ends. | ✅ On |
vault.payout.sent | An Energy Staking payout is sent for an address of yours. | ✅ On |
autoenergy.activated | An AutoEnergy subscription becomes active. | — Off |
balance.low | Balance drops below your threshold. | — Off |
account.daily_summary | Once-a-day account digest. | — Off |
provider.demand_changed is for capacity partners only. It appears in your webhook's subscription list but is never sent to other accounts.
TRONAgg Radar address events
TRONAgg Radar sends one address.* event for each alert. Connect a webhook and choose the Deposits & transfers, Energy added, Problems and Security groups in TRONAgg Radar → Alerts. All address events are on for a newly connected customer destination unless you turn a group off; selected destinations are applied per scenario. Webhooks never fold into a digest, even when a busy Telegram chat does.
Every data object includes address_id (UUID string), Base58Check address, display_name, nullable label and external_id, scenario_id (UUID string), scenario_name and event_id (integer Radar signal id, equal to the REST timeline event id). For chain events, tx_id, integer block_number, UTC block_time and finality (head, solid, reverted) are included; state/reconcile events have these keys with nullable references, while Energy-action events include them only when triggered by a chain event. Expiration events have no chain references. The internal scenario destination selection is never part of webhook data. null is possible for unknown balances, cost estimates and other nullable fields described in the full payload reference.
| Group | Event | Additional data keys and units | Meaning |
|---|---|---|---|
| Deposits & transfers | address.usdt.received | asset: "USDT", direction: "in", amount, balance: six-decimal USDT strings (balance nullable); counterparty: Base58Check | Official USDT arrives. |
| Deposits & transfers | address.usdt.sent | asset: "USDT", direction: "out", amount, balance: six-decimal USDT strings (balance nullable); counterparty: Base58Check | Official USDT leaves. |
| Deposits & transfers | address.trx.received | asset: "TRX", direction: "in", amount, balance: integer SUN (balance nullable); amount_trx: six-decimal TRX string; counterparty: Base58Check | TRX arrives (at least 10 TRX). |
| Deposits & transfers | address.trx.sent | asset: "TRX", direction: "out", amount, balance: integer SUN (balance nullable); amount_trx: six-decimal TRX string; counterparty: Base58Check | TRX leaves (at least 10 TRX). |
| Energy added | address.sweep.ready | episode_id: UUID string; balance: nullable six-decimal USDT string; energy_available, energy_required: integer Energy; expires_at: nullable UTC timestamp; source: radar or existing | Enough Energy for one transfer under the selected preset; wait for this before you sweep. |
| Energy added | address.energy.charged | action_id, order_id: UUID strings (order_id nullable); energy: integer Energy; total_price_sun: integer SUN; delegation_txid: nullable string; expires_at: nullable UTC timestamp | Energy delivered after a charge action. |
| Energy added | address.charge.skipped | action_id: UUID string; reason: radar_disabled, actions_disabled, energy_paused, outside_hours, frozen, contract, autoenergy_active, no_bandwidth, budget, limit, insufficient_balance | Charge skipped or deferred; some recorded skips are silent and emit no webhook. |
| Energy added | address.charge.failed | action_id: UUID string; reason: purchase_failed or order_failed | Charge failed. |
| Energy added | address.energy.expired | action_id: UUID string; order_id: nullable UUID string; energy: nullable integer Energy; expires_at: nullable UTC timestamp | Delegated Energy expired before an open sweep episode completed. |
| Problems | address.energy.insufficient | energy_available, energy_required, deficit: integer Energy (deficit is rounded order amount, not arithmetic subtraction) | Next transfer needs more Energy. |
| Problems | address.trx.low | trx_balance_sun, needed_sun: integer SUN; reason: bandwidth or multisig | Insufficient TRX for bandwidth or multisig. |
| Problems | address.trx.burned | trx_burned_sun: integer SUN; energy_used: integer Energy; would_cost_sun: nullable integer SUN | TRX was burned for Energy by an address transaction. |
| Problems | address.tx.failed | reason: out_of_energy or reverted; fee_limit_sun: nullable integer SUN; energy_required: nullable integer Energy (both null for reverted) | Transaction failed; a revert need not be an Energy shortage. |
| Problems | address.usdt.low | balance, threshold: six-decimal USDT strings | USDT crosses the configured low-balance threshold. |
| Security | address.permissions.changed | owner_before, owner_after: nullable {threshold: integer, keys: [{address: Base58Check, weight: integer}]}; active_changed: nullable boolean | Owner/active permissions changed. |
| Security | address.usdt.approval | spender: Base58Check; amount: six-decimal USDT string; unlimited: boolean | USDT allowance updated (unlimited when raw value ≥ 2²⁵⁵). |
| Security | address.usdt.frozen | frozen: boolean | Issuer froze or unfroze USDT. |
| Security | address.poisoning.suspected | lookalike_of, counterparty: Base58Check; direction: in or out; asset: USDT, TRX or null; token: nullable Base58Check contract; amount: nullable six-decimal string in the known asset | Lookalike counterparty appears in history. |
address.energy.insufficient and address.trx.low are not sent for addresses in an Only alert me scenario with Energy warnings off (energy_alerts: false). The webhook envelope's id is the delivery de-duplication key; data.event_id identifies the Radar signal in the replay API. Payload examples, replay and finality handling are in the TRONAgg Radar developer guide. Security changes are reported right after they happen. Busy Telegram chats may receive a text radar.digest summary instead of individual non-priority notifications; it is not a webhook event. Permission changes, issuer freezes and unlimited USDT approvals are never folded.
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
}
| Field | Meaning |
|---|---|
status | COMPLETED or FAILED (upper-case, matching the API). |
resource_amount | Amount of the order's resource_type (ENERGY or BANDWIDTH), as a string. |
total_price_sun | What you were charged, in SUN. 0 when an Energy Pass covered the whole Energy order. |
error_message | Set only when status is FAILED. |
balance.credited
Fires when a deposit (entry_type: deposit) or a transfer in from a reseller balance (reseller_transfer) is credited. Refunds and other credits appear only in balance.changed.
{
"entry_id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"entry_type": "deposit",
"amount_sun": 100000000,
"balance_sun": 250000000,
"reference_id": "0199a1b0-7c2e-7d41-9f3a-5b6c7d8e9f01"
}
| Field | Meaning |
|---|---|
amount_sun | Amount credited (SUN). |
balance_sun | Your balance after the credit (SUN). |
entry_type | deposit for on-chain deposits, reseller_transfer for money moved in from a reseller balance (a white-label bot top-up, or your own transfer from your bot balance). |
reference_id | For deposit, the deposit id (as in GET /deposits). Treat other values as opaque. |
balance.changed
The full balance feed: fires on every balance movement — purchases, deposits, refunds, withdrawals, AutoEnergy charges, Energy Pass payments and more — with the running balance after each one. High-volume accounts get one event per operation; keep it off unless you reconcile continuously. A purchase fully covered by an Energy Pass moves no balance and sends no event.
{
"entry_id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"entry_type": "purchase",
"amount_sun": -2210000,
"balance_sun": 33196070000,
"reference_id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b"
}
| Field | Meaning |
|---|---|
amount_sun | Signed amount (SUN): negative for debits, positive for credits. |
balance_sun | Your balance after this movement (SUN). |
entry_type | Movement type, e.g. purchase, refund, deposit, withdrawal, withdrawal_reversal, energy_pass, autoenergy_activate, autoenergy_daily, autoenergy_unit, autoenergy_penalty. Treat as an open set. |
reference_id | Id of what caused the movement: the deposit id (as in GET /deposits) for deposit, the order id (as in GET /order/{id}) for purchase and refund. Other entry types use other formats — treat it as opaque. May be null. |
threshold_sun | Present only when this same movement also tripped your low-balance rule — see balance.low. |
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
}
| Field | Meaning |
|---|---|
energy_class | Energy covered for this transfer — 65k or 131k. |
billed_sun | What was billed for it (SUN). |
used_tx_id | The transfer that consumed the energy. |
tx_used_total / quota_max_tx | Transfers covered so far / quota (null = uncapped). |
autoenergy.closed
Fires when TRONAgg stops a subscription automatically — its quota is used up, it expires, it sits idle, or your balance can no longer cover it. Closing a subscription yourself (API, workspace or Telegram bot) does not send this event.
{
"subscription_id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"address": "TC7UXt7t2WLGbFasqvErydRQZ7bnGbQs",
"close_reason": "quota",
"tx_used_total": 42
}
| Field | Meaning |
|---|---|
close_reason | Why it stopped — quota, expired, idle, insufficient_balance, reseller_insolvent (a white-label bot's service was stopped), … Treat as an open set. |
tx_used_total | Total transfers the subscription covered. |
autoenergy.activated
Fires when a subscription becomes active on an address.
{
"subscription_id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"address": "TC7UXt7t2WLGbFasqvErydRQZ7bnGbQs"
}
agent.sale
Fires when a payment to one of your Energy Agent configurations is turned into an Energy order for the sender.
{
"config_id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"public_id": "k7Qm2xPa",
"title": "Night Shift Energy",
"payer": "TLa2f6VPqDgRE67v1736s7bJ8Ray5wYjU7",
"amount_sun": 6000000,
"units": 2,
"energy": 130000,
"cost_sun": 3900000,
"order_id": "0199a1b2-e5f6-7a8b-9c0d-1e2f3a4b5c6d",
"tx_hash": "8e1a4f2c7b3d..."
}
| Field | Meaning |
|---|---|
payer | The customer address that paid and receives the Energy. |
amount_sun | What the customer paid (SUN). |
units / energy | Base payments counted / Energy ordered. |
cost_sun | What your balance was charged (SUN). Profit is amount_sun − cost_sun. |
order_id | The Energy order; its outcome arrives as order.status_changed. |
tx_hash | The customer's payment transaction. |
agent.payment_skipped
Fires when an Energy Agent payment can't be turned into Energy. Nothing is charged to your balance. Sent at most once per configuration, reason and hour; GET /agent/payments lists the individual skipped payments.
{
"config_id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"public_id": "k7Qm2xPa",
"title": "Night Shift Energy",
"payer": "TLa2f6VPqDgRE67v1736s7bJ8Ray5wYjU7",
"amount_sun": 3000000,
"reason": "insufficient_balance",
"paused": true,
"tx_hash": "3b7d9e0f1a2c..."
}
| Field | Meaning |
|---|---|
reason | below_base, price_above_payment, self_transfer, insufficient_balance, config_paused, program_disabled, order_rejected, … Treat as an open set; see skipped payments. |
paused | true when this payment also paused your configurations for lack of balance. |
energy_pass.renewed
Fires when your Energy Pass renews at the end of a period and the plan's price is debited from your balance.
{
"plan_name": "Pro",
"daily_energy": 650000,
"price_sun": 90000000,
"period_end": "2026-08-11T10:30:00.123456+00:00"
}
| Field | Meaning |
|---|---|
plan_name | The plan that renewed. |
daily_energy | Daily Energy allowance for the new period. |
price_sun | What was debited for the new period (SUN). |
period_end | When the new period ends (ISO-8601 with UTC offset). |
energy_pass.expired
Fires when your Energy Pass ends at the end of a period instead of renewing.
{
"plan_name": "Pro",
"reason": "insufficient_balance",
"price_sun": 90000000,
"balance_sun": 41000000
}
| Field | Meaning |
|---|---|
reason | auto_renew_off (you cancelled), insufficient_balance (your balance couldn't cover the renewal), plan_unavailable (the plan is no longer offered), program_disabled (Energy Pass is not available right now), account_blocked. Treat as an open set. |
price_sun | What a renewal of this plan costs now (SUN); null when the plan is no longer offered. |
balance_sun | Your balance when the renewal failed (SUN). Set only when reason is insufficient_balance, otherwise null. |
vault.payout.sent
Fires when an Energy Staking payout is sent for a delegating address you sign in with or have verified in the workspace.
{
"payout_id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"delegator_address": "TC7UXt7t2WLGbFasqvErydRQZ7bnGbQs",
"amount_sun": 12500000,
"tx_id": "5c9e2a7f1b3d..."
}
| Field | Meaning |
|---|---|
delegator_address | The delegating address the payout is for. |
amount_sun | Amount paid (SUN). |
tx_id | The on-chain payout transaction. |
balance.low
Fires when a balance movement takes your balance below the threshold. It won't fire again until your balance has gone back up to the threshold or above. Default threshold: 50 TRX (50000000 SUN); change it in Workspace → Notifications.
If balance.changed is also enabled on the same webhook, the alert arrives as that balance.changed event with threshold_sun set, not as a separate balance.low.
{
"entry_id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"entry_type": "purchase",
"amount_sun": -3500000,
"balance_sun": 12000000,
"reference_id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"threshold_sun": 50000000
}
| Field | Meaning |
|---|---|
balance_sun | Balance after the change that crossed the threshold (SUN). |
threshold_sun | The level you configured (SUN). |
amount_sun | The change itself — negative for a debit. |
The other fields mean the same as in balance.changed.
account.daily_summary
Sent once a day at the hour you choose (default 09:00 UTC).
{
"date": "2026-07-23",
"completed_orders": 12,
"energy_received": "780000",
"spent_sun": 42000000,
"balance_sun": 250000000
}
| Field | Meaning |
|---|---|
date | Report date in your chosen timezone. |
completed_orders | Orders completed in the 24 hours before the report hour. |
energy_received | Total Energy of those orders (string). |
spent_sun | What those orders cost (SUN). |
balance_sun | Your balance when the report was generated (SUN). |
Reseller events
If your account runs a reseller bot, each webhook created after you became a reseller also gets reseller subscriptions. These events carry "audience": "reseller".
| Event | Fires when | Default |
|---|---|---|
order.status_changed | An order placed through your bot finishes. | ✅ On |
reseller.balance.low | Your reseller balance drops below your threshold (default 500 TRX). | ✅ On |
reseller.bot_status_changed | Your bot is activated or paused, or its health check finds it unavailable or recovered. | ✅ On |
reseller.autoenergy.insolvent | Your reseller balance can't cover your below-cost AutoEnergy prices: new AutoEnergy connections in your bot are blocked and active subscriptions are stopped. At most once a day. | ✅ On |
reseller.daily_summary | Once-a-day digest of your bot's sales. | — Off |
When you buy through your own bot, you get only the reseller copy of order.status_changed, not a second customer copy.
order.status_changed (reseller audience) has the same fields as the customer event, plus:
| Field | Meaning |
|---|---|
total_price_sun | Your cost for the order (SUN), before your markup. |
markup_sun | Your markup on the order (SUN). |
customer | The buyer's Telegram @username or name, when known. |
reseller.balance.low: entry_id, entry_type, amount_sun, balance_sun, reference_id and threshold_sun. Same meaning as balance.low, for your reseller balance.
reseller.bot_status_changed:
| Field | Meaning |
|---|---|
bot_id / bot_username | The bot. |
source | operator — the bot was activated or paused (status active / paused). monitor — the health check found it unavailable or recovered; these events also carry a diagnostic details object. |
status / previous_status | New and previous state. Treat both as open sets. |
reseller.autoenergy.insolvent: reseller_id only.
reseller.daily_summary (sent at the hour you choose, default 09:00 UTC; covers the 24 hours before it):
| Field | Meaning |
|---|---|
date | Report date in your chosen timezone. |
orders / completed / failed | Orders through your bot, and how many completed or failed. |
turnover_sun | What your customers paid (SUN). |
revenue_sun | Your markup earned (SUN). |
new_users | New customers in your bot. |
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. It has no audience field. You can send up to 5 tests per hour per account, shared with the workspace Send test button.
{
"id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "notification.test",
"created_at": "2026-07-12T10:30:00.123456+00:00",
"data": { "message": "TRONAgg notifications are connected." }
}
Next steps
- Create a webhook — register one with your API key.
- Webhooks & Notifications — setup, channels, and the workspace flow.
- TRONAgg Radar — watch addresses and process signed
address.*alerts. - Track order status — polling, no webhook required.