diff --git a/cumulus/client/consensus/common/src/tests.rs b/cumulus/client/consensus/common/src/tests.rs
index 06f90330d4745525cb20f6c3950b7d011a71f486..794ce30de3e132ace2360816f5f20654de054f38 100644
--- a/cumulus/client/consensus/common/src/tests.rs
+++ b/cumulus/client/consensus/common/src/tests.rs
@@ -268,6 +268,15 @@ impl RelayChainInterface for Relaychain {
 	async fn version(&self, _: PHash) -> RelayChainResult<RuntimeVersion> {
 		unimplemented!("Not needed for test")
 	}
+
+	async fn call_runtime_api(
+		&self,
+		_method_name: &'static str,
+		_hash: PHash,
+		_payload: &[u8],
+	) -> RelayChainResult<Vec<u8>> {
+		unimplemented!("Not needed for test")
+	}
 }
 
 fn sproof_with_best_parent(client: &Client) -> RelayStateSproofBuilder {
diff --git a/cumulus/client/network/src/tests.rs b/cumulus/client/network/src/tests.rs
index 1c8edd803ed84dc2e8b4466b8621d61a0966bd38..686943063bb0a721c323b7576cfe9d21b20439fe 100644
--- a/cumulus/client/network/src/tests.rs
+++ b/cumulus/client/network/src/tests.rs
@@ -326,6 +326,15 @@ impl RelayChainInterface for DummyRelayChainInterface {
 			system_version: 1,
 		})
 	}
