Skip to content

Monitoring Transaction Status⚓︎

After announcing a transaction to the Symbol 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 monitor a transaction's status as it moves from unconfirmed to confirmed.

Confirmed transactions can still be reversed

A confirmed transaction has been included in a block but is not yet irreversible. The final state is finalization, which occurs only after the block is finalized by the network. Until then, rollbacks are still possible.

Prerequisites⚓︎

This tutorial uses the Symbol REST API without requiring an SDK. You only need a way to make HTTP requests.

Full Code⚓︎

This tutorial uses polling to check the transaction status. Polling is used here 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.

import json
import os
import time
import urllib.request

# Configuration
NODE_URL = os.environ.get(
    "NODE_URL", "https://001-sai-dual.symboltest.net:3001"
)
print(f'Using node {NODE_URL}')

# Transaction hash to monitor
transaction_hash = os.environ.get(
    "TRANSACTION_HASH",
    "2B6D3B5232E06B9D32682F518C765301FCF9716BFA1EEEF9523653406E04C7EA",
)

print(f"Monitoring transaction: {transaction_hash}")


def wait_for_transaction_confirmation(
    transaction_hash, max_attempts=60, wait_seconds=2
):
    """
    Poll the transaction status endpoint until the transaction
    is confirmed.

    Args:
        transaction_hash: The hash of the transaction to monitor
        max_attempts: Maximum number of polling attempts for confirmation
        wait_seconds: Seconds to wait between attempts

    Returns:
        True if transaction was confirmed
    """
    status_path = f"/transactionStatus/{transaction_hash}"
    print(f"\nWaiting for transaction confirmation")
    print(f"Polling {status_path}")

    for attempt in range(1, max_attempts + 1):
        try:
            # Query the transaction status endpoint
            url = f"{NODE_URL}{status_path}"
            with urllib.request.urlopen(url) as response:
                response_json = json.loads(response.read().decode())

                # Parse the response
                status_group = response_json["group"]
                status_code = response_json["code"]
                status_hash = response_json["hash"]
                status_deadline = response_json["deadline"]

                print(f"  Attempt {attempt}:")
                print(f"    Status: {status_group}")
                print(f"    Code: {status_code}")
                print(f"    Hash: {status_hash}")
                print(f"    Deadline: {status_deadline}")

                # Check if the transaction has been confirmed
                if status_group == "confirmed":
                    print(f"\nTransaction confirmed!")
                    return True

                # Check if the transaction failed
                if status_group == "failed":
                    print(
                        f"\nTransaction failed with code: {status_code}"
                    )
                    raise RuntimeError(
                        f"Transaction failed: {status_code}"
                    )

        except Exception as e:
            if hasattr(e, 'code') and e.code == 404:
                print(
                    f"  Attempt {attempt}: Transaction status not "
                    "yet available"
                )
            else:
                raise

        # Wait before next attempt (except on last attempt)
        if attempt < max_attempts:
            time.sleep(wait_seconds)

    print(f"\nTransaction not confirmed after {max_attempts} attempts")
    raise RuntimeError(
        f"Transaction {transaction_hash} not confirmed in time"
    )


# Monitor the transaction until it's confirmed
wait_for_transaction_confirmation(transaction_hash)

Download source

// Configuration
const NODE_URL = process.env.NODE_URL||
    'https://001-sai-dual.symboltest.net:3001';
console.log('Using node', NODE_URL);

// Transaction hash to monitor.
const transactionHash = process.env.TRANSACTION_HASH ||
    '2B6D3B5232E06B9D32682F518C765301FCF9716BFA1EEEF9523653406E04C7EA';

console.log(`Monitoring transaction: ${transactionHash}`);

/**
 * Poll the transaction status endpoint until the transaction is confirmed.
 *
 * @param {string} transactionHash - The hash of the transaction to monitor
 * @param {number} maxAttempts - Maximum number of polling attempts
 *   for confirmation
 * @param {number} waitSeconds - Seconds to wait between attempts
 * @returns {boolean} True if transaction was confirmed
 */
