GET /apiv2/balances/
Read the balances of any TRON address: right now, at a past moment, over time, or summed up for a period. Six endpoints, all synchronous — the answer comes in the reply, there is no queue and nothing to poll.
Endpoint base URL
https://netts.io/apiv2/balances/{address}{address} is a TRON address in base58, exactly 34 characters.
Request headers
| Header | Required | Description |
|---|---|---|
X-API-KEY | yes | API key from the dashboard |
X-Real-IP | yes | An address from the key whitelist |
Your account balance must be at least 4 TRX. A depleted account is answered with 402 before the request reaches the data.
The six endpoints
| Endpoint | Answers |
|---|---|
GET /apiv2/balances/{address} | every token held right now |
GET /apiv2/balances/{address}/at?on=YYYY-MM-DD | balances at the end of that date, UTC |
GET /apiv2/balances/{address}/at-block?block=N | balances at an exact block, or at ts=YYYY-MM-DD HH:MM:SS |
GET /apiv2/balances/{address}/history?token_id=TRX&days=90 | how one token moved, day by day |
GET /apiv2/balances/{address}/summary?date_from=&date_to= | opening, inflow, outflow, fees and closing per token |
GET /apiv2/balances/{address}/statement?date_from=&date_to= | statement preview with individual operations |
Example
curl -s 'https://netts.io/apiv2/balances/TXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' \
-H 'X-API-KEY: your-api-key' \
-H 'X-Real-IP: 203.0.113.10'{
"status": "success",
"code": 0,
"msg": "",
"data": {
"address": "TXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"as_of_block": 86012345,
"live": false,
"hide_spam": false,
"total_value_usd": "385.25",
"balances": [
{
"token_id": "TRX",
"symbol": "TRON",
"token_type": "TRX",
"decimals": 6,
"balance": "1141.899000",
"price_usd": "0.334968",
"value_usd": "382.50",
"is_verified": true,
"is_spam": false,
"balance_source": "events",
"node_balance": "1141.899000"
}
]
}
}Things worth knowing before you integrate
Amounts are strings, not numbers. "1141.899000" is a decimal serialised as text so that no precision is lost to a floating-point round trip. Parse it with a decimal type, not a float.
In a reply about the past, balance is the answer and node_balance is not. balance is the amount at the point you asked about. node_balance is what the chain holds right now, in every reply — so in an answer about last month it still shows today's number. Never display it as the historical amount. For a question about now the roles swap; that is the next section.
A date means the end of that day. ?on=2026-09-01 answers for 2026-09-01 23:59:59Z. If you need the start of a day, ask for the end of the previous one, or use /at-block with an explicit ts.
Two balances, and why "now" is the tricky one
A reply carries two different numbers, and on an active address they do not agree:
| Field | What it is | When it is exact |
|---|---|---|
balance | The ledger value, rebuilt from indexed chain events up to the block reported in as_of_block | Exact for that block — which is not the newest block |
node_balance | What a TRON node holds right now | Always current, never historical |
balance is not "the balance on the chain right now". It is the balance as of as_of_block. When you need the chain's present number, read node_balance — it is taken from a TRON node at request time and, for TRX, by the full formula: liquid balance plus staked frozenV2 plus what is delegated out. On an address holding 41.7 million staked TRX it answered 42035672.226020, which is exactly 237799.226020 + 41760434 + 36816 + 623. Reading only the node's plain balance field would have shown 237 thousand and been wrong by two orders of magnitude.
But node_balance is not filled in for every row. TRX and every TRC10 come back in one getaccount call, so they always carry it. TRC20 cannot: a node has no way to list the TRC20 tokens an address holds, so balanceOf is only asked for the major ones. Measured on a wallet with 504 TRC20 rows, 494 of them came back null. That null is policy rather than a failure, and the same null appears if the node is briefly unreachable. So for TRX and TRC10 the chain's present value is always available to you; for a long-tail TRC20 all you have is balance and the block it stands on.
The ledger advances to a block only once every indexing writer has confirmed that block, and its watermark is the minimum across all of them. The slowest writers publish their marks in batches, so the gap widens steadily and then snaps back — a sawtooth, not a constant.
Sampled over a 20-minute window on 6 September 2026:
| Blocks behind the head | Time behind | |
|---|---|---|
| Best | 22 | ~1 min |
| Median | 44 | ~2 min |
| 90th percentile | 86 | ~4 min |
| Worst observed | 121 | ~6 min |
Plan for the ledger being a few minutes behind the chain, not a few seconds.
What follows from that:
- A quiet address is exact even for "now". Once nothing has moved for longer than the current lag, the ledger has caught up and
balanceequalsnode_balance. - On an address that just transacted,
balancecan be wrong in either direction — too low while an incoming transfer is still unindexed, too high while an outgoing one is. live=truenarrows the gap without closing it. It applies the tail of transfer events betweenas_of_blockand the chain head on the fly, for 30–80 ms. It does not moveas_of_block, it does not account for fees, and it deliberately skips TRC10 tokens, which are tracked by a separate index. A measured example: an address whose ledger reported157.317444TRX answered766.194807withlive=true, while the node held1698.995472. Helpful, butnode_balancestays the only field that is the chain's current value.- Historical answers are exact, full stop.
/at,/at-block,/history,/summaryand/statementdescribe points the ledger passed long ago. There is no lag to account for.
For accounting, reconciliation and statements, use the historical endpoints and trust them. For a live wallet screen, show node_balance where it is present — which covers TRX and every TRC10 — and fall back to balance with as_of_block beside it where it is null, so the reader knows which block the number stands on.
/history returns only the days that had activity. Asking for days=7 on an address that moved on three of them returns three points, not seven. Each point carries that day's closing balance and the delta against the previous point.
/summary reconciles itself. For every token opening_balance + period_in − period_out − period_fees = closing_balance, and the engine returns that arithmetic already written out in control_formula, for example 76493.940406 + 141490.950160 - 216763.731366 - 79.260200 = 1141.899000. Fees are broken out into period_fees_energy and period_fees_bandwidth. Note that /summary may report a small negative balance for a token that the current-balance endpoint leaves out entirely.
/statement is capped by ops_limit. It accepts 10 to 5000 operations; anything outside that range is a 422. operations_total is the real count for the period and operations_truncated says whether the list was cut short. token_id defaults to TRX when omitted. For a complete statement beyond 5000 operations, order a file instead — see Statement files.
Token order is deliberate. TRX and the major stablecoins come first, then verified tokens that have a price, then everything else. Do not re-sort by amount: airdropped spam often carries enormous nominal balances and would float to the top.
Spam is marked, not removed. is_spam flags tokens classified as deceptive. Pass hide_spam=true to drop them from the reply; TRX and USDT are never hidden.
Rate limits
Each endpoint accepts 10 requests per second, shared across all clients of that endpoint. The limit is per endpoint, so /history and /summary do not compete with each other.
Exceeding it gives 429 with Retry-After: 1, alongside RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset. Retry after the stated delay.
A second, much wider limit of 100 requests per second per source IP applies across the whole API. The two are told apart by the message: the endpoint limit says Endpoint rate limit exceeded (10 req/s shared), the account-wide one says API rate limit exceeded.
Errors
| HTTP | Meaning |
|---|---|
400 | the address is 34 characters but fails its base58 checksum |
401 | key missing or invalid, or the source IP is not whitelisted |
402 | account balance below the 4 TRX minimum |
403 | the API key is blocked; contact support |
422 | a parameter is missing or out of range — a wrong address length, or an ops_limit outside 10–5000 |
429 | rate limit exceeded |
503 | the balance engine did not answer; the request was not counted, retry |
Error bodies come in three shapes, depending on which layer rejected the request. Match on the HTTP status, not on the body.
// 401, 402, 403 — the gateway, before the request reaches the service
{"detail": {"code": 1004, "msg": "Insufficient funds. Minimum balance is 4 TRX. Please top up your account."}}
// 400, 422 — the service, after parameters are parsed
{"status": "error", "code": -4, "msg": "address must be base58 (T...)", "data": {"code": -4, "msg": "address must be base58 (T...)"}}
// 429 — the rate limiter
{"message": "Endpoint rate limit exceeded (10 req/s shared), retry shortly", "request_id": "e5dbf25437d2eb215c764617d50b8d3b"}Related
- Statement files — the full statement as a CSV or PDF file
- Report webhooks — being told when a file is ready