diff --git a/Cargo.lock b/Cargo.lock
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -656,6 +656,7 @@
  "hex-literal",
  "ripemd",
  "serde",
+ "serde_json",
  "sha2",
  "thiserror 2.0.4",
 ]
diff --git a/chronik/bitcoinsuite-core/Cargo.toml b/chronik/bitcoinsuite-core/Cargo.toml
--- a/chronik/bitcoinsuite-core/Cargo.toml
+++ b/chronik/bitcoinsuite-core/Cargo.toml
@@ -25,6 +25,9 @@
 # Serialize structs
 serde = { version = "1.0", features = ["derive"] }
 
+# JSON en-/decoding
+serde_json = "1.0.133"
+
 # Implementation of SHA-256 etc. cryptographic hash functions
 sha2 = "0.10"
 
diff --git a/chronik/bitcoinsuite-core/src/tx/txid.rs b/chronik/bitcoinsuite-core/src/tx/txid.rs
--- a/chronik/bitcoinsuite-core/src/tx/txid.rs
+++ b/chronik/bitcoinsuite-core/src/tx/txid.rs
@@ -2,6 +2,8 @@
 // Distributed under the MIT software license, see the accompanying
 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
 
+use std::str::FromStr;
+
 use serde::{Deserialize, Serialize};
 
 use crate::{
@@ -136,6 +138,19 @@
     }
 }
 
+impl TryFrom<&'_ serde_json::Value> for TxId {
+    type Error = &'static str;
+
+    fn try_from(value: &'_ serde_json::Value) -> Result<Self, Self::Error> {
+        let txid_hex = value.as_str();
+        if txid_hex.is_none() {
+            return Err("TxId must be a hexadecimal Value::String");
+        }
+        TxId::from_str(txid_hex.unwrap())
+            .or(Err("Cannot parse TxId from hex string"))
+    }
+}
+
 impl From<[u8; 32]> for TxId {
     fn from(array: [u8; 32]) -> Self {
         TxId(Sha256d(array))
diff --git a/chronik/chronik-http/src/electrum.rs b/chronik/chronik-http/src/electrum.rs
--- a/chronik/chronik-http/src/electrum.rs
+++ b/chronik/chronik-http/src/electrum.rs
@@ -4,7 +4,6 @@
 
 //! Module for [`ChronikElectrumServer`].
 
-use std::str::FromStr;
 use std::{net::SocketAddr, sync::Arc};
 
 use abc_rust_error::Result;
@@ -275,6 +274,9 @@
             "transaction.get" => Some(Box::new(move |params: Value| {
                 Box::pin(self.transaction_get(params))
             })),
+            "transaction.get_height" => Some(Box::new(move |params: Value| {
+                Box::pin(self.transaction_get_height(params))
+            })),
             _ => None,
         }
     }
@@ -332,14 +334,9 @@
         let txid_hex = get_param!(params, 0, "txid")?;
         let verbose =
             get_optional_param!(params, 1, "verbose", Value::Bool(false))?;
-        let txid_hex = match txid_hex {
-            Value::String(s) => Ok(s),
-            _ => Err(RPCError::InvalidParams(
-                "'txid' must be a hexadecimal string",
-            )),
-        }?;
-        let txid = TxId::from_str(&txid_hex)
-            .or(Err(RPCError::InvalidParams("Failed to parse txid")))?;
+        let txid = TxId::try_from(&txid_hex).or(Err(
+            RPCError::InvalidParams("'txid' must be a hexadecimal string"),
+        ))?;
 
         let verbose = match verbose {
             Value::Bool(v) => Ok(v),
@@ -371,7 +368,7 @@
             // mempool transaction
             return Ok(json!({
                 "confirmations": 0,
-                "hash": txid_hex,
+                "hash": txid_hex.as_str(),
                 "hex": raw_tx,
                 "time": tx.time_first_seen,
             }));
@@ -386,9 +383,32 @@
             "blockhash": blockhash.hex_be(),
             "blocktime": block.timestamp,
             "confirmations": confirmations,
-            "hash": txid_hex,
+            "hash": txid_hex.as_str(),
             "hex": raw_tx,
             "time": tx.time_first_seen,
         }))
     }
