Track status
Follow a relay until it is done, by polling or with a live stream.
Every execute call creates an operation and returns its operationId. The relay runs in the background. Follow it in one of two ways:
- Poll
GET /chain_relayers/portal/operations/{id}. Simple, and always works. - Stream
GET /chain_relayers/portal/stream?operationId={id}. Server-Sent Events, updates arrive in moments.
Tronergy does not send webhooks.
Statuses
An operation moves pending → reserved → executing → confirming → completed. It can stop early as failed, voided or expired.
pending: accepted, not started yet.reserved: the quoted cost is held on your balance.executing: energy and bandwidth are being supplied, and the transaction is being broadcast.confirming: broadcast, waiting for the network.completed(final): confirmed on-chain.txHashis set.failed(final): did not succeed. ReaderrorCodeanderrorMessage.voided(final): cancelled.expired(final): did not finish within 15 minutes.
Stop when you reach a final status.
Poll
curl https://apiv2.tronergy.io/api/chain_relayers/portal/operations/3d6f9c1a-0b2e-4c8d-9f17-5a6b7c8d9e0f \
-H "x-api-key: $TRONERGY_API_KEY"Every 1 to 2 seconds is a good interval. Most relays finish in seconds.
Useful fields on the operation:
txHash: the user's transaction.txHashSecondary: the fee transaction, in fee modes.estimatedFee: what your balance was charged, in TRX.errorCode,errorMessage: why it failed.progress.phase: the current step, for fee modes and swaps. Use it for display only.
Stream
One connection follows one operation. Each message is a data: line with JSON:
data: {"id":"chain_relayers.operation.status_changed","payload":{"operationId":"3d6f...","status":"confirming"},"timestamp":1761048003000}
data: {"id":"chain_relayers.operation.completed","payload":{"operationId":"3d6f...","status":"completed","txHash":"a1b2..."},"timestamp":1761048006000}const FINAL = ['completed', 'failed', 'voided', 'expired']
export async function followOperation(operationId: string) {
const controller = new AbortController()
const res = await fetch(
`https://apiv2.tronergy.io/api/chain_relayers/portal/stream?operationId=${operationId}`,
{ headers: { 'x-api-key': process.env.TRONERGY_API_KEY!, accept: 'text/event-stream' }, signal: controller.signal },
)
// Events are not replayed. Read the current state once after connecting.
const { operation } = await call(`/chain_relayers/portal/operations/${operationId}`)
if (FINAL.includes(operation.status)) {
controller.abort()
return operation
}
const reader = res.body!.pipeThrough(new TextDecoderStream()).getReader()
let buffer = ''
try {
for (;;) {
const { value, done } = await reader.read()
if (done) return null
buffer += value
const blocks = buffer.split('\n\n')
buffer = blocks.pop() ?? ''
for (const block of blocks) {
const line = block.split('\n').find((entry) => entry.startsWith('data: '))
if (!line) continue
const event = JSON.parse(line.slice(6))
console.log(event.id, event.payload.status)
if (FINAL.includes(event.payload.status)) {
controller.abort()
return event.payload
}
}
}
} catch (error) {
if (!controller.signal.aborted) throw error
return null
}
}- Close it yourself. The server does not close the stream when the operation ends.
- No replay. You only get events sent while connected. After a reconnect, read the operation again.
- Heartbeat. A
:heartbeatcomment arrives every 30 seconds. Ignore it. - Browsers. Do not put your API key in a browser. Open the stream on your backend and forward events to your frontend.
Cancel
POST /chain_relayers/portal/operations/{id}/cancel moves an operation to voided, but only while it is pending or reserved. After that it returns 409, because the transaction may already be on the network.
See operations and stream for every field.