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:00.123456+00:00",
"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 with a UTC offset and microseconds, e.g. 2026-07-12T10:30:00.123456+00:00.
audiencecustomer 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.
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 this attempt was signed.
X-TronAgg-Signaturev1=<hex> — verify it.
Answer fast, expect duplicates

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-Timestamp is more than 5 minutes from your clock. Every retry is re-signed with a fresh timestamp, so a genuine delivery always passes this check.
verify.py
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)
verify.js
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.

EventFires whenDefault
order.status_changedAn order finishes — COMPLETED or FAILED.✅ On (API)
balance.creditedA deposit, or a transfer in from a reseller balance, is credited to your balance.✅ On
balance.changedEvery balance movement — debits included.— Off
autoenergy.delegatedAutoEnergy covers one transfer.✅ On
autoenergy.closedTRONAgg stops an AutoEnergy subscription automatically.✅ On
agent.saleAn Energy Agent payment turns into an Energy order.✅ On
agent.payment_skippedAn Energy Agent payment is skipped.✅ On
energy_pass.renewedYour Energy Pass renews for another period.✅ On
energy_pass.expiredYour Energy Pass ends.✅ On
vault.payout.sentAn Energy Staking payout is sent for an address of yours.✅ On
autoenergy.activatedAn AutoEnergy subscription becomes active.— Off
balance.lowBalance drops below your threshold.— Off
account.daily_summaryOnce-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.

