What a TRON energy API actually does
A TRON energy API sells one thing, and it is not a discount on a fee. Your account needs energy to run the USDT contract, and without it the network takes TRX instead, at 100 sun for every unit. An energy API arranges for a different account — one that has staked TRX — to lend its energy to the address that will sign your transfer, for a few minutes. Everything below is the shape of the requests around that single on-chain fact.
On chain it is a delegation, and you sign none of it
The lending account signs a delegation naming your address. Your address signs nothing, approves nothing, and grants nothing back: the network raises its energy limit, and that is the whole of the event (what energy is). Which is why an energy API asks for an address and never for a key — the only signature in the arrangement belongs to the other side.
The smallest flow that works
The base URL is https://api.nrg.market/v1 and the key travels in one header, Authorization: Bearer nrg_live_…. It is shown once when issued and can be pinned to a list of IPs. Call it from your server: the authenticated endpoints allow no browser origin at all, because a key that reaches a page is a key everyone has. There is a second base URL, on a sandbox subdomain of nrg.market — the same /v1 surface over test money and a simulated chain, with its own nrg_test_… keys and its own cabinet, and it is documented at docs.nrg.market.
Start with POST /v1/estimate. It reserves nothing and charges nothing, takes up to 500 recipients in one call, and returns what an order would compute.
curl -s https://api.nrg.market/v1/estimate \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"duration_s":300,
"transfers":[{"from":"TQ5NMqJjW8sBGqcpvUcXhbXm3jGnyDCmwK",
"to":"TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9"}]}'
{"items":[{"to":"TN3W…","kind":"single","energy_units":65000,"warnings":[]}],
"zone":"day","duration_s":300}
# trimmed — the answer also carries price_trx per item, total_trx and burn_cost_trx
kind is single for a recipient that already holds USDT, double for one that does not and needs a token account created first, and custom for a contract, priced from a dry-run rather than from either standard number. from is optional and moves no price; giving it buys two answers about the sender — inactive_sender and blacklisted_sender among the warnings — before an order in the mode you are using can refuse on one of them.
Then the order. Mode a is the one where you broadcast the transfer yourself.
curl -s https://api.nrg.market/v1/orders \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-H "Idempotency-Key: 8f3c1e02-6b47-4a1d-9d5c-0b2e7c4a1f90" \
-d '{"mode":"a","client_ref":"invoice-1042",
"transfers":[{"from":"TQ5NMqJjW8sBGqcpvUcXhbXm3jGnyDCmwK",
"to":"TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9"}]}'
The answer is 202 with two headers worth reading rather than assembling: Location, the order's own URL, and Retry-After, the seconds to wait before the first poll and between polls. The body is the order, normally with status at funded and send_before still null — the money is reserved and the purchase has started.
send_before appears when the energy is confirmed on chain and the order reaches ready. It is a window of about four minutes, counted from the last confirmed delegation, so every item of a batch shares one deadline; read the field rather than assume the four minutes. Watch for the field rather than for the word ready, too: an order in mode A is fulfilled by the delivery of energy and not by your transfer, so completed can arrive before you have sent anything.
Two refusals are worth wiring before the first live order: 402 insufficient_balance, decided before any money moves and carrying required_trx and available_trx, and 422 invalid_address with details.reason set to inactive_wallet — the sending wallet was never activated, so there is no account to delegate to (the failures behind these).
The idempotency key
POST /v1/orders requires an Idempotency-Key, and it is bound to the raw bytes of the body rather than to the parsed JSON. The same bytes return the original order; a different body under the same key is 409 idempotency_conflict and not a second order; a request turned down by validation does not consume the key. At batch size that is what turns a network timeout into a question with an answer.
What the rate-limit headers tell you
Three buckets are counted per API key: orders at 120 a minute, reference requests at 600, and GET /v1/address-check at 60, because every miss of its cache is a question to the chain. Every answer from a keyed bucket, the successful ones included, carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset, so a client can slow down before the refusal rather than after it; past the limit it is 429 rate_limited with Retry-After. One other 429 means close to the opposite — too_many_auth_failures, where waiting does not help because the credential itself is being refused. Branch on code, never on message.
Webhooks, so the loop is not a poll
Register a receiver with POST /v1/webhooks: the url has to be https, resolve to a public host and carry no credentials. The signing secret comes back once, on creation, and an account holds at most five active endpoints. Subscribe to what the flow needs — order.ready, order.completed, order.partially_completed, order.failed, plus the per-item order.item.sent and order.item.expired. Every delivery carries X-NRG-Timestamp, X-NRG-Event-Id and X-NRG-Signature, an HMAC-SHA256 over the timestamp, a dot and the raw body: verify against the bytes you received rather than JSON you re-encoded, and deduplicate on event_id. POST /v1/webhooks/{webhook_id}/test puts a real signed delivery down the actual path and reports what your endpoint replied, at no cost.
Mode B: you sign it, we broadcast it
Mode b replaces from and to with signed_tx, the signed transaction in hex, and we broadcast it the moment the energy lands — nobody sits waiting on a four-minute window. What is accepted is narrow on purpose: a transfer call of the USDT contract and nothing else, one owner signature, no TRX attached, and multisig refused, because a permission's key set lives on chain and can change without our knowing. The transaction needs three minutes of life left when the order is created, and its own expiry shortens the window — send_before never runs past that deadline less a broadcast margin. Fail to relay it in time and the item closes failed with broadcast_window_missed, reservation back in full.
Mode C: energy with no recipient named
Mode c takes from and kind and no recipient at all — for a wallet you want ready before the batch exists. kind is single or double, and it is yours to get right: with no recipient there is nothing to read the volume from, so single where double was needed leaves the transfer short and the difference burns TRX. custom is deliberately not on offer here, being the outcome of a dry-run against a specific contract recipient. Items carry to: null, never reach sent, and become expired when the window closes: the lease ran out, which is the normal ending rather than a failure — and charged, because the energy was delivered.
What is never in the request
There is no field for a private key anywhere in the contract, in any mode. Mode A never sees your transaction. Mode B sees one you have already signed, which we cannot alter by a byte without breaking that signature; it is stored encrypted and erased seven days after the order is final. Mode C has no transaction to see. What the API is given is addresses (the rest of what we hold and do not hold).
The parts that need no key
The reference endpoints and the contract need no key at all, and are rate limited by IP rather than by key. GET /v1/tariff answers the price of a unit of energy in sun right now, the zone running, next_change_at and a reference schedule — it is what the pricing page reads at every load. GET /v1/market answers what the vendors we watch publish for themselves, each figure stamped with the moment it was read and our own price ranked among them, which is the market page. Quote the endpoint rather than a number: our price is recomputed every few minutes.
The contract itself is at https://docs.nrg.market/openapi.yaml, and byte for byte the same document at https://api.nrg.market/v1/contract.yaml — worth knowing, because the documentation host sits behind an edge that refuses some programmatic clients before the request reaches us. ETag there is the SHA-256 of the body, so If-None-Match gets a 304 when nothing has changed. Generate your client from that file: this page is a summary, and the file is what we are held to.