async function waitForTransactionConfirmation(
    transactionHash,
    maxAttempts = 60,
    waitSeconds = 2
) {
    const statusPath = `/transactionStatus/${transactionHash}`;
    console.log(`\nWaiting for transaction confirmation`);
    console.log(`Polling ${statusPath}`);

    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
        try {
            // Query the transaction status endpoint
            const statusResponse = await fetch(`${NODE_URL}${statusPath}`);

            if (!statusResponse.ok) {
                const status = statusResponse.status;
                const statusText = statusResponse.statusText;
                const error = new Error(`HTTP ${status}: ${statusText}`);
                error.status = statusResponse.status;
                throw error;
            }

            const statusJSON = await statusResponse.json();

            // Parse the response
            const statusGroup = statusJSON.group;
            const statusCode = statusJSON.code;
            const statusHash = statusJSON.hash;
            const statusDeadline = statusJSON.deadline;

            console.log(`  Attempt ${attempt}:`);
            console.log(`    Status: ${statusGroup}`);
            console.log(`    Code: ${statusCode}`);
            console.log(`    Hash: ${statusHash}`);
            console.log(`    Deadline: ${statusDeadline}`);

            // Check if the transaction has been confirmed
            if (statusGroup === 'confirmed') {
                console.log(`\nTransaction confirmed!`);
                return true;
            }

            // Check if the transaction failed
            if (statusGroup === 'failed') {
                console.log(
                    `\nTransaction failed with code: ${statusCode}`
                );
                throw new Error(`Transaction failed: ${statusCode}`);
            }

        } catch (error) {
            if (error.status === 404) {
                console.log(
                    `  Attempt ${attempt}: Transaction status not yet ` +
                    `available`
                );
            } else {
                throw error;
            }
        }

        // Wait before next attempt (except on last attempt)
        if (attempt < maxAttempts) {
            await new Promise((resolve) =>
                setTimeout(resolve, waitSeconds * 1000)
            );
        }
    }

    console.log(
        `\nTransaction not confirmed after ${maxAttempts} attempts`
    );
    throw new Error(
        `Transaction ${transactionHash} not confirmed in time`
    );
}

// Monitor the transaction until it's confirmed
await waitForTransactionConfirmation(transactionHash);

Download source

The snippet uses the NODE_URL environment variable to set the Symbol API node. If no value is provided, a default one is used.

Code Explanation⚓︎

Finding the Transaction Hash⚓︎

transaction_hash = os.environ.get(
    "TRANSACTION_HASH",
    "2B6D3B5232E06B9D32682F518C765301FCF9716BFA1EEEF9523653406E04C7EA",
)
const transactionHash = process.env.TRANSACTION_HASH ||
    '2B6D3B5232E06B9D32682F518C765301FCF9716BFA1EEEF9523653406E04C7EA';

To monitor a transaction, you need its hash, which is generated after signing. The hash uniquely identifies the transaction on the Symbol network.

This tutorial uses a sample transaction hash to demonstrate the monitoring. You can provide your own hash by setting the TRANSACTION_HASH environment variable when running the code.

In practice, you would obtain this hash immediately after signing a transaction (see the Transfer tutorial for an example) and use it to track its status.

Querying the Status Endpoint⚓︎

def wait_for_transaction_confirmation(
    transaction_hash, max_attempts=60, wait_seconds=2
):
    """
    Poll the transaction status endpoint until the transaction
    is confirmed.

    Args:
        transaction_hash: The hash of the transaction to monitor
        max_attempts: Maximum number of polling attempts for confirmation
        wait_seconds: Seconds to wait between attempts

    Returns:
        True if transaction was confirmed
    """
    status_path = f"/transactionStatus/{transaction_hash}"
    print(f"\nWaiting for transaction confirmation")
    print(f"Polling {status_path}")

    for attempt in range(1, max_attempts + 1):
        try:
            # Query the transaction status endpoint
            url = f"{NODE_URL}{status_path}"
            with urllib.request.urlopen(url) as response:
                response_json = json.loads(response.read().decode())

                # Parse the response
                status_group = response_json["group"]
                status_code = response_json["code"]
                status_hash = response_json["hash"]
                status_deadline = response_json["deadline"]

                print(f"  Attempt {attempt}:")
                print(f"    Status: {status_group}")
                print(f"    Code: {status_code}")
                print(f"    Hash: {status_hash}")
                print(f"    Deadline: {status_deadline}")