GroupEventAdditional data keys and unitsMeaning
Deposits & transfersaddress.usdt.receivedasset: "USDT", direction: "in", amount, balance: six-decimal USDT strings (balance nullable); counterparty: Base58CheckOfficial USDT arrives.
Deposits & transfersaddress.usdt.sentasset: "USDT", direction: "out", amount, balance: six-decimal USDT strings (balance nullable); counterparty: Base58CheckOfficial USDT leaves.
Deposits & transfersaddress.trx.receivedasset: "TRX", direction: "in", amount, balance: integer SUN (balance nullable); amount_trx: six-decimal TRX string; counterparty: Base58CheckTRX arrives (at least 10 TRX).
Deposits & transfersaddress.trx.sentasset: "TRX", direction: "out", amount, balance: integer SUN (balance nullable); amount_trx: six-decimal TRX string; counterparty: Base58CheckTRX leaves (at least 10 TRX).
Energy addedaddress.sweep.readyepisode_id: UUID string; balance: nullable six-decimal USDT string; energy_available, energy_required: integer Energy; expires_at: nullable UTC timestamp; source: radar or existingEnough Energy for one transfer under the selected preset; wait for this before you sweep.
Energy addedaddress.energy.chargedaction_id, order_id: UUID strings (order_id nullable); energy: integer Energy; total_price_sun: integer SUN; delegation_txid: nullable string; expires_at: nullable UTC timestampEnergy delivered after a charge action.
Energy addedaddress.charge.skippedaction_id: UUID string; reason: radar_disabled, actions_disabled, energy_paused, outside_hours, frozen, contract, autoenergy_active, no_bandwidth, budget, limit, insufficient_balanceCharge skipped or deferred; some recorded skips are silent and emit no webhook.
Energy addedaddress.charge.failedaction_id: UUID string; reason: purchase_failed or order_failedCharge failed.
Energy addedaddress.energy.expiredaction_id: UUID string; order_id: nullable UUID string; energy: nullable integer Energy; expires_at: nullable UTC timestampDelegated Energy expired before an open sweep episode completed.
Problemsaddress.energy.insufficientenergy_available, energy_required, deficit: integer Energy (deficit is rounded order amount, not arithmetic subtraction)Next transfer needs more Energy.
Problemsaddress.trx.lowtrx_balance_sun, needed_sun: integer SUN; reason: bandwidth or multisigInsufficient TRX for bandwidth or multisig.
Problemsaddress.trx.burnedtrx_burned_sun: integer SUN; energy_used: integer Energy; would_cost_sun: nullable integer SUNTRX was burned for Energy by an address transaction.
Problemsaddress.tx.failedreason: 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.
Problemsaddress.usdt.lowbalance, threshold: six-decimal USDT stringsUSDT crosses the configured low-balance threshold.
Securityaddress.permissions.changedowner_before, owner_after: nullable {threshold: integer, keys: [{address: Base58Check, weight: integer}]}; active_changed: nullable booleanOwner/active permissions changed.
Securityaddress.usdt.approvalspender: Base58Check; amount: six-decimal USDT string; unlimited: booleanUSDT allowance updated (unlimited when raw value ≥ 2²⁵⁵).
Securityaddress.usdt.frozenfrozen: booleanIssuer froze or unfroze USDT.
Securityaddress.poisoning.suspectedlookalike_of, counterparty: Base58Check; direction: in or out; asset: USDT, TRX or null; token: nullable Base58Check contract; amount: nullable six-decimal string in the known assetLookalike 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
}
FieldMeaning
statusCOMPLETED or FAILED (upper-case, matching the API).
resource_amountAmount of the order's resource_type (ENERGY or BANDWIDTH), as a string.
total_price_sunWhat you were charged, in SUN. 0 when an Energy Pass covered the whole Energy order.
error_messageSet 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"
}
FieldMeaning
amount_sunAmount credited (SUN).
balance_sunYour balance after the credit (SUN).
entry_typedeposit 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_idFor 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"
}
FieldMeaning
amount_sunSigned amount (SUN): negative for debits, positive for credits.
balance_sunYour balance after this movement (SUN).
entry_typeMovement 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_idId 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_sunPresent 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
}
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 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
}
FieldMeaning
close_reasonWhy it stopped — quota, expired, idle, insufficient_balance, reseller_insolvent (a white-label bot's service was stopped), … 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"
}

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..."
}
FieldMeaning
payerThe customer address that paid and receives the Energy.
amount_sunWhat the customer paid (SUN).
units / energyBase payments counted / Energy ordered.
cost_sunWhat your balance was charged (SUN). Profit is amount_sun − cost_sun.
order_idThe Energy order; its outcome arrives as order.status_changed.
tx_hashThe 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..."
}
FieldMeaning
reasonbelow_base, price_above_payment, self_transfer, insufficient_balance, config_paused, program_disabled, order_rejected, … Treat as an open set; see skipped payments.
pausedtrue 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"
}
FieldMeaning
plan_nameThe plan that renewed.
daily_energyDaily Energy allowance for the new period.
price_sunWhat was debited for the new period (SUN).
period_endWhen 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
}
FieldMeaning
reasonauto_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_sunWhat a renewal of this plan costs now (SUN); null when the plan is no longer offered.
balance_sunYour 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..."
}
FieldMeaning
delegator_addressThe delegating address the payout is for.
amount_sunAmount paid (SUN).
tx_idThe 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
}
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.

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
}
FieldMeaning
dateReport date in your chosen timezone.
completed_ordersOrders completed in the 24 hours before the report hour.
energy_receivedTotal Energy of those orders (string).
spent_sunWhat those orders cost (SUN).
balance_sunYour 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".

EventFires whenDefault
order.status_changedAn order placed through your bot finishes.✅ On
reseller.balance.lowYour reseller balance drops below your threshold (default 500 TRX).✅ On
reseller.bot_status_changedYour bot is activated or paused, or its health check finds it unavailable or recovered.✅ On
reseller.autoenergy.insolventYour 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_summaryOnce-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:

FieldMeaning
total_price_sunYour cost for the order (SUN), before your markup.
markup_sunYour markup on the order (SUN).
customerThe 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:

FieldMeaning
bot_id / bot_usernameThe bot.
sourceoperator — 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_statusNew 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):

FieldMeaning
dateReport date in your chosen timezone.
orders / completed / failedOrders through your bot, and how many completed or failed.
turnover_sunWhat your customers paid (SUN).
revenue_sunYour markup earned (SUN).
new_usersNew 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​