After announcing a transaction to the NEM network, it remains unconfirmed until it is included in a block.
Monitoring status changes is essential for building responsive applications that can react to transaction confirmation
or failure.
This tutorial shows how to poll a transaction's status until it is confirmed, how to check whether it is still
waiting in the unconfirmed pool, and how to decide when it will never confirm.
This kind of monitoring typically happens right after announcing a transaction, as shown in the
Transfer XEM tutorial, to make sure it gets confirmed.
Polling is not recommended for production
This tutorial uses polling to check the transaction status for illustration purposes,
but it is not the recommended approach for production applications.
WebSockets provide a more responsive solution without the overhead of repeated
API calls.
importjsonimportosimporttimeimporturllib.errorimporturllib.requestNODE_URL=os.getenv("NODE_URL","http://libertalia.nemtest.net:7890")print(f'Using node {NODE_URL}')# Transaction hash to monitortransaction_hash=os.getenv("TRANSACTION_HASH","AE0B2142DFB75C9C126442EF612944E926BCE63FA34B353CDE409E2E87703C0B")# Signer's addresssigner_address=os.getenv("SIGNER_ADDRESS","TBONKWCOWBZYZB2I5JD3LSDBQVBYHB757VN3SKPP")# Transaction signaturetransaction_signature=os.getenv("TRANSACTION_SIGNATURE","99B1850FADDB964112D030AA0A5C9F8B5B1B6B992B407D9C70F52F089BD651DF""A7D4991639A48B810EFD98C45060D7AD9AE57FDA37F58561459DCE8D0A747F02")print(f"Monitoring transaction: {transaction_hash}")defget_confirmation_height(tx_hash):""" Query /transaction/get once to check for confirmation. Args: tx_hash: hash of the transaction to check Returns: The height of the block containing the transaction, or None if the transaction is not confirmed yet """url=f"{NODE_URL}/transaction/get?hash={tx_hash}"try:withurllib.request.urlopen(url)asresponse:confirmed=json.loads(response.read().decode())returnconfirmed["meta"]["height"]excepturllib.error.HTTPErroraserr:iferr.status!=400:raisereturnNonedefis_in_unconfirmed_pool(signature,address):""" Check whether a transaction with the given signature is in the address's unconfirmed pool. """path=f"/account/unconfirmedTransactions?address={address}"withurllib.request.urlopen(f"{NODE_URL}{path}")asresponse:pool=json.loads(response.read().decode())["data"]target=signature.lower()returnany(entry["transaction"]["signature"].lower()==targetforentryinpool)defwait_for_confirmation(tx_hash,max_attempts=120,wait_seconds=1):""" Check for confirmation repeatedly until the transaction is confirmed or the attempts run out. Args: tx_hash: hash of the transaction to monitor max_attempts: maximum polling attempts wait_seconds: seconds to wait between attempts Returns: True if the transaction was confirmed, False otherwise """print("\nWaiting for transaction confirmation")forattemptinrange(1,max_attempts+1):time.sleep(wait_seconds)height=get_confirmation_height(tx_hash)status=f"confirmed in block {height}"ifheightelse"pending"print(f" Attempt {attempt}: {status}")ifheight:returnTruereturnFalsetry:block_height=get_confirmation_height(transaction_hash)ifblock_height:print(f"\nTransaction confirmed in block {block_height}")elifnotis_in_unconfirmed_pool(transaction_signature,signer_address):print("\nTransaction not found")elifwait_for_confirmation(transaction_hash):print("\nTransaction confirmed!")else:print("\nTransaction not confirmed within the polling window")exceptExceptionaserror:print(f"\nCould not reach the node: {error}")
constNODE_URL=process.env.NODE_URL||'http://libertalia.nemtest.net:7890';console.log('Using node',NODE_URL);// Transaction hash to monitor.consttransactionHash=process.env.TRANSACTION_HASH||'AE0B2142DFB75C9C126442EF612944E926BCE63FA34B353CDE409E2E87703C0B';// Signer's address.constsignerAddress=process.env.SIGNER_ADDRESS||'TBONKWCOWBZYZB2I5JD3LSDBQVBYHB757VN3SKPP';// Transaction signature.consttransactionSignature=process.env.TRANSACTION_SIGNATURE||'99B1850FADDB964112D030AA0A5C9F8B5B1B6B992B407D9C70F52F089BD651DF'+'A7D4991639A48B810EFD98C45060D7AD9AE57FDA37F58561459DCE8D0A747F02';console.log(`Monitoring transaction: ${transactionHash}`);/** * Query /transaction/get once to check for confirmation. * @param {string} txHash - hash of the transaction to check * @returns {number|null} height of the block containing the * transaction, or null if it is not confirmed yet */asyncfunctiongetConfirmationHeight(txHash){consturl=`${NODE_URL}/transaction/get?hash=${txHash}`;constresponse=awaitfetch(url);if(response.ok){constconfirmed=awaitresponse.json();returnconfirmed.meta.height;}if(400!==response.status)thrownewError(`Unexpected status: ${response.status}`);returnnull;}/** * Check whether a transaction with the given signature is in the * address's unconfirmed pool. * @param {string} signature - hex signature of the monitored transaction * @param {string} address - signer's address * @returns {boolean} true if the signature is in the signer's pool */asyncfunctionisInUnconfirmedPool(signature,address){constpath=`/account/unconfirmedTransactions?address=${address}`;constresponse=awaitfetch(`${NODE_URL}${path}`);constpool=(awaitresponse.json()).data;consttarget=signature.toLowerCase();returnpool.some(entry=>entry.transaction.signature.toLowerCase()===target);}/** * Check for confirmation repeatedly until the transaction is * confirmed or the attempts run out. * @param {string} txHash - hash of the transaction to monitor * @param {number} maxAttempts - maximum polling attempts * @param {number} waitSeconds - seconds to wait between attempts * @returns {boolean} true if the transaction was confirmed */asyncfunctionwaitForConfirmation(txHash,maxAttempts=120,waitSeconds=1){console.log('\nWaiting for transaction confirmation');for(letattempt=1;attempt<=maxAttempts;attempt++){awaitnewPromise(resolve=>{setTimeout(resolve,waitSeconds*1000);});constheight=awaitgetConfirmationHeight(txHash);conststatus=height?`confirmed in block ${height}`:'pending';console.log(` Attempt ${attempt}: ${status}`);if(height)returntrue;}returnfalse;}try{constblockHeight=awaitgetConfirmationHeight(transactionHash);if(blockHeight)console.log(`\nTransaction confirmed in block ${blockHeight}`);elseif(!(awaitisInUnconfirmedPool(transactionSignature,signerAddress)))console.log('\nTransaction not found');elseif(awaitwaitForConfirmation(transactionHash))console.log('\nTransaction confirmed!');elseconsole.log('\nConfirmation timed out');}catch(error){console.log(`\nCould not reach the node: ${error.message}`);}
//JAVA 21+//DEPS org.symbol:symbol-sdk:3.3.3importjava.io.IOException;importjava.net.URI;importjava.net.http.HttpClient;importjava.net.http.HttpRequest;importjava.net.http.HttpResponse;importjava.net.http.HttpResponse.BodyHandlers;importcom.fasterxml.jackson.databind.JsonNode;importcom.fasterxml.jackson.databind.ObjectMapper;publicfinalclassMonitoringStatus{privatestaticfinalObjectMapperJSON_MAPPER=newObjectMapper();privatestaticfinalHttpClientHTTP_CLIENT=HttpClient.newHttpClient();privatestaticfinalStringNODE_URL=System.getenv().getOrDefault("NODE_URL","http://libertalia.nemtest.net:7890");// Query /transaction/get once to check for confirmation.privatestaticLonggetConfirmationHeight(finalStringtransactionHash)throwsIOException,InterruptedException{finalStringpath=String.format("/transaction/get?hash=%s",transactionHash);finalHttpRequestrequest=HttpRequest.newBuilder(URI.create(NODE_URL+path)).GET().build();finalHttpResponse<String>response=HTTP_CLIENT.send(request,BodyHandlers.ofString());if(2==response.statusCode()/100)returnJSON_MAPPER.readTree(response.body()).get("meta").get("height").asLong();if(400!=response.statusCode())thrownewIOException("Unexpected status: "+response.statusCode());returnnull;}// Check whether a transaction is in the address's unconfirmed pool.privatestaticbooleanisInUnconfirmedPool(finalStringsignature,finalStringaddress)throwsIOException,InterruptedException{finalStringpath=String.format("/account/unconfirmedTransactions?address=%s",address);finalHttpRequestrequest=HttpRequest.newBuilder(URI.create(NODE_URL+path)).GET().build();finalHttpResponse<String>response=HTTP_CLIENT.send(request,BodyHandlers.ofString());finalJsonNodepool=JSON_MAPPER.readTree(response.body()).get("data");for(finalJsonNodeentry:pool){finalStringcandidate=entry.get("transaction").get("signature").asText();if(signature.equalsIgnoreCase(candidate))returntrue;}returnfalse;}// Check repeatedly until the transaction is confirmed.privatestaticbooleanwaitForConfirmation(finalStringtransactionHash,finalintmaxAttempts,finalintwaitSeconds)throwsIOException,InterruptedException{System.out.println("\nWaiting for transaction confirmation");for(intattempt=1;maxAttempts>=attempt;++attempt){Thread.sleep(waitSeconds*1000L);finalLongheight=getConfirmationHeight(transactionHash);finalStringstatus=null==height?"pending":"confirmed in block "+height;System.out.printf(" Attempt %d: %s%n",attempt,status);if(null!=height)returntrue;}returnfalse;}publicstaticvoidmain(finalString[]args){try{newMonitoringStatus().run();}catch(finalExceptionex){System.out.println(null==ex.getMessage()?ex.toString():ex.getMessage());}}privatevoidrun()throwsIOException,InterruptedException{System.out.printf("Using node %s%n",NODE_URL);// Transaction hash to monitor.finalStringtransactionHash=System.getenv().getOrDefault("TRANSACTION_HASH","AE0B2142DFB75C9C126442EF612944E926BCE63FA34B353CDE409E2E87703"+"C0B");// Signer's address.finalStringsignerAddress=System.getenv().getOrDefault("SIGNER_ADDRESS","TBONKWCOWBZYZB2I5JD3LSDBQVBYHB757VN3SKPP");// Transaction signature.finalStringtransactionSignature=System.getenv().getOrDefault("TRANSACTION_SIGNATURE","99B1850FADDB964112D030AA0A5C9F8B5B1B6B992B407D9C70"+"F52F089BD651DFA7D4991639A48B810EFD98C45060D7AD9AE5"+"7FDA37F58561459DCE8D0A747F02");System.out.printf("Monitoring transaction: %s%n",transactionHash);finalLongblockHeight=getConfirmationHeight(transactionHash);if(null!=blockHeight)System.out.printf("%nTransaction confirmed in block %d%n",blockHeight);elseif(!isInUnconfirmedPool(transactionSignature,signerAddress))System.out.println("\nTransaction not found");elseif(waitForConfirmation(transactionHash,120,1))System.out.println("\nTransaction confirmed!");elseSystem.out.println("\nConfirmation timed out");}}
// Transaction hash to monitor.finalStringtransactionHash=System.getenv().getOrDefault("TRANSACTION_HASH","AE0B2142DFB75C9C126442EF612944E926BCE63FA34B353CDE409E2E87703"+"C0B");// Signer's address.finalStringsignerAddress=System.getenv().getOrDefault("SIGNER_ADDRESS","TBONKWCOWBZYZB2I5JD3LSDBQVBYHB757VN3SKPP");// Transaction signature.finalStringtransactionSignature=System.getenv().getOrDefault("TRANSACTION_SIGNATURE","99B1850FADDB964112D030AA0A5C9F8B5B1B6B992B407D9C70"+"F52F089BD651DFA7D4991639A48B810EFD98C45060D7AD9AE5"+"7FDA37F58561459DCE8D0A747F02");
To monitor a transaction, you need its hash, which is generated after signing.
The hash uniquely identifies the transaction on the NEM network.
The snippet also reads a signer's address and the transaction signature.
Neither is needed to detect confirmation, but both are used to check whether a transaction is still waiting in the
unconfirmed pool.
The snippet uses sample values.
Set TRANSACTION_HASH, SIGNER_ADDRESS, and TRANSACTION_SIGNATURE environment variables to override them.
All three values are produced when signing a transaction, as shown in the Transfer XEM tutorial.
defget_confirmation_height(tx_hash):""" Query /transaction/get once to check for confirmation. Args: tx_hash: hash of the transaction to check Returns: The height of the block containing the transaction, or None if the transaction is not confirmed yet """url=f"{NODE_URL}/transaction/get?hash={tx_hash}"try:withurllib.request.urlopen(url)asresponse:confirmed=json.loads(response.read().decode())returnconfirmed["meta"]["height"]excepturllib.error.HTTPErroraserr:iferr.status!=400:raisereturnNone
/** * Query /transaction/get once to check for confirmation. * @param {string} txHash - hash of the transaction to check * @returns {number|null} height of the block containing the * transaction, or null if it is not confirmed yet */asyncfunctiongetConfirmationHeight(txHash){consturl=`${NODE_URL}/transaction/get?hash=${txHash}`;constresponse=awaitfetch(url);if(response.ok){constconfirmed=awaitresponse.json();returnconfirmed.meta.height;}if(400!==response.status)thrownewError(`Unexpected status: ${response.status}`);returnnull;}
// Query /transaction/get once to check for confirmation.privatestaticLonggetConfirmationHeight(finalStringtransactionHash)throwsIOException,InterruptedException{finalStringpath=String.format("/transaction/get?hash=%s",transactionHash);finalHttpRequestrequest=HttpRequest.newBuilder(URI.create(NODE_URL+path)).GET().build();finalHttpResponse<String>response=HTTP_CLIENT.send(request,BodyHandlers.ofString());if(2==response.statusCode()/100)returnJSON_MAPPER.readTree(response.body()).get("meta").get("height").asLong();if(400!=response.statusCode())thrownewIOException("Unexpected status: "+response.statusCode());returnnull;}
The function checks whether the transaction is confirmed by querying
/transaction/getGET with the transaction hash.
When a transaction has been included in a block, this endpoint returns its contents together with the block height
in meta.height, which the function returns.
Otherwise, the endpoint responds with HTTP 400 ("Hash was not found in cache") and the function returns no height,
meaning the transaction is not confirmed.
A transaction that is not confirmed may still be waiting in the unconfirmed pool, which the next function inspects.
Hash lookup is short-lived
/transaction/getGET reads from a cache with a default retention of 36 hours.
The lookup is enabled by default, but node operators can disable it or change the retention.
Querying for a transaction hash older than the retention period returns an HTTP 400 error, even if the
transaction is in fact confirmed.
Therefore, when announcing a transaction that might need to be looked up later than this retention period,
store its confirmation block height along with its hash.
In this way, the transaction can be directly retrieved from the block via the /block/at/publicPOST endpoint.
Otherwise, the transaction needs to be located by paging through the signer's full history with
/account/transfers/allGET, or by searching the blockchain block by block.
defis_in_unconfirmed_pool(signature,address):""" Check whether a transaction with the given signature is in the address's unconfirmed pool. """path=f"/account/unconfirmedTransactions?address={address}"withurllib.request.urlopen(f"{NODE_URL}{path}")asresponse:pool=json.loads(response.read().decode())["data"]target=signature.lower()returnany(entry["transaction"]["signature"].lower()==targetforentryinpool)
/** * Check whether a transaction with the given signature is in the * address's unconfirmed pool. * @param {string} signature - hex signature of the monitored transaction * @param {string} address - signer's address * @returns {boolean} true if the signature is in the signer's pool */asyncfunctionisInUnconfirmedPool(signature,address){constpath=`/account/unconfirmedTransactions?address=${address}`;constresponse=awaitfetch(`${NODE_URL}${path}`);constpool=(awaitresponse.json()).data;consttarget=signature.toLowerCase();returnpool.some(entry=>entry.transaction.signature.toLowerCase()===target);}
// Check whether a transaction is in the address's unconfirmed pool.privatestaticbooleanisInUnconfirmedPool(finalStringsignature,finalStringaddress)throwsIOException,InterruptedException{finalStringpath=String.format("/account/unconfirmedTransactions?address=%s",address);finalHttpRequestrequest=HttpRequest.newBuilder(URI.create(NODE_URL+path)).GET().build();finalHttpResponse<String>response=HTTP_CLIENT.send(request,BodyHandlers.ofString());finalJsonNodepool=JSON_MAPPER.readTree(response.body()).get("data");for(finalJsonNodeentry:pool){finalStringcandidate=entry.get("transaction").get("signature").asText();if(signature.equalsIgnoreCase(candidate))returntrue;}returnfalse;}
queries /account/unconfirmedTransactionsGET for the signer's address and
reports whether the monitored transaction is among the pending list.
Invalid transactions never enter the pool
A transaction that fails validation does not reach the unconfirmed
pool: the receiving node rejects it immediately when announced, as shown in the
Transfer XEM tutorial.
The above endpoint response omits each entry's hash, so the function matches by signature instead.
A transaction's signature is unique and appears under transaction.signature in every pool entry.
For multisig transactions, this is the signature of the announced wrapper, not of the inner transaction.
The function returns:
: the transaction is still in the unconfirmed pool, waiting to be included in a block.
: the transaction is not in the response.
Possible causes include: it has not arrived at this node yet, it has already been confirmed,
it was dropped from the pool, or it was left out of the response.
The response is limited to 25 transactions
The endpoint returns at most the 25 most recent transactions involving the address.
Incoming transactions count toward this limit too, so on a busy account the monitored transaction can be missing
from the response while it is still in the unconfirmed pool.
defwait_for_confirmation(tx_hash,max_attempts=120,wait_seconds=1):""" Check for confirmation repeatedly until the transaction is confirmed or the attempts run out. Args: tx_hash: hash of the transaction to monitor max_attempts: maximum polling attempts wait_seconds: seconds to wait between attempts Returns: True if the transaction was confirmed, False otherwise """print("\nWaiting for transaction confirmation")forattemptinrange(1,max_attempts+1):time.sleep(wait_seconds)height=get_confirmation_height(tx_hash)status=f"confirmed in block {height}"ifheightelse"pending"print(f" Attempt {attempt}: {status}")ifheight:returnTruereturnFalse
/** * Check for confirmation repeatedly until the transaction is * confirmed or the attempts run out. * @param {string} txHash - hash of the transaction to monitor * @param {number} maxAttempts - maximum polling attempts * @param {number} waitSeconds - seconds to wait between attempts * @returns {boolean} true if the transaction was confirmed */asyncfunctionwaitForConfirmation(txHash,maxAttempts=120,waitSeconds=1){console.log('\nWaiting for transaction confirmation');for(letattempt=1;attempt<=maxAttempts;attempt++){awaitnewPromise(resolve=>{setTimeout(resolve,waitSeconds*1000);});constheight=awaitgetConfirmationHeight(txHash);conststatus=height?`confirmed in block ${height}`:'pending';console.log(` Attempt ${attempt}: ${status}`);if(height)returntrue;}returnfalse;}
// Check repeatedly until the transaction is confirmed.privatestaticbooleanwaitForConfirmation(finalStringtransactionHash,finalintmaxAttempts,finalintwaitSeconds)throwsIOException,InterruptedException{System.out.println("\nWaiting for transaction confirmation");for(intattempt=1;maxAttempts>=attempt;++attempt){Thread.sleep(waitSeconds*1000L);finalLongheight=getConfirmationHeight(transactionHash);finalStringstatus=null==height?"pending":"confirmed in block "+height;System.out.printf(" Attempt %d: %s%n",attempt,status);if(null!=height)returntrue;}returnfalse;}
The function calls every
second until the transaction is confirmed, or two minutes ellapse (configurable timeout).
The function returns as soon as a check reports a confirmation.
When the attempts run out, it returns instead.
This means the transaction was not confirmed within the polling window, not that it failed, but this is a rare case.
try:block_height=get_confirmation_height(transaction_hash)ifblock_height:print(f"\nTransaction confirmed in block {block_height}")elifnotis_in_unconfirmed_pool(transaction_signature,signer_address):print("\nTransaction not found")elifwait_for_confirmation(transaction_hash):print("\nTransaction confirmed!")else:print("\nTransaction not confirmed within the polling window")exceptExceptionaserror:print(f"\nCould not reach the node: {error}")
try{constblockHeight=awaitgetConfirmationHeight(transactionHash);if(blockHeight)console.log(`\nTransaction confirmed in block ${blockHeight}`);elseif(!(awaitisInUnconfirmedPool(transactionSignature,signerAddress)))console.log('\nTransaction not found');elseif(awaitwaitForConfirmation(transactionHash))console.log('\nTransaction confirmed!');elseconsole.log('\nConfirmation timed out');}catch(error){console.log(`\nCould not reach the node: ${error.message}`);}
finalLongblockHeight=getConfirmationHeight(transactionHash);if(null!=blockHeight)System.out.printf("%nTransaction confirmed in block %d%n",blockHeight);elseif(!isInUnconfirmedPool(transactionSignature,signerAddress))System.out.println("\nTransaction not found");elseif(waitForConfirmation(transactionHash,120,1))System.out.println("\nTransaction confirmed!");elseSystem.out.println("\nConfirmation timed out");
The snippet starts with a call to to check if the transaction is already
confirmed.
Confirmed transactions can still be reversed
A confirmed transaction has been included in a block but is not yet irreversible.
Until enough subsequent blocks are added to surpass the rewrite limit, rollbacks are still possible.
If the transaction is not already part of a block, looks for it in the
unconfirmed pool.
A transaction that is neither confirmed nor in this pool is reported as not found.
Only when the transaction is waiting in the unconfirmed pool does the snippet call
to poll until the transaction is confirmed or the polling window ends.
Only rejection or a passed deadline means failure
There are only two ways to know a transaction will never confirm: it was rejected when announced, or its
deadline has passed.
A transaction disappearing from one node's unconfirmed pool does not mean it has failed.
Each node maintains its own pool, and another peer may still hold and eventually confirm the transaction.
For example, a node may restart with an empty pool or remove older transactions while managing pool capacity.
Because announcement rejections are returned immediately, the deadline provides the final verdict.
Once network time passes the deadline, the transaction can no longer be included in a block and it is safe
to announce a replacement transaction.
Transaction hash (line 2): The hash of the transaction to monitor, which uniquely identifies it on the network.
Polling start (line 4): Polling starts because the transaction was not already present in the blockchain and was
found waiting in the unconfirmed pool.
Polling attempts (lines 5-9): Each attempt reports pending while the transaction waits to be included in a
block.
Confirmation (line 10): A polling attempt finally reports the inclusion in block 652601.
Final outcome (line 12): The transaction is confirmed and monitoring ends.
The number of attempts and timing vary depending on network conditions and block production rate.
To see the transaction from the network's perspective, visit the NEM Testnet Explorer and
search for the transaction hash.
For production applications, consider these improvements:
Wait past the rewrite limit. A confirmed transaction can still be rolled back until enough subsequent
blocks have been added.
See the rewrite limit for the practical threshold.
Query multiple nodes. Check status across several nodes for greater reliability and protection against
single-node issues.
Use WebSockets: Replace polling with WebSocket subscriptions for real-time updates without repeated API calls.
See the Listening to Transaction Flow WebSocket tutorial.