Logo
New RPC users get 35% off their first monthView the offer
OnFinality Learn
Integration & Development10 min read

Transaction Receipt Explained: Fields, Retrieval, and Verification

A practical guide to blockchain transaction receipts: what each field means, how to fetch and verify a receipt over JSON-RPC, and how to handle pending, reverted, and missing receipts.

TL;DR

A transaction receipt is the post-execution record of a blockchain transaction: it confirms whether execution succeeded, reports gas consumed, and carries the event logs emitted during execution. Unlike a transaction object, which describes what was requested, a receipt describes what actually happened once the transaction was included in a block. This article explains the receipt data model, shows how to retrieve and verify receipts over JSON-RPC in curl, JavaScript, and Python, and covers the common failure modes: null receipts, reverted status, missing logs, and chain or provider mismatches. It also outlines a reproducible measurement procedure for receipt latency so you can evaluate your own RPC path rather than relying on vendor claims.

What a Transaction Receipt Represents

A transaction receipt is the canonical record that a transaction was included in a block and what its execution produced. It is keyed by the transaction hash and becomes available only after the transaction has been mined or otherwise finalized into a block. Before inclusion, the receipt does not exist and receipt lookups return null.

The distinction that matters most in practice is between the transaction and its receipt. The transaction object describes intent: sender, recipient, value, gas limit, gas price fields, nonce, and calldata. The receipt describes outcome: whether execution succeeded or reverted, how much gas was actually consumed, the effective gas price paid, the contract address created by a deployment, and the logs emitted. Applications that need to know whether something worked should read the receipt, not the transaction.

Receipts are also the substrate for event-driven indexing. Every Transfer, Swap, or custom event your backend reacts to is a log entry inside a receipt. If you build confirmation logic, you are effectively waiting for a receipt with a sufficient number of descendant blocks.

  • Transaction: what was requested, signed, and broadcast.
  • Receipt: what executed, what it cost, and what it emitted.
  • Receipts exist only after inclusion; pre-inclusion lookups return null.

Assumptions and Prerequisites

The examples below assume an EVM-compatible JSON-RPC endpoint and a transaction hash you control or can observe. You need an RPC URL for the correct network; substitute your own RPC_URL placeholder rather than hardcoding a public endpoint, since public endpoints vary in rate limits, archive depth, and supported methods. If you are choosing a provider for receipt-heavy workloads such as indexers or confirmation services, the RPC endpoint guide and the multi-chain RPC endpoints guide cover the selection criteria, and OnFinality's API service describes managed access across supported networks.

You should also know which chain you are targeting. A receipt fetched from the wrong chain for a given hash will either be null or, in rare cross-chain hash collisions, misleading. Always pin the chain ID alongside the hash in your application state.

Receipt Fields and Their Meaning

The exact field set is defined by the JSON-RPC specification and is broadly consistent across EVM chains, though some chains add fields such as L1 fee data on rollups. The core fields are stable enough to build on.

  • transactionHash: the hash that identifies the transaction and its receipt.
  • blockHash and blockNumber: where the transaction was included.
  • transactionIndex: position within the block.
  • from and to: sender and recipient; to is null for contract creation.
  • contractAddress: the deployed address when the transaction created a contract, otherwise null.
  • cumulativeGasUsed: gas used by all transactions up to and including this one in the block.
  • gasUsed: gas consumed by this transaction alone.
  • effectiveGasPrice: the price actually paid per unit of gas (post-EIP-1559 on supporting chains).
  • status: 0x1 for success, 0x0 for reverted execution.
  • logs: array of event logs, each with address, topics, data, and log index.
  • logsBloom: a probabilistic filter over the block's logs, useful for light clients and some indexers.
  • type: transaction type, for example legacy or EIP-1559.

Retrieving a Receipt with curl

The standard method is eth_getTransactionReceipt. It takes a single argument, the transaction hash, and returns the receipt object or null. The following request is runnable once you replace the placeholders.

curl -s -X POST "$RPC_URL" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "eth_getTransactionReceipt",
    "params": ["0xYOUR_TRANSACTION_HASH"]
  }'

Retrieving and Verifying a Receipt in JavaScript

In application code you rarely want a single fire-and-forget call. You want to poll until the receipt exists, then assert success and inspect logs. The example below uses fetch directly so it works without a library, and it treats null as pending rather than as an error.

const RPC_URL = process.env.RPC_URL;
const TX_HASH = process.env.TX_HASH;

async function rpc(method, params) {
  const res = await fetch(RPC_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })
  });
  const json = await res.json();
  if (json.error) throw new Error(json.error.message);
  return json.result;
}

async function waitForReceipt(hash, timeoutMs = 120000, intervalMs = 2000) {
  const start = Date.now();
  while (Date.now() - start < timeoutMs) {
    const receipt = await rpc('eth_getTransactionReceipt', [hash]);
    if (receipt) return receipt;
    await new Promise(r => setTimeout(r, intervalMs));
  }
  throw new Error('Timed out waiting for receipt');
}