/**
 * Poll the transaction status endpoint until the transaction is confirmed.
 *
 * @param {string} transactionHash - The hash of the transaction to monitor
 * @param {number} maxAttempts - Maximum number of polling attempts
 *   for confirmation
 * @param {number} waitSeconds - Seconds to wait between attempts
 * @returns {boolean} True if transaction was confirmed
 */
async function waitForTransactionConfirmation(
    transactionHash,
    maxAttempts = 60,
    waitSeconds = 2
) {
    const statusPath = `/transactionStatus/${transactionHash}`;
    console.log(`\nWaiting for transaction confirmation`);
    console.log(`Polling ${statusPath}`);

    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
        try {
            // Query the transaction status endpoint
            const statusResponse = await fetch(`${NODE_URL}${statusPath}`);

            if (!statusResponse.ok) {
                const status = statusResponse.status;
                const statusText = statusResponse.statusText;
                const error = new Error(`HTTP ${status}: ${statusText}`);
                error.status = statusResponse.status;
                throw error;
            }

            const statusJSON = await statusResponse.json();

            // Parse the response
            const statusGroup = statusJSON.group;
            const statusCode = statusJSON.code;
            const statusHash = statusJSON.hash;
            const statusDeadline = statusJSON.deadline;

            console.log(`  Attempt ${attempt}:`);
            console.log(`    Status: ${statusGroup}`);
            console.log(`    Code: ${statusCode}`);
            console.log(`    Hash: ${statusHash}`);
            console.log(`    Deadline: ${statusDeadline}`);

The wait_for_transaction_confirmation function is the core of this tutorial. It monitors a transaction until it is confirmed or fails.

It uses a for loop to check the transaction status up to 60 times by default (2 minutes with 2-second intervals between attempts). This loop structure ensures that monitoring will eventually stop, even if the transaction never confirms.

On each attempt, the function queries the /transactionStatus/{hash} GET endpoint, which returns information about the transaction's current state.

The response includes:

  • Group: The transaction's current status group. Possible values:

    Group Meaning
    unconfirmed The transaction is in the unconfirmed pool waiting to be included in a block.
    confirmed The transaction has been included in a block.
    failed The transaction failed validation and has been rejected.
    partial For bonded aggregate transactions waiting for cosignatures.
  • Code: A status code providing more details (for example, Success or specific error codes). See the TransactionStatusEnum schema for all possible values.

  • Hash: The transaction hash being monitored.
  • Deadline: The transaction's deadline in network time.

The function displays all these fields on each polling attempt so you can see how the transaction progresses through states.

Checking for Confirmation⚓︎

                # Check if the transaction has been confirmed
                if status_group == "confirmed":
                    print(f"\nTransaction confirmed!")
                    return True
            // Check if the transaction has been confirmed
            if (statusGroup === 'confirmed') {
                console.log(`\nTransaction confirmed!`);
                return true;
            }

After parsing the response, the function checks the group field. If it is confirmed, the transaction was successfully included in a block through harvesting, and the function returns successfully.

Checking for Failure⚓︎

                # Check if the transaction failed
                if status_group == "failed":
                    print(
                        f"\nTransaction failed with code: {status_code}"
                    )
                    raise RuntimeError(
                        f"Transaction failed: {status_code}"
                    )
            // Check if the transaction failed
            if (statusGroup === 'failed') {
                console.log(
                    `\nTransaction failed with code: ${statusCode}`
                );
                throw new Error(`Transaction failed: ${statusCode}`);
            }

If the transaction status group is failed, the function raises an error with the status code.

Common reasons include insufficient balance, invalid signatures, or deadline expiration. Failed transactions are rejected during validation and will not be included in a block.

See TransactionStatusEnum for all possible codes.

Handling Unknown Status⚓︎

        except Exception as e:
            if hasattr(e, 'code') and e.code == 404:
                print(
                    f"  Attempt {attempt}: Transaction status not "
                    "yet available"
                )
            else:
                raise
        } catch (error) {
            if (error.status === 404) {
                console.log(
                    `  Attempt ${attempt}: Transaction status not yet ` +
                    `available`
                );
            } else {
                throw error;
            }
        }

If the endpoint returns HTTP 404, the transaction status is not yet available. This can happen immediately after announcing a transaction, before the node processes it, or if the hash is invalid. The function handles this case by logging the attempt and continuing to poll.

For any other error (such as connectivity issues or failed transactions), the function re-raises the exception immediately.

Waiting Between Attempts⚓︎

        # Wait before next attempt (except on last attempt)
        if attempt < max_attempts:
            time.sleep(wait_seconds)
        // Wait before next attempt (except on last attempt)
        if (attempt < maxAttempts) {
            await new Promise((resolve) =>
                setTimeout(resolve, waitSeconds * 1000)
            );
        }

Between polling attempts, the function waits for a configurable delay (default: 2 seconds). This prevents overwhelming the node with requests and allows time for network processing.

Handling Timeouts⚓︎

    print(f"\nTransaction not confirmed after {max_attempts} attempts")
    raise RuntimeError(
        f"Transaction {transaction_hash} not confirmed in time"
    )
    console.log(
        `\nTransaction not confirmed after ${maxAttempts} attempts`
    );
    throw new Error(
        `Transaction ${transactionHash} not confirmed in time`
    );

If the transaction is not confirmed after the specified number of attempts, the function raises a RuntimeError explaining the problem.

This ensures that the calling code is aware that the transaction didn't complete in the expected timeframe and can take appropriate action, such as:

  • Retrying the transaction announcement
  • Alerting the user
  • Logging the issue for investigation

Output⚓︎

The following output shows a typical run monitoring a transaction as it moves from unconfirmed to confirmed:

Using node https://001-sai-dual.symboltest.net:3001
Monitoring transaction: 2B6D3B5232E06B9D32682F518C765301FCF9716BFA1EEEF9523653406E04C7EA

Waiting for transaction confirmation
Polling /transactionStatus/2B6D3B5232E06B9D32682F518C765301FCF9716BFA1EEEF9523653406E04C7EA
  Attempt 1: Transaction status not yet available
  Attempt 2: Transaction status not yet available
  Attempt 3:
    Status: unconfirmed
    Code: Success
    Hash: 2B6D3B5232E06B9D32682F518C765301FCF9716BFA1EEEF9523653406E04C7EA
    Deadline: 47578965854
  Attempt 4:
    Status: unconfirmed
    Code: Success
    Hash: 2B6D3B5232E06B9D32682F518C765301FCF9716BFA1EEEF9523653406E04C7EA
    Deadline: 47578965854
  Attempt 5:
    Status: confirmed
    Code: Success
    Hash: 2B6D3B5232E06B9D32682F518C765301FCF9716BFA1EEEF9523653406E04C7EA
    Deadline: 47578965854

Transaction confirmed!

The output shows:

  1. The transaction hash being monitored.
  2. Multiple polling attempts showing the transaction status, code, hash, and deadline.
  3. The status changing from unconfirmed to confirmed between attempts.
  4. A success message when the transaction is confirmed.

The number of attempts and timing vary depending on network conditions and block production rate. On the Symbol network, blocks are typically produced every 30 seconds, so you may see several unconfirmed status responses before the transaction is confirmed.

Once confirmed, you can get additional details such as the block height where the transaction was included by querying /transactions/confirmed/{transactionId} GET with the transaction hash.

To see the transaction from the network's perspective, visit the Symbol Testnet Explorer and search for the transaction hash.

Conclusion⚓︎

This tutorial showed how to:

Step Related documentation
Query the status endpoint /transactionStatus/{hash} GET
Check for confirmation TransactionStatusEnum
Check for failure TransactionStatusEnum

Next steps⚓︎

For production applications, consider these improvements:

  • Wait for finalization: Verify that the block containing the transaction has been finalized using /finalization/proof/height/{height} GET to ensure it is truly irreversible.
  • Query multiple nodes: Check status and finalization 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.