How to Automate TRON Energy Rental with the API

2026-05-13

Why Automate Energy Rental at All

If you are running a hot wallet, an exchange withdrawal engine, or a contract that triggers TRC-20 transfers on behalf of users, you already know the pain: you need TRON energy ready the second a transaction fires. Topping up energy by hand through a UI does not scale past a handful of transfers a day. Miss the window and the transaction burns TRX from the sending address instead, often at a much higher cost than pre-rented energy.

The fix is to call the rental API the moment your backend is about to broadcast a transaction, so energy is provisioned programmatically with no human in the loop. This article walks through exactly how to do that against tronenergyrent.com: the real request shape, the asynchronous order lifecycle, sensible duration tradeoffs, and a working Python example you can drop into your service.

What the API Actually Does On-Chain

Before writing code, it helps to understand the on-chain mechanics so you are not debugging blind.

TRON's resource model splits computation (energy) from transaction size (bandwidth). A standard USDT TRC-20 transfer consumes roughly 65,000 energy when the recipient already holds USDT, and about 130,000 when the recipient's USDT balance is zero. The first incoming transfer pays the storage cost of creating the balance entry, which is why the cost depends entirely on the recipient, not the sender. Bandwidth usage for a transfer is around 345 bytes, which is small enough that most accounts cover it from their daily free allowance.

When you call the rental API, the service stakes its own TRX and delegates the resulting energy to the address you specify, using DelegateResourceContract from Stake 2.0. Delegation is address-specific and time-bounded. Once the rental period expires, the energy is reclaimed by the provider automatically.

One important detail: the rental is asynchronous. The API call returns immediately with an orderId and a state of PAID_BY_USER, but the on-chain delegation transaction is broadcast in the background and typically lands within a few seconds. Your integration should treat the initial response as confirmation that the order was accepted and paid, then poll the order details endpoint until the state moves to ENERGY_DELEGATED.

Choosing the Right Rental Duration

The API accepts a period parameter with four allowed values: 1h, 1d, 3d, 30d. Prices float with the on-chain energy market and shift during the day, so the live numbers always live on the pricing page. The relative ordering is stable: 1h is cheapest per call, 30d is cheapest when amortized across many transfers from the same address.

For an event-driven system where you trigger one rental per outgoing transfer, the 1h tier is almost always the right call. You pay the lowest absolute cost and the energy is consumed within seconds of being delegated. The 1d and longer tiers make sense when you are running predictable batch workloads, for example a nightly payout job, and want to rent a large energy block once instead of calling the API dozens of times.

If your system consistently fires more than 20-30 transfers per hour from the same address, renting a 1d block sized to cover your expected volume is cleaner than per-transfer calls. The upfront cost goes up but the API overhead and on-chain confirmation latency disappear from the hot path.

Authentication and Request Structure

The rental endpoint lives at:

GET https://api.tronenergyrent.com/place-energy-order

It is a plain GET request with query parameters. Authentication is the apiKey query parameter, which you generate from your dashboard after registering and funding your account. There is no header-based auth and no JSON request body for this endpoint.

The parameters are:

  • apiKey (required): your API key from the dashboard.
  • period (required): rental duration, one of 1h, 1d, 3d, 30d.
  • energyAmount (required): how much energy to delegate. Minimum is 15000. For one standard USDT transfer to a recipient who already holds USDT, 65000 is the safe number; for a first transfer to a fresh USDT holder, use 130000.
  • destinationAddress (required): the TRON address (base58check, starts with T) that will receive the delegated energy.
  • preActivateDestinationAddress (optional, default 0): set to 1 if the destination address has never received any TRX and is therefore not activated on-chain. The service then sends 1.5 TRX from your prepaid balance to activate the address before delegating energy. If the address is already active, leave this at 0 to avoid the extra cost.

The response is always returned with HTTP status 200, regardless of whether the order succeeded or failed. Branch on the JSON body's status field instead of the HTTP status. A success body looks like this:

