Symbol provides WebSocket channels that send real-time notifications as a transaction moves through the confirmation
process for a specific account.
Compared to polling the /transactionStatus/{hash}GET endpoint, WebSockets push updates as they happen without the
overhead of repeated API calls.
This tutorial shows how to subscribe to transaction channels, announce a minimal
Transfer Transaction, and wait for its confirmation using WebSockets.
importasyncioimportjsonimportosimporturllib.requestfromsymbolchain.CryptoTypesimportPrivateKeyfromsymbolchain.facade.SymbolFacadeimportSymbolFacadefromsymbolchain.symbol.NetworkimportNetworkTimestampfromsymbolchain.scimportAmountfromwebsocketsimportconnectNODE_URL=os.getenv('NODE_URL','https://reference.symboltest.net:3001')WS_URL=NODE_URL.replace('http','ws',1)+'/ws'print(f'Using node {NODE_URL}')MONITOR_ADDRESS=os.getenv('MONITOR_ADDRESS','TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I')print(f'Monitoring address: {MONITOR_ADDRESS}')SIGNER_PRIVATE_KEY=os.getenv('SIGNER_PRIVATE_KEY','0000000000000000000000000000000000000000000000000000000000000000')facade=SymbolFacade('testnet')signer_key_pair=SymbolFacade.KeyPair(PrivateKey(SIGNER_PRIVATE_KEY))asyncdefmain():asyncwithconnect(WS_URL)aswebsocket:# Connect to WebSocketresponse=json.loads(awaitwebsocket.recv())uid=response['uid']print(f'Connected to {WS_URL} with uid {uid}')# Subscribe to transaction channelschannels=[f'unconfirmedAdded/{MONITOR_ADDRESS}',f'unconfirmedRemoved/{MONITOR_ADDRESS}',f'confirmedAdded/{MONITOR_ADDRESS}',]forchannelinchannels:awaitwebsocket.send(json.dumps({'uid':uid,'subscribe':channel}))name=channel.split('/')[0]print(f'Subscribed to {name} channel')# Build and announce a transfer transactionwithurllib.request.urlopen(f'{NODE_URL}/node/time')asresp:time_json=json.loads(resp.read().decode())timestamp=NetworkTimestamp(int(time_json['communicationTimestamps']['receiveTimestamp']))withurllib.request.urlopen(f'{NODE_URL}/network/fees/transaction')asresp:fee_json=json.loads(resp.read().decode())fee_mult=max(fee_json['medianFeeMultiplier'],fee_json['minFeeMultiplier'])transaction=facade.transaction_factory.create({'type':'transfer_transaction_v1','signer_public_key':signer_key_pair.public_key,'deadline':timestamp.add_hours(2).timestamp,'recipient_address':MONITOR_ADDRESS,})transaction.fee=Amount(fee_mult*transaction.size)signature=facade.sign_transaction(signer_key_pair,transaction)json_payload=facade.transaction_factory.attach_signature(transaction,signature)transaction_hash=str(facade.hash_transaction(transaction))announce_request=urllib.request.Request(f'{NODE_URL}/transactions',data=json_payload.encode(),headers={'Content-Type':'application/json'},method='PUT')withurllib.request.urlopen(announce_request)asresp:resp.read()print(f'Announced transaction {transaction_hash[:16]}...')# Wait for confirmation via WebSocketasyncforraw_messageinwebsocket:message=json.loads(raw_message)topic=message['topic']message_hash=message['data']['meta']['hash']name=topic.split('/')[0]print(f'{name}: hash={message_hash[:16]}...')if(name=='confirmedAdded'andmessage_hash==transaction_hash):print(f'Transaction {transaction_hash[:16]}... confirmed')break# Unsubscribe before closingforchannelinchannels:awaitwebsocket.send(json.dumps({'uid':uid,'unsubscribe':channel}))print('Unsubscribed from all channels')try:asyncio.run(main())exceptExceptionaserror:print(error)
import{PrivateKey}from'symbol-sdk';import{SymbolFacade,NetworkTimestamp,models}from'symbol-sdk/symbol';constNODE_URL=process.env.NODE_URL||'https://reference.symboltest.net:3001';constWS_URL=NODE_URL.replace('http','ws')+'/ws';console.log(`Using node ${NODE_URL}`);constMONITOR_ADDRESS=process.env.MONITOR_ADDRESS||'TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I';console.log(`Monitoring address: ${MONITOR_ADDRESS}`);constSIGNER_PRIVATE_KEY=process.env.SIGNER_PRIVATE_KEY||'0000000000000000000000000000000000000000000000000000000000000000';constfacade=newSymbolFacade('testnet');constsignerKeyPair=newSymbolFacade.KeyPair(newPrivateKey(SIGNER_PRIVATE_KEY));try{// Connect to WebSocketconstwebsocket=newWebSocket(WS_URL);constuid=awaitnewPromise((resolve)=>{websocket.addEventListener('message',(event)=>{constmessage=JSON.parse(event.data);resolve(message.uid);},{once:true});});console.log(`Connected to ${WS_URL} with uid ${uid}`);// Subscribe to transaction channelsconstchannels=[`unconfirmedAdded/${MONITOR_ADDRESS}`,`unconfirmedRemoved/${MONITOR_ADDRESS}`,`confirmedAdded/${MONITOR_ADDRESS}`,];for(constchannelofchannels){websocket.send(JSON.stringify({uid,subscribe:channel}));constname=channel.split('/')[0];console.log(`Subscribed to ${name} channel`);}// Build and announce a transfer transactionconsttimeResponse=awaitfetch(`${NODE_URL}/node/time`);consttimeJSON=awaittimeResponse.json();consttimestamp=newNetworkTimestamp(timeJSON.communicationTimestamps.receiveTimestamp);constfeeResponse=awaitfetch(`${NODE_URL}/network/fees/transaction`);constfeeJSON=awaitfeeResponse.json();constfeeMult=Math.max(feeJSON.medianFeeMultiplier,feeJSON.minFeeMultiplier);consttransaction=facade.transactionFactory.create({type:'transfer_transaction_v1',signerPublicKey:signerKeyPair.publicKey.toString(),deadline:timestamp.addHours(2).timestamp,recipientAddress:MONITOR_ADDRESS,});transaction.fee=newmodels.Amount(feeMult*transaction.size);constsignature=facade.signTransaction(signerKeyPair,transaction);constjsonPayload=facade.transactionFactory.static.attachSignature(transaction,signature);consttransactionHash=facade.hashTransaction(transaction).toString();constconfirmed=newPromise((resolve)=>{websocket.addEventListener('message',(event)=>{constmessage=JSON.parse(event.data);consttopic=message.topic;constmessageHash=message.data.meta.hash;constname=topic.split('/')[0];console.log(`${name}: hash=${messageHash.substring(0,16)}...`);if(name==='confirmedAdded'&&messageHash===transactionHash){console.log(`Transaction ${transactionHash.substring(0,16)}`+'... confirmed');resolve();}});});awaitfetch(`${NODE_URL}/transactions`,{method:'PUT',headers:{'Content-Type':'application/json'},body:jsonPayload});console.log('Announced transaction '+`${transactionHash.substring(0,16)}...`);// Wait for confirmation via WebSocketawaitconfirmed;// Unsubscribe before closingfor(constchannelofchannels){websocket.send(JSON.stringify({uid,unsubscribe:channel}));}console.log('Unsubscribed from all channels');websocket.close();}catch(error){console.error(error);}
The snippet uses the NODE_URL environment variable to set the Symbol API node.
If no value is provided, a default one is used.
The WebSocket URL is derived from NODE_URL by replacing the HTTP protocol with the WebSocket protocol and appending
/ws.
Each transaction WebSocket channel is scoped to a specific address.
The MONITOR_ADDRESS environment variable sets the address to watch.
The channel sends a notification whenever this address is involved in a transaction, whether as sender, recipient,
or any other role derived from the transaction's content (for example, signer of an embedded transaction in an
aggregate transaction).
To trigger notifications, this tutorial sends a transfer transaction to the monitored address.
The sender's private key is read from SIGNER_PRIVATE_KEY.
If any of these environment variables is not provided, the tutorial provides default values that correspond to
the same account.
asyncwithconnect(WS_URL)aswebsocket:# Connect to WebSocketresponse=json.loads(awaitwebsocket.recv())uid=response['uid']print(f'Connected to {WS_URL} with uid {uid}')
// Connect to WebSocketconstwebsocket=newWebSocket(WS_URL);constuid=awaitnewPromise((resolve)=>{websocket.addEventListener('message',(event)=>{constmessage=JSON.parse(event.data);resolve(message.uid);},{once:true});});console.log(`Connected to ${WS_URL} with uid ${uid}`);
The code opens a WebSocket connection to the node's /ws endpoint.
Upon connecting, the server sends a message containing a unique identifier (uid) that must be included in all
subsequent subscription requests.
# Subscribe to transaction channelschannels=[f'unconfirmedAdded/{MONITOR_ADDRESS}',f'unconfirmedRemoved/{MONITOR_ADDRESS}',f'confirmedAdded/{MONITOR_ADDRESS}',]forchannelinchannels:awaitwebsocket.send(json.dumps({'uid':uid,'subscribe':channel}))name=channel.split('/')[0]print(f'Subscribed to {name} channel')
// Subscribe to transaction channelsconstchannels=[`unconfirmedAdded/${MONITOR_ADDRESS}`,`unconfirmedRemoved/${MONITOR_ADDRESS}`,`confirmedAdded/${MONITOR_ADDRESS}`,];for(constchannelofchannels){websocket.send(JSON.stringify({uid,subscribe:channel}));constname=channel.split('/')[0];console.log(`Subscribed to ${name} channel`);}
The code subscribes to three address-scoped channels, appending the monitored address to each channel name:
# Build and announce a transfer transactionwithurllib.request.urlopen(f'{NODE_URL}/node/time')asresp:time_json=json.loads(resp.read().decode())timestamp=NetworkTimestamp(int(time_json['communicationTimestamps']['receiveTimestamp']))withurllib.request.urlopen(f'{NODE_URL}/network/fees/transaction')asresp:fee_json=json.loads(resp.read().decode())fee_mult=max(fee_json['medianFeeMultiplier'],fee_json['minFeeMultiplier'])transaction=facade.transaction_factory.create({'type':'transfer_transaction_v1','signer_public_key':signer_key_pair.public_key,'deadline':timestamp.add_hours(2).timestamp,'recipient_address':MONITOR_ADDRESS,})transaction.fee=Amount(fee_mult*transaction.size)signature=facade.sign_transaction(signer_key_pair,transaction)json_payload=facade.transaction_factory.attach_signature(transaction,signature)transaction_hash=str(facade.hash_transaction(transaction))
// Build and announce a transfer transactionconsttimeResponse=awaitfetch(`${NODE_URL}/node/time`);consttimeJSON=awaittimeResponse.json();consttimestamp=newNetworkTimestamp(timeJSON.communicationTimestamps.receiveTimestamp);constfeeResponse=awaitfetch(`${NODE_URL}/network/fees/transaction`);constfeeJSON=awaitfeeResponse.json();constfeeMult=Math.max(feeJSON.medianFeeMultiplier,feeJSON.minFeeMultiplier);consttransaction=facade.transactionFactory.create({type:'transfer_transaction_v1',signerPublicKey:signerKeyPair.publicKey.toString(),deadline:timestamp.addHours(2).timestamp,recipientAddress:MONITOR_ADDRESS,});transaction.fee=newmodels.Amount(feeMult*transaction.size);constsignature=facade.signTransaction(signerKeyPair,transaction);constjsonPayload=facade.transactionFactory.static.attachSignature(transaction,signature);consttransactionHash=facade.hashTransaction(transaction).toString();
This tutorial builds a minimal Transfer Transaction to the monitored address, with no
mosaics and no message.
A transfer is used for simplicity, but any transaction type triggers the same WebSocket notifications.
The transaction is built as usual: fetching the network time and fee multiplier, creating the transaction descriptor,
and signing it.
The hash is computed locally so it can be matched against incoming WebSocket messages later.
constconfirmed=newPromise((resolve)=>{websocket.addEventListener('message',(event)=>{constmessage=JSON.parse(event.data);consttopic=message.topic;constmessageHash=message.data.meta.hash;constname=topic.split('/')[0];console.log(`${name}: hash=${messageHash.substring(0,16)}...`);if(name==='confirmedAdded'&&messageHash===transactionHash){console.log(`Transaction ${transactionHash.substring(0,16)}`+'... confirmed');resolve();}});});awaitfetch(`${NODE_URL}/transactions`,{method:'PUT',headers:{'Content-Type':'application/json'},body:jsonPayload});console.log('Announced transaction '+`${transactionHash.substring(0,16)}...`);// Wait for confirmation via WebSocketawaitconfirmed;
The code announces the transaction and then listens for incoming messages, printing each one.
Announce after subscribing to channels
Always announce the transaction after subscribing to the WebSocket channels to ensure the listener is ready.
Otherwise, notifications could arrive before the WebSocket is listening.
Each message includes a topic field identifying the channel and a data object with the event payload.
For confirmedAdded and unconfirmedAdded messages, the payload follows the
TransactionInfoDTO schema.
For unconfirmedRemoved messages, the payload contains only the transaction hash (meta.hash).
When a confirmedAdded message arrives whose hash matches the announced transaction, the program prints a confirmation
message and exits.
The expected sequence for a successful transaction is described in the
Transaction Lifecycle section:
unconfirmedAdded: The transaction enters the unconfirmed pool.
unconfirmedRemoved: The transaction leaves the unconfirmed pool.
confirmedAdded: The transaction is confirmed in a block.
# Unsubscribe before closingforchannelinchannels:awaitwebsocket.send(json.dumps({'uid':uid,'unsubscribe':channel}))print('Unsubscribed from all channels')
// Unsubscribe before closingfor(constchannelofchannels){websocket.send(JSON.stringify({uid,unsubscribe:channel}));}console.log('Unsubscribed from all channels');websocket.close();
After confirmation, the code sends unsubscribe messages for all three channels before closing the connection.
Using node https://reference.symboltest.net:3001
Monitoring address: TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I
Connected to wss://reference.symboltest.net:3001/ws with uid Hj3kL9mN2pQr5tVw=
Subscribed to unconfirmedAdded channel
Subscribed to unconfirmedRemoved channel
Subscribed to confirmedAdded channel
Announced transaction 7A3F1B9E4C2D8A65...
unconfirmedAdded: hash=7A3F1B9E4C2D8A65...
unconfirmedRemoved: hash=7A3F1B9E4C2D8A65...
confirmedAdded: hash=7A3F1B9E4C2D8A65...
Transaction 7A3F1B9E4C2D8A65... confirmed
Unsubscribed from all channels
The output shows:
Address (line 2): The monitored address.
Connection (line 3): The WebSocket connection is established and the server returns a unique uid.
Subscriptions (lines 4-6): All three transaction channels are subscribed.
Announcement (line 7): The transaction is announced and its hash is printed.
Transaction flow (lines 8-10): The transaction moves from unconfirmedAdded to unconfirmedRemoved to
confirmedAdded, showing the full confirmation lifecycle.
Confirmation (line 11): The hash from confirmedAdded matches the announced transaction, confirming success.
Unsubscribe (line 12): The code unsubscribes from all channels.