const receipt = await waitForReceipt(TX_HASH);
if (receipt.status !== '0x1') {
  throw new Error(`Transaction reverted in block ${receipt.blockNumber}`);
}
console.log('Gas used:', parseInt(receipt.gasUsed, 16));
console.log('Logs emitted:', receipt.logs.length);

Retrieving a Receipt in Python

Python services often batch receipt checks across many hashes. The snippet below uses only the standard library and demonstrates the same pending-versus-reverted distinction. For high-volume indexing, batch requests or a websocket subscription to new heads is usually more efficient than per-hash polling.

import json, os, time, urllib.request

RPC_URL = os.environ["RPC_URL"]
TX_HASH = os.environ["TX_HASH"]

def rpc(method, params):
    payload = json.dumps({"jsonrpc": "2.0", "id": 1, "method": method, "params": params}).encode()
    req = urllib.request.Request(RPC_URL, data=payload, headers={"Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=30) as resp:
        body = json.loads(resp.read())
    if "error" in body:
        raise RuntimeError(body["error"]["message"])
    return body["result"]

def wait_for_receipt(tx_hash, timeout=120, interval=2):
    deadline = time.time() + timeout
    while time.time() < deadline:
        receipt = rpc("eth_getTransactionReceipt", [tx_hash])
        if receipt:
            return receipt
        time.sleep(interval)
    raise TimeoutError("receipt not found before timeout")

receipt = wait_for_receipt(TX_HASH)
assert receipt["status"] == "0x1", f"reverted in block {receipt['blockNumber']}"
print("gasUsed", int(receipt["gasUsed"], 16))
print("logs", len(receipt["logs"]))

Expected Verification Result

A successful verification has three observable outcomes. First, the receipt object is non-null and its transactionHash matches the hash you queried. Second, status is 0x1. Third, if your application depends on events, the expected log entries appear with the correct emitting contract address and topic0 signature.

If you are confirming finality rather than mere inclusion, compare the receipt's blockNumber against the current head from eth_blockNumber and require a confirmation depth appropriate to the chain. Chains with probabilistic finality need a larger depth than chains with explicit finality gadgets. Record the depth you use; it is a policy decision, not a protocol constant.

When you run latency measurements, capture them in a table rather than asserting a number. A reproducible format looks like this:

  • Columns: network, RPC path, sample size, p50 receipt latency, p95 receipt latency, null-rate at first poll.
  • Procedure: broadcast or select a known transaction, poll eth_getTransactionReceipt at a fixed interval, and timestamp the first non-null response.
  • Repeat across several transactions and at different times of day; report percentiles, not averages alone.

Troubleshooting Null, Reverted, and Mismatched Receipts

A null receipt is the most common issue and almost always means the transaction is not yet included. It can also mean the transaction was dropped from the mempool, was replaced by a higher-fee transaction with the same nonce, or was never broadcast. Check the sender's nonce and the mempool status before concluding the transaction failed.

A receipt with status: 0x0 means the transaction was included and paid for, but execution reverted. The gas was still spent. To diagnose, replay the call with eth_call at the parent block, or inspect the transaction trace if your provider supports tracing methods. Do not treat a reverted receipt as a network error; it is a valid on-chain outcome.

A receipt that exists but whose logs do not match expectations usually indicates a wrong contract address, a changed event signature, or a proxy upgrade. Decode topics[0] against the ABI you believe is deployed and confirm the emitting address in each log.

Finally, a receipt fetched from the wrong chain will typically be null, but if you operate multiple networks, tag every request with a chain ID and assert it in your client. Provider-side issues such as rate limiting usually surface as JSON-RPC errors rather than null results, so distinguish transport errors from genuine absence.

Limitations and Tradeoffs

Receipts are not a complete audit trail. They do not include internal calls, revert reasons, or state diffs unless you use tracing APIs, which many providers restrict or meter separately. If you need full execution detail, plan for trace-capable infrastructure and budget accordingly.

Polling for receipts is simple but wasteful at scale. Websocket subscriptions to new heads, combined with local filtering, reduce request volume but add connection-management complexity and reconnection logic. Batch JSON-RPC requests are a middle ground for indexers processing many hashes per block.

Archive requirements matter if you backfill historical receipts. Recent receipts are usually served by full nodes, but deep history may require archive access. Confirm your provider's retention before designing a backfill job, and see dedicated node options if you need predictable capacity. Pricing for higher-throughput RPC access is outlined on the RPC pricing page.

Next Steps

Start by instrumenting your receipt path: log the hash, chain ID, poll count, and final status for every transaction your application submits. That single change turns most 'transaction stuck' reports into diagnosable data.

Then decide your confirmation policy per chain and encode it as configuration rather than scattered constants. If you operate across many networks, review the supported networks list and standardize your receipt client so chain-specific quirks are handled in one place.

For teams choosing an endpoint to carry receipt traffic, the concrete provider decision is best handled by the RPC Assistant, which compares endpoint options against your latency and reliability needs. Use this article for the mechanics and your own measurements for the verdict.

Never Worry about Infrastructure Again

OnFinality takes away the heavy lifting of DevOps so you can build smarter and faster.

Get Started