{
  "status": "SUCCESS",
  "errorCode": null,
  "errorDescription": null,
  "requestId": "2651eacd-2428",
  "payload": {
    "orderId": "128de799-501e-44b2-8d6f-1fa825c2deed",
    "totalPriceSun": 5662800,
    "totalPriceTrx": 5.6628,
    "state": "PAID_BY_USER"
  }
}

An error body has status: "ERROR", a machine-readable errorCode, and a human-readable errorDescription:

{
  "status": "ERROR",
  "errorCode": "INVALID_ENERGY_AMOUNT",
  "errorDescription": "energyAmount is less than 15000",
  "requestId": "71431087-4",
  "payload": null
}

Always log requestId on both branches. If you ever need to ask support to trace a specific rental, that ID is what they search on.

The Order Lifecycle

The state field in the payload progresses through a small fixed sequence:

  • PAID_BY_USER: initial state, the order is paid from your balance but no on-chain action has happened yet.
  • WAITING_DELEGATION: the service has picked up the order and is preparing the delegation transaction.
  • ENERGY_DELEGATED: the delegation transaction has landed on-chain. The destination address now holds the rented energy and can spend it on the next outbound transaction.
  • ERROR_DELEGATION: the delegation failed for some reason. Rare, but possible during heavy network congestion.
  • CANCELLED: the order was cancelled and the funds refunded to your balance.

To check the current state, call:

GET https://api.tronenergyrent.com/single-order-details?apiKey=YOUR_API_KEY&orderId=ORDER_ID

The response uses the same status / errorCode / payload envelope, with the current state inside payload. Poll this every second or two after placing the order and proceed with your TRC-20 transfer only after you see ENERGY_DELEGATED. In practice this is usually one or two polls.

A Working Python Integration

Here is a minimal but production-shaped pattern. It places the rental, polls until the delegation is on-chain, and surfaces a clear error if anything goes wrong.

import logging
import time
import requests

API_BASE = "https://api.tronenergyrent.com"
API_KEY = "your_api_key_here"
ENERGY_FOR_TRANSFER = 65_000   # use 130_000 if recipient has zero USDT balance
HTTP_TIMEOUT_SECS = 10
POLL_INTERVAL_SECS = 1
POLL_TIMEOUT_SECS = 30


def place_order(destination_address: str, period: str = "1h",
                energy_amount: int = ENERGY_FOR_TRANSFER) -> dict:
    resp = requests.get(
        f"{API_BASE}/place-energy-order",
        params={
            "apiKey": API_KEY,
            "period": period,
            "energyAmount": energy_amount,
            "destinationAddress": destination_address,
            "preActivateDestinationAddress": 0,
        },
        timeout=HTTP_TIMEOUT_SECS,
    )
    resp.raise_for_status()
    body = resp.json()
    if body.get("status") != "SUCCESS":
        raise RuntimeError(
            f"place-energy-order failed: {body.get('errorCode')} "
            f"({body.get('errorDescription')}) requestId={body.get('requestId')}"
        )
    return body["payload"]


def wait_for_delegation(order_id: str) -> dict:
    deadline = time.monotonic() + POLL_TIMEOUT_SECS
    while time.monotonic() < deadline:
        resp = requests.get(
            f"{API_BASE}/single-order-details",
            params={"apiKey": API_KEY, "orderId": order_id},
            timeout=HTTP_TIMEOUT_SECS,
        )
        resp.raise_for_status()
        body = resp.json()
        if body.get("status") != "SUCCESS":
            raise RuntimeError(
                f"single-order-details failed: {body.get('errorCode')} "
                f"({body.get('errorDescription')})"
            )
        state = body["payload"]["state"]
        if state == "ENERGY_DELEGATED":
            return body["payload"]
        if state == "ERROR_DELEGATION":
            raise RuntimeError(f"Delegation failed for order {order_id}")
        if state == "CANCELLED":
            raise RuntimeError(f"Order {order_id} was cancelled")
        time.sleep(POLL_INTERVAL_SECS)
    raise TimeoutError(f"Order {order_id} did not reach ENERGY_DELEGATED in {POLL_TIMEOUT_SECS}s")


