Sign and broadcast — the API workflow
Every state-changing operation on TRON follows the same three-step workflow: construct an unsigned transaction, sign it client-side with the sender's private key, and broadcast the signed transaction. This page explains the lifecycle, gives a worked example, and lists common pitfalls.
Every TRON operation that changes state — a TRX transfer, a contract call, staking, voting, deploying a contract — follows the same three-step workflow:
- Construct the unsigned transaction by calling a node API
- Sign the transaction client-side with the sender's private key
- Broadcast the signed transaction back to a node
Read-only operations (querying balances, reading contract state, fetching blocks) skip steps 2 and 3 entirely — they are single requests returning chain state. This page is about the write path.
Prerequisites
The three-step workflow
┌───────────────────┐ ┌──────────────────┐ ┌────────────────────┐
│ 1. Construct │ │ 2. Sign │ │ 3. Broadcast │
│ POST a builder │ ──> │ Client-side: │ ──> │ POST signed tx to │
│ endpoint to get │ │ SHA-256(raw_data)│ │ /wallet/broadcast- │
│ unsigned raw_data │ │ + secp256k1 sign │ │ transaction │
└───────────────────┘ └──────────────────┘ └────────────────────┘
↓ unsigned tx ↓ signed tx ↓ txidThe signing step happens on the client side. Your private key never leaves your machine — neither the node nor TronGrid ever sees it.
Step 1 — Construct the unsigned transaction
The TRON node exposes one builder endpoint per contract type. The client sends a JSON request describing the operation; the node returns a fully-formed but unsigned transaction object.
| Operation | Builder endpoint |
|---|---|
| Transfer TRX | /wallet/createtransaction |
| Transfer TRC-10 | /wallet/transferasset |
| Call a smart contract (including TRC-20 transfer) | /wallet/triggersmartcontract |
| Stake TRX (Stake 2.0) | /wallet/freezebalancev2 |
| Vote for Super Representatives | /wallet/votewitnessaccount |
| Deploy a smart contract | /wallet/deploycontract |
The returned JSON has three top-level fields you need to understand:
txID— the SHA-256 hash ofraw_data; this is what gets signedraw_data— the structured transaction body (contract type, parameters, TAPOS reference, expiration)raw_data_hex— protobuf serialization ofraw_data, ready for hashing
The node also populates two independent timing fields:
- TAPOS reference (
ref_block_bytes,ref_block_hash) — pins the transaction to a recent block, preventing replay across forks. If that block falls off the canonical chain (microfork / reorg), the TAPOS check fails on inclusion. expiration— a separate deadline field. Default is ~60 seconds, set at construction time. If the node built the transaction (via/wallet/createtransactionand similar endpoints), that node'strx.expiration.timeInMillisecondsinconfig.confchooses the default; if you built it client-side with an SDK (TronWeb, Trident, GoTron), the SDK sets the value (most SDKs let you override per transaction). The network rejects the transaction as expired if not included by that deadline; the upper bound is 24 hours from the head block.
Construct, sign, and broadcast within ~50 seconds — otherwise the transaction is rejected as expired.
Step 2 — Sign the transaction
Signing happens in three sub-steps client-side:
- Compute
SHA-256(raw_data)— the result equalstxID - Sign that hash with the sender's secp256k1 private key
- Append the 65-byte signature to
transaction.signature[]
All SDKs wrap these three sub-steps behind a single call:
| SDK | Signing call |
|---|---|
| TronWeb (JavaScript) | tronWeb.trx.sign(unsignedTx, privateKey) |
| Trident (Java) | wrapper.signTransaction(unsignedTx) |
For applications that cannot use an SDK — air-gapped signers, HSMs, hardware wallets — compute the hash yourself from raw_data_hex and sign with a secp256k1 library. The signing primitives are documented at the protocol level; see Transactions.
Multi-signature transactions follow the same flow with one difference: each cosigner produces a signature against the same txID, and all signatures are collected into signature[] before broadcasting. See Account permission management.
Step 3 — Broadcast the signed transaction
POST the fully-signed transaction back to /wallet/broadcasttransaction:
POST /wallet/broadcasttransaction
Content-Type: application/json
{
"txID": "...",
"raw_data": { ... },
"raw_data_hex": "...",
"signature": ["..."]
}A successful broadcast returns:
{ "result": true, "txid": "f947f1283f7b43c111eba662..." }The transaction is now in the mempool. Block production picks it up within one block interval (3 seconds on average). To confirm execution, poll /walletsolidity/gettransactioninfobyid until the transaction appears at a solidified height.
Broadcast does not mean executed. A successful broadcast means the transaction was accepted into the mempool. It can still fail at execution time — for example, a smart-contract call might revert, or insufficient Bandwidth/Energy might cause execution failure. If your application needs final state, query the receipt from a solidified block instead of treating the broadcast response as final. See Broadcast and RPC errors.
Step 4 — Confirm the transaction result
After broadcast, a transaction still moves through acceptance, block inclusion, execution, and solidification. Different APIs return data from different stages and should not be used interchangeably.
| Stage | Recommended API | Return semantics | Next step |
|---|---|---|---|
| Construct unsigned transaction | CreateTransaction, TriggerSmartContract, and other builder APIs | Returns raw_data, raw_data_hex, and txID. The transaction is not signed and has not entered the network. | Sign locally |
| Check signature weight | GetSignWeight | Shows whether current signatures reach the target permission threshold. | Broadcast after the threshold is reached |
| Broadcast signed transaction | BroadcastTransaction, BroadcastHex | result: true means the broadcast node accepted the transaction into its local mempool. | Continue querying transaction status |
| Query transaction body | GetTransactionById | Returns original transaction data, including raw_data, signatures, and contract-call parameters. It does not include the execution receipt. | Query the receipt when execution result matters |
| Query execution receipt | GetTransactionInfoById | Returns fee, Energy usage, contract execution result, events, and internal transactions. | Decide whether execution succeeded or failed |
| Query solidified result | GetTransactionInfoById (SolidityNode) | Returns receipts only from solidified blocks. | Use this when final state is required |
If you need a final result, use the SolidityNode receipt as the confirmation source. If a receipt is not found immediately, do not mark the transaction as failed right away. Wait for the transaction expiration window and combine follow-up queries with block scanning to determine whether it was included.
For ordinary TRX and TRC-10 top-level transfers, the transaction body usually contains the recipient and amount. For TriggerSmartContract, read the receipt to determine the actual execution result. TRC-20 transfers should be detected from the Transfer(address,address,uint256) event in the receipt logs. Contract-derived TRX or TRC-10 transfers should be detected from internal_transactions[].
Wait for solidification and check the execution result
The following Node.js 18+ script queries the same txID until SolidityNode returns a solidified receipt. Ordinary non-contract transactions may omit receipt.result; smart-contract transactions should return SUCCESS. The script also checks whether the top-level result is FAILED. If the retry limit is reached, keep querying the original txID while investigating instead of creating a replacement transaction that could duplicate a payment.
const solidityNode = (process.env.SOLIDITY_NODE_URL || 'https://api.trongrid.io')
.replace(/\/$/, '');
const txID = process.env.TX_ID;
if (!/^[0-9a-fA-F]{64}$/.test(txID || '')) {
throw new Error('Set TX_ID to a 64-character hexadecimal transaction ID');
}
async function queryReceipt() {
const response = await fetch(`${solidityNode}/walletsolidity/gettransactioninfobyid`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ value: txID }),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
for (let attempt = 1; attempt <= 30; attempt += 1) {
const info = await queryReceipt();
if (info.id?.toLowerCase() === txID.toLowerCase()) {
const executionResult = info.receipt?.result;
if (info.result === 'FAILED' || (executionResult && executionResult !== 'SUCCESS')) {
throw new Error(`Transaction execution failed: ${executionResult || info.result}`);
}
console.log(`Transaction solidified in block ${info.blockNumber}`);
process.exit(0);
}
await new Promise((resolve) => setTimeout(resolve, 3000));
}
throw new Error('No solidified receipt found yet; continue querying the same TX_ID');The script uses the TronGrid Mainnet endpoint by default. Set SOLIDITY_NODE_URL to query a self-hosted SolidityNode. Save the script as confirm-transaction.mjs, then run:
export TX_ID="replace-with-the-txid-from-the-broadcast-response"
# Optional: export SOLIDITY_NODE_URL="https://your-solidity-node"
node confirm-transaction.mjsWorked example: stake TRX
The full lifecycle for a Stake 2.0 deposit:
1. Construct
POST /wallet/freezebalancev2
Content-Type: application/json
{
"owner_address": "41e552f6487585c2b58bc2c9bb4492bc1f17132cd0",
"frozen_balance": 1000000,
"resource": "ENERGY"
}Response:
{
"visible": false,
"txID": "f947f1283f7b43c111eba662ab5d27b31dc906b77fdb0b3e52ee626335b21108",
"raw_data": {
"contract": [{
"parameter": {
"value": {
"resource": "ENERGY",
"frozen_balance": 1000000,
"owner_address": "41e552f6487585c2b58bc2c9bb4492bc1f17132cd0"
},
"type_url": "type.googleapis.com/protocol.FreezeBalanceV2Contract"
},
"type": "FreezeBalanceV2Contract"
}],
"ref_block_bytes": "a59c",
"ref_block_hash": "aef359052c4aa176",
"expiration": 1681117668000,
"timestamp": 1681117610802
},
"raw_data_hex": "0a02a59c2208aef359052c4aa17640a0c5afd3f6305a59083612550a34..."
}2. Sign
const signedTx = await tronWeb.trx.sign(unsignedTx, privateKey);
// signedTx now has signature[0] populated3. Broadcast
POST /wallet/broadcasttransaction
Content-Type: application/json
{ ...signedTx }Response:
{ "result": true, "txid": "f947f1283f7b43c111eba662ab5d27b31dc906b77fdb0b3e52ee626335b21108" }The 1 TRX stake (1,000,000 sun) is now in the mempool. Once included in a block and solidified, the staked amount and resulting Energy quota are visible via /wallet/getaccountresource.
Common pitfalls
- TAPOS reference invalidation —
ref_block_bytesandref_block_hashmust point to a block that remains on the canonical chain. The primary failure mode is a chain reorganization (microfork) that drops the referenced block, causingTAPOS_ERRORrejection on inclusion. Fix: where possible, reference a recently solidified block rather than the chain tip. - Transaction expiry — separately from TAPOS, the
expirationfield (default ~60 seconds) caps how long the transaction can sit before inclusion. The default comes from the constructor: node-built transactions take it from the node'strx.expiration.timeInMilliseconds(config.conf), client-built transactions take it from the SDK (most SDKs accept a per-call override). Long delays between construct and broadcast cause expired-transaction rejection. Fix: construct as late as possible, sign quickly, broadcast immediately. visibleflag mismatch — whenvisible=true, addresses in the request and response are base58 (T...); whenfalse, they are hex (41...). Inconsistent use across construct and sign causes signature verification failure. See Encoding.- Signing the wrong hash — the signing hash is
SHA-256(raw_data), not Keccak-256, and notraw_data_hex's hash directly (although both produce the same bytes). Use the SDK signing call rather than rolling your own. - Reusing
txIDafter expiry — once a transaction expires, the sametxIDis dead; reconstruct from scratch rather than re-signing the old raw_data. - Treating broadcast result as success —
{"result": true}only confirms mempool acceptance. Check/walletsolidity/gettransactioninfobyidfor execution outcome. Smart-contract reverts and Energy shortfalls report success on broadcast butFAILEDon execution. - Double-broadcasting — a duplicate broadcast of the same signed transaction returns the same
txidbut is harmless; the network deduplicates. Mid-flight retries are safe. - Server-side signing of user keys — never sign with a user's private key on your server. Either keep keys client-side, use a wallet (TronLink, Adapter), or use an HSM with strict access controls.
Related resources
- Confirmation semantics — distinguish broadcast success, block inclusion, receipt execution result, and solidified state
- Encoding — base58 vs hex, the
visibleflag, ABI encoding - Broadcast and RPC errors — troubleshoot
TAPOS_ERROR,TRANSACTION_EXPIRATION_ERROR, resource shortage, and broadcast failure - Accounts — address format and key derivation
- Transactions — full transaction structure and lifecycle
- Account permission management — multi-sig signing flow
Updated 8 days ago