+
+    async fn transaction_get_height(
+        &self,
+        params: Value,
+    ) -> Result<Value, RPCError> {
+        let txid_hex = get_param!(params, 0, "txid")?;
+        let txid = TxId::try_from(&txid_hex).or(Err(
+            RPCError::InvalidParams("'txid' must be a hexadecimal string"),
+        ))?;
+
+        let indexer = self.indexer.read().await;
+        let query_tx = indexer.txs(&self.node);
+        let tx = query_tx
+            .tx_by_id(txid)
+            .or(Err(RPCError::InvalidRequest("Unknown txid")))?;
+
+        if tx.block.is_none() {
+            // mempool transaction
+            return Ok(json!(0));
+        }
+        let block = tx.block.unwrap();
+        Ok(json!(block.height))
+    }
 }
diff --git a/test/functional/chronik_electrum_blockchain.py b/test/functional/chronik_electrum_blockchain.py
--- a/test/functional/chronik_electrum_blockchain.py
+++ b/test/functional/chronik_electrum_blockchain.py
@@ -13,6 +13,7 @@
 )
 from test_framework.test_framework import BitcoinTestFramework
 from test_framework.util import assert_equal
+from test_framework.wallet import MiniWallet
 
 COINBASE_TX_HEX = (
     "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d"
@@ -25,7 +26,6 @@
 
 class ChronikElectrumBlockchain(BitcoinTestFramework):
     def set_test_params(self):
-        self.setup_clean_chain = True
         self.num_nodes = 1
         self.extra_args = [["-chronik"]]
 
@@ -34,6 +34,9 @@
 
     def run_test(self):
         self.client = self.nodes[0].get_chronik_electrum_client()
+        self.node = self.nodes[0]
+        self.wallet = MiniWallet(self.node)
+
         self.test_invalid_params()
         self.test_transaction_get()
 
@@ -79,7 +82,7 @@
         ):
             assert_equal(
                 response.error,
-                {"code": -32602, "message": "Failed to parse txid"},
+                {"code": -32602, "message": "'txid' must be a hexadecimal string"},
             )
 
         # Valid txid, but no such transaction was found
@@ -106,20 +109,40 @@
                 {
                     "blockhash": GENESIS_BLOCK_HASH,
                     "blocktime": TIME_GENESIS_BLOCK,
-                    "confirmations": 1,
+                    "confirmations": 201,
                     "hash": GENESIS_CB_TXID,
                     "hex": COINBASE_TX_HEX,
                     "time": 0,
                 },
             )
+
         self.generate(self.nodes[0], 2)
         assert_equal(
             self.client.blockchain.transaction.get(
                 txid=GENESIS_CB_TXID, verbose=True
             ).result["confirmations"],
-            3,
+            203,
         )
 
+    def test_transaction_get_height(self):
+        response = self.client.blockchain.transaction.get_height(GENESIS_CB_TXID)
+        assert_equal(response.result, 0)
+
+        self.wallet.rescan_utxos()
+        tx = self.wallet.send_self_transfer(from_node=self.node)
+
+        response = self.client.blockchain.transaction.get(tx["txid"])
+        assert_equal(response.result, tx["hex"])
+
+        # A mempool transaction has height 0
+        response = self.client.blockchain.transaction.get_height(tx["txid"])
+        assert_equal(response.result, 0)
+
+        # Mine the tx
+        self.generate(self.node, 1)
+        response = self.client.blockchain.transaction.get_height(tx["txid"])
+        assert_equal(response.result, 203)
+
 
 if __name__ == "__main__":
     ChronikElectrumBlockchain().main()