def rent_energy(destination_address: str) -> dict:
    placed = place_order(destination_address)
    logging.info(
        "Order placed: id=%s priceSun=%s priceTrx=%s",
        placed["orderId"], placed["totalPriceSun"], placed["totalPriceTrx"],
    )
    delegated = wait_for_delegation(placed["orderId"])
    logging.info("Order delegated: id=%s", delegated["orderId"])
    return delegated

A few things worth highlighting. The explicit status == "SUCCESS" check is important because the HTTP status is always 200, so resp.raise_for_status() alone tells you nothing about the actual outcome. The polling loop has a hard timeout so a stuck order cannot block your transfer pipeline indefinitely. The branching on state handles the three terminal outcomes (ENERGY_DELEGATED, ERROR_DELEGATION, CANCELLED) explicitly instead of relying on a generic "not success".

In your transfer workflow, call rent_energy() first, then broadcast the TRC-20 transfer. The order of operations matters: broadcast first and the transaction will burn TRX from the sender because the delegation has not landed yet.

Sizing Your Prepaid Balance

The rental cost is deducted from a prepaid balance you maintain in your tronenergyrent.com account. Set up an alert or automated top-up when the balance drops below a threshold. A reasonable floor is whatever covers two hours of peak volume.

To read the current balance programmatically:

GET https://api.tronenergyrent.com/account-info?apiKey=YOUR_API_KEY

The payload includes your current balance and account state. Wire that into your monitoring stack and you will never be surprised by a depleted balance at 2am. Note that the minimum top-up to your tronenergyrent.com balance is 10 TRX.

Error Handling in Practice

Three failure modes come up most often in production. The error codes below come from the real API and you can branch on them safely.

  1. INSUFFICIENT_BALANCE: your prepaid balance is too low to cover the requested rental plus any pre-activation. Do not retry, retrying will not fix it. Catch this specifically and trigger an alert or top-up flow. Add the requestId to your alert payload.
  2. INVALID_ADDRESS: the destinationAddress failed the base58check decode on the server. If your system constructs addresses dynamically, for example from user-supplied input, validate them locally before calling the API. The tronpy Python library has is_address() for this; rejecting bad input client-side is faster than waiting for the round trip.
  3. INACTIVE_DESTINATION_ADDRESS_ERROR: the destination address has never been activated on-chain and you did not ask the service to activate it. Fix by either pre-funding the address with a small amount of TRX from elsewhere, or by passing preActivateDestinationAddress=1 on the next call so the service activates it for 1.5 TRX.

A subtler case: ORDER_IS_ALREADY_IN_PROGRESS can come back if you have multiple workers placing rentals for the same destination at the same time. The fix is process-side, not API-side. Take a distributed lock (Redis works fine) keyed on the destination address and held until the delegation completes.

Verifying On-Chain

For most integrations, polling /single-order-details until ENERGY_DELEGATED is enough. If you want belt-and-braces verification, you can also query the TRON full node directly after the delegation lands:

GET https://api.trongrid.io/v1/accounts/{destinationAddress}

The response contains the address's current resource state, including energy delegated to it from external addresses. The exact field name depends on the current Stake 2.0 view, so consult the live TRON HTTP API docs when wiring this in. As a soft check that logs a warning rather than blocking the transfer, it is a useful canary for the rare case when something goes wrong downstream of the rental API itself.

That extra check will save hours of debugging the one time the rental looks successful in the API but something else goes wrong on-chain.

Want to save on TRON transaction fees? Check energy prices now. Price Estimate
Back to Blog