+
+	async fn call_runtime_api(
+		&self,
+		_method_name: &'static str,
+		_hash: PHash,
+		_payload: &[u8],
+	) -> RelayChainResult<Vec<u8>> {
+		unimplemented!("Not needed for test")
+	}
 }
 
 fn make_validator_and_api() -> (
diff --git a/cumulus/client/pov-recovery/src/tests.rs b/cumulus/client/pov-recovery/src/tests.rs
index 5935824e173ab407e3dd40a1d87930d30f3f2d22..539f7f33ad34b1660470c60b77999cdbe4169a41 100644
--- a/cumulus/client/pov-recovery/src/tests.rs
+++ b/cumulus/client/pov-recovery/src/tests.rs
@@ -487,6 +487,15 @@ impl RelayChainInterface for Relaychain {
 	) -> RelayChainResult<Vec<CoreState<PHash, NumberFor<Block>>>> {
 		unimplemented!("Not needed for test");
 	}
+
+	async fn call_runtime_api(
+		&self,
+		_method_name: &'static str,
+		_hash: PHash,
+		_payload: &[u8],
+	) -> RelayChainResult<Vec<u8>> {
+		unimplemented!("Not needed for test")
+	}
 }
 
 fn make_candidate_chain(candidate_number_range: Range<u32>) -> Vec<CommittedCandidateReceipt> {
diff --git a/cumulus/client/relay-chain-inprocess-interface/src/lib.rs b/cumulus/client/relay-chain-inprocess-interface/src/lib.rs
index 629fa728be372b915af2e51248706700ac6909c9..0455c03fc4dedce7480efe069378a3c630d4b966 100644
--- a/cumulus/client/relay-chain-inprocess-interface/src/lib.rs
+++ b/cumulus/client/relay-chain-inprocess-interface/src/lib.rs
@@ -36,7 +36,7 @@ use sc_client_api::{
 	StorageProof,
 };
 use sc_telemetry::TelemetryWorkerHandle;
-use sp_api::ProvideRuntimeApi;
+use sp_api::{CallApiAt, CallApiAtParams, CallContext, ProvideRuntimeApi};
 use sp_consensus::SyncOracle;
 use sp_core::Pair;
 use sp_state_machine::{Backend as StateBackend, StorageValue};
@@ -180,6 +180,23 @@ impl RelayChainInterface for RelayChainInProcessInterface {
 		Ok(self.backend.blockchain().info().finalized_hash)
 	}
 
+	async fn call_runtime_api(
+		&self,
+		method_name: &'static str,
+		hash: PHash,
+		payload: &[u8],
+	) -> RelayChainResult<Vec<u8>> {
+		Ok(self.full_client.call_api_at(CallApiAtParams {
+			at: hash,
+			function: method_name,
+			arguments: payload.to_vec(),
+			overlayed_changes: &Default::default(),
+			call_context: CallContext::Offchain,
+			recorder: &None,
+			extensions: &Default::default(),
+		})?)
+	}
+
 	async fn is_major_syncing(&self) -> RelayChainResult<bool> {
 		Ok(self.sync_oracle.is_major_syncing())
 	}
diff --git a/cumulus/client/relay-chain-interface/src/lib.rs b/cumulus/client/relay-chain-interface/src/lib.rs
index d02035e84e92f45c4da74f91912ee57abaa083ee..8d172e423eb9303531d807565c1017e74aad2cb0 100644
--- a/cumulus/client/relay-chain-interface/src/lib.rs
+++ b/cumulus/client/relay-chain-interface/src/lib.rs
@@ -22,11 +22,11 @@ use sc_client_api::StorageProof;
 use sp_version::RuntimeVersion;
 
 use async_trait::async_trait;
-use codec::Error as CodecError;
+use codec::{Decode, Encode, Error as CodecError};
 use jsonrpsee_core::ClientError as JsonRpcError;
 use sp_api::ApiError;
 
-use cumulus_primitives_core::relay_chain::BlockId;
+use cumulus_primitives_core::relay_chain::{BlockId, Hash as RelayHash};
 pub use cumulus_primitives_core::{
 	relay_chain::{
 		BlockNumber, CommittedCandidateReceipt, CoreState, Hash as PHash, Header as PHeader,
@@ -117,6 +117,14 @@ pub trait RelayChainInterface: Send + Sync {
 	/// Get the hash of the finalized block.
 	async fn finalized_block_hash(&self) -> RelayChainResult<PHash>;
 
+	/// Call an arbitrary runtime api. The input and output are SCALE-encoded.
+	async fn call_runtime_api(
+		&self,
+		method_name: &'static str,
+		hash: RelayHash,
+		payload: &[u8],
+	) -> RelayChainResult<Vec<u8>>;
+
 	/// Returns the whole contents of the downward message queue for the parachain we are collating
 	/// for.
 	///
@@ -296,6 +304,15 @@ where
 		(**self).finalized_block_hash().await
 	}
 
+	async fn call_runtime_api(
+		&self,
+		method_name: &'static str,
+		hash: RelayHash,
+		payload: &[u8],
+	) -> RelayChainResult<Vec<u8>> {
+		(**self).call_runtime_api(method_name, hash, payload).await
+	}
+
 	async fn is_major_syncing(&self) -> RelayChainResult<bool> {
 		(**self).is_major_syncing().await
 	}
@@ -364,3 +381,19 @@ where
 		(**self).version(relay_parent).await
 	}
 }
+
+/// Helper function to call an arbitrary runtime API using a `RelayChainInterface` client.
+/// Unlike the trait method, this function can be generic, so it handles the encoding of input and
+/// output params.
+pub async fn call_runtime_api<R>(
+	client: &(impl RelayChainInterface + ?Sized),
+	method_name: &'static str,
+	hash: RelayHash,
+	payload: impl Encode,
+) -> RelayChainResult<R>
+where
+	R: Decode,
+{
+	let res = client.call_runtime_api(method_name, hash, &payload.encode()).await?;
+	Decode::decode(&mut &*res).map_err(Into::into)
+}
diff --git a/cumulus/client/relay-chain-rpc-interface/src/lib.rs b/cumulus/client/relay-chain-rpc-interface/src/lib.rs
index 3698938bfd8f6634f7aa26a5f61d340c21c49e0c..77dc1d7318ab2fbc34963d2f6862f4add04f7ccb 100644
--- a/cumulus/client/relay-chain-rpc-interface/src/lib.rs
+++ b/cumulus/client/relay-chain-rpc-interface/src/lib.rs
@@ -165,6 +165,18 @@ impl RelayChainInterface for RelayChainRpcInterface {
 		self.rpc_client.chain_get_finalized_head().await
 	}
 
+	async fn call_runtime_api(
+		&self,
+		method_name: &'static str,
+		hash: RelayHash,
+		payload: &[u8],
+	) -> RelayChainResult<Vec<u8>> {
+		self.rpc_client
+			.call_remote_runtime_function_encoded(method_name, hash, payload)
+			.await
+			.map(|bytes| bytes.to_vec())
+	}
+
 	async fn is_major_syncing(&self) -> RelayChainResult<bool> {
 		self.rpc_client.system_health().await.map(|h| h.is_syncing)
 	}
diff --git a/cumulus/client/relay-chain-rpc-interface/src/rpc_client.rs b/cumulus/client/relay-chain-rpc-interface/src/rpc_client.rs
index 6e282281de69aafdb521c4217ec18e2a5a9c1756..381f4a046a40a7d73e7855f24e9d745fa0eea970 100644
--- a/cumulus/client/relay-chain-rpc-interface/src/rpc_client.rs
+++ b/cumulus/client/relay-chain-rpc-interface/src/rpc_client.rs
@@ -148,15 +148,13 @@ impl RelayChainRpcClient {
 		}
 	}
 
-	/// Call a call to `state_call` rpc method.
-	pub async fn call_remote_runtime_function<R: Decode>(
+	/// Same as `call_remote_runtime_function` but work on encoded data
+	pub async fn call_remote_runtime_function_encoded(
 		&self,
 		method_name: &str,
 		hash: RelayHash,
-		payload: Option<impl Encode>,
-	) -> RelayChainResult<R> {
-		let payload_bytes =
-			payload.map_or(sp_core::Bytes(Vec::new()), |v| sp_core::Bytes(v.encode()));
+		payload_bytes: &[u8],
+	) -> RelayChainResult<sp_core::Bytes> {
 		let params = rpc_params! {
 			method_name,
 			payload_bytes,
@@ -174,6 +172,22 @@ impl RelayChainRpcClient {
 				);
 			})
 			.await?;
+
+		Ok(res)
+	}
+
+	/// Call a call to `state_call` rpc method.
+	pub async fn call_remote_runtime_function<R: Decode>(
+		&self,
+		method_name: &str,
+		hash: RelayHash,
+		payload: Option<impl Encode>,
+	) -> RelayChainResult<R> {
+		let payload_bytes =
+			payload.map_or(sp_core::Bytes(Vec::new()), |v| sp_core::Bytes(v.encode()));
+		let res = self
+			.call_remote_runtime_function_encoded(method_name, hash, &payload_bytes)
+			.await?;
 		Decode::decode(&mut &*res.0).map_err(Into::into)
 	}
 
diff --git a/prdoc/pr_5521.prdoc b/prdoc/pr_5521.prdoc
new file mode 100644
index 0000000000000000000000000000000000000000..564d9df58ceb7ff57f861706f982de2e61331a8c
--- /dev/null
+++ b/prdoc/pr_5521.prdoc
@@ -0,0 +1,24 @@
+# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0
+# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json
+
+title: Allow to call arbitrary runtime apis using RelayChainInterface
+
+doc:
+  - audience: Node Dev
+    description: |
+      This PR adds a `call_runtime_api` method to RelayChainInterface trait, and a separate function also named `call_runtime_api`
+      which allows the caller to specify the input and output types, as opposed to having to encode them.
+
+crates:
+  - name: cumulus-relay-chain-interface
+    bump: patch
+  - name: cumulus-client-consensus-common
+    bump: patch
+  - name: cumulus-client-pov-recovery
+    bump: patch
+  - name: cumulus-client-network
+    bump: patch
+  - name: cumulus-relay-chain-inprocess-interface
+    bump: patch
+  - name: cumulus-relay-chain-rpc-interface
+    bump: patch