Bug: Hubspot api integration



Closes issue https://github.com/juspay/hyperswitch-control-center-cloud/issues/28
diff --git a/Cargo.lock b/Cargo.lock
index f0b8ce94980..d2ae0213703 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2936,6 +2936,7 @@ dependencies = [
  "dyn-clone",
  "error-stack",
  "hex",
+ "http 0.2.12",
  "http-body-util",
  "hyper 0.14.30",
  "hyper-proxy",
@@ -2945,6 +2946,8 @@ dependencies = [
  "masking",
  "once_cell",
  "prost",
+ "quick-xml",
+ "reqwest 0.11.27",
  "router_env",
  "serde",
  "thiserror 1.0.63",
@@ -2953,6 +2956,7 @@ dependencies = [
  "tonic-build",
  "tonic-reflection",
  "tonic-types",
+ "url",
  "vaultrs",
 ]
 
@@ -6531,7 +6535,6 @@ dependencies = [
  "openssl",
  "payment_methods",
  "pm_auth",
- "quick-xml",
  "rand",
  "rand_chacha",
  "rdkafka",
diff --git a/config/config.example.toml b/config/config.example.toml
index e505e9106f0..bd1a1639608 100644
--- a/config/config.example.toml
+++ b/config/config.example.toml
@@ -970,6 +970,13 @@ encryption_manager = "aws_kms" # Encryption manager client to be used
 key_id = "kms_key_id" # The AWS key ID used by the KMS SDK for decrypting data.
 region = "kms_region" # The AWS region used by the KMS SDK for decrypting data.
 
+[crm]
+crm_manager = "hubspot_proxy" # Crm manager client to be used
+
+[crm.hubspot_proxy]
+form_id="hubspot_proxy_form_id"         # Form ID for Hubspot integration
+request_url="hubspot_proxy_request_url" # Request URL for Hubspot API
+
 [opensearch]
 host = "https://localhost:9200"
 enabled = false
diff --git a/config/deployments/env_specific.toml b/config/deployments/env_specific.toml
index 5621a4940e8..7c1866b7794 100644
--- a/config/deployments/env_specific.toml
+++ b/config/deployments/env_specific.toml
@@ -303,6 +303,14 @@ encryption_manager = "aws_kms" # Encryption manager client to be used
 key_id = "kms_key_id" # The AWS key ID used by the KMS SDK for decrypting data.
 region = "kms_region" # The AWS region used by the KMS SDK for decrypting data.
 
+[crm]
+crm_manager = "hubspot_proxy" # Crm manager client to be used
+
+[crm.hubspot_proxy]
+form_id=""     # Form ID for Hubspot integration
+request_url="" # Request URL for Hubspot API
+
+
 [multitenancy]
 enabled = false
 global_tenant = { tenant_id = "global", schema = "public", redis_key_prefix = "", clickhouse_database = "default"}
diff --git a/crates/api_models/src/user/dashboard_metadata.rs b/crates/api_models/src/user/dashboard_metadata.rs
index 8e734d31b53..fe590f6c7d5 100644
--- a/crates/api_models/src/user/dashboard_metadata.rs
+++ b/crates/api_models/src/user/dashboard_metadata.rs
@@ -99,12 +99,13 @@ pub struct ProdIntent {
     pub business_type: Option<String>,
     pub business_identifier: Option<String>,
     pub business_website: Option<String>,
-    pub poc_name: Option<String>,
-    pub poc_contact: Option<String>,
+    pub poc_name: Option<Secret<String>>,
+    pub poc_contact: Option<Secret<String>>,
     pub comments: Option<String>,
     pub is_completed: bool,
     #[serde(default)]
     pub product_type: MerchantProductType,
+    pub business_country_name: Option<String>,
 }
 
 #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs
index d722518154b..1606d731a21 100644
--- a/crates/common_utils/src/consts.rs
+++ b/crates/common_utils/src/consts.rs
@@ -176,3 +176,9 @@ pub const DEFAULT_CARD_TESTING_GUARD_EXPIRY_IN_SECS: i32 = 3600;
 
 /// SOAP 1.1 Envelope Namespace
 pub const SOAP_ENV_NAMESPACE: &str = "http://schemas.xmlsoap.org/soap/envelope/";
+
+/// The tag name used for identifying the host in metrics.
+pub const METRICS_HOST_TAG_NAME: &str = "host";
+
+/// API client request timeout (in seconds)
+pub const REQUEST_TIME_OUT: u64 = 30;
diff --git a/crates/external_services/Cargo.toml b/crates/external_services/Cargo.toml
index ebab30eec70..9fdbbf133ca 100644
--- a/crates/external_services/Cargo.toml
+++ b/crates/external_services/Cargo.toml
@@ -41,6 +41,10 @@ tonic-reflection = { version = "0.12.2", optional = true }
 tonic-types = { version = "0.12.2", optional = true }
 hyper-util = { version = "0.1.9", optional = true }
 http-body-util = { version = "0.1.2", optional = true }
+reqwest = { version = "0.11.27", features = ["rustls-tls"] }
+http = "0.2.12"
+url = { version = "2.5.0", features = ["serde"] }
+quick-xml = { version = "0.31.0", features = ["serialize"] }
 
 
 # First party crates
diff --git a/crates/external_services/src/crm.rs b/crates/external_services/src/crm.rs
new file mode 100644
index 00000000000..ef2982fb380
--- /dev/null
+++ b/crates/external_services/src/crm.rs
@@ -0,0 +1,148 @@
+use std::sync::Arc;
+
+use common_utils::{
+    errors::CustomResult,
+    ext_traits::ConfigExt,
+    request::{Method, Request, RequestBuilder, RequestContent},
+};
+use error_stack::ResultExt;
+use http::header;
+use hyperswitch_interfaces::{
+    crm::{CrmInterface, CrmPayload},
+    errors::HttpClientError,
+    types::Proxy,
+};
+use reqwest;
+use router_env::logger;
+
+use crate::{http_client, hubspot_proxy::HubspotRequest};
+
+/// Hubspot Crm configuration
+#[derive(Debug, Clone, serde::Deserialize)]
+pub struct HubspotProxyConfig {
+    /// The ID of the Hubspot form to be submitted.
+    pub form_id: String,
+
+    /// The URL to which the Hubspot form data will be sent.
+    pub request_url: String,
+}
+
+impl HubspotProxyConfig {
+    /// Validates Hubspot configuration
+    pub(super) fn validate(&self) -> Result<(), InvalidCrmConfig> {
+        use common_utils::fp_utils::when;
+
+        when(self.request_url.is_default_or_empty(), || {
+            Err(InvalidCrmConfig("request url must not be empty"))
+        })?;
+
+        when(self.form_id.is_default_or_empty(), || {
+            Err(InvalidCrmConfig("form_id must not be empty"))
+        })
+    }
+}
+
+/// Error thrown when the crm config is invalid
+#[derive(Debug, Clone)]
+pub struct InvalidCrmConfig(pub &'static str);
+
+impl std::error::Error for InvalidCrmConfig {}
+
+impl std::fmt::Display for InvalidCrmConfig {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "crm: {}", self.0)
+    }
+}
+
+#[derive(Debug, Clone, Copy)]
+/// NoCrm struct
+pub struct NoCrm;
+
+/// Enum representing different Crm configurations
+#[derive(Debug, Clone, Default, serde::Deserialize)]
+#[serde(tag = "crm_manager")]
+#[serde(rename_all = "snake_case")]
+pub enum CrmManagerConfig {
+    /// Hubspot Crm configuration
+    HubspotProxy {
+        /// Hubspot Crm configuration
+        hubspot_proxy: HubspotProxyConfig,
+    },
+
+    /// No Crm configuration
+    #[default]
+    NoCrm,
+}
+
+impl CrmManagerConfig {
+    /// Verifies that the client configuration is usable
+    pub fn validate(&self) -> Result<(), InvalidCrmConfig> {
+        match self {
+            Self::HubspotProxy { hubspot_proxy } => hubspot_proxy.validate(),
+            Self::NoCrm => Ok(()),
+        }
+    }
+
+    /// Retrieves the appropriate Crm client based on the configuration.
+    pub async fn get_crm_client(&self) -> Arc<dyn CrmInterface> {
+        match self {
+            Self::HubspotProxy { hubspot_proxy } => Arc::new(hubspot_proxy.clone()),
+            Self::NoCrm => Arc::new(NoCrm),
+        }
+    }
+}
+
+#[async_trait::async_trait]
+impl CrmInterface for NoCrm {
+    async fn make_body(&self, _details: CrmPayload) -> RequestContent {
+        RequestContent::Json(Box::new(()))
+    }
+
+    async fn make_request(&self, _body: RequestContent, _origin_base_url: String) -> Request {
+        RequestBuilder::default().build()
+    }
+
+    async fn send_request(
+        &self,
+        _proxy: &Proxy,
+        _request: Request,
+    ) -> CustomResult<reqwest::Response, HttpClientError> {
+        logger::info!("No CRM configured!");
+        Err(HttpClientError::UnexpectedState).attach_printable("No CRM configured!")
+    }
+}
+
+#[async_trait::async_trait]
+impl CrmInterface for HubspotProxyConfig {
+    async fn make_body(&self, details: CrmPayload) -> RequestContent {
+        RequestContent::FormUrlEncoded(Box::new(HubspotRequest::new(
+            details.business_country_name.unwrap_or_default(),
+            self.form_id.clone(),
+            details.poc_name.unwrap_or_default(),
+            details.poc_email.clone().unwrap_or_default(),
+            details.legal_business_name.unwrap_or_default(),
+            details.business_website.unwrap_or_default(),
+        )))
+    }
+
+    async fn make_request(&self, body: RequestContent, origin_base_url: String) -> Request {
+        RequestBuilder::new()
+            .method(Method::Post)
+            .url(self.request_url.as_str())
+            .set_body(body)
+            .attach_default_headers()
+            .headers(vec![(
+                header::ORIGIN.to_string(),
+                format!("{origin_base_url}/dashboard").into(),
+            )])
+            .build()
+    }
+
+    async fn send_request(
+        &self,
+        proxy: &Proxy,
+        request: Request,
+    ) -> CustomResult<reqwest::Response, HttpClientError> {
+        http_client::send_request(proxy, request, None).await
+    }
+}
diff --git a/crates/external_services/src/http_client.rs b/crates/external_services/src/http_client.rs
new file mode 100644
index 00000000000..dedeb0c1983
--- /dev/null
+++ b/crates/external_services/src/http_client.rs
@@ -0,0 +1,182 @@
+use common_utils::{consts, errors::CustomResult, request::Request};
+use hyperswitch_interfaces::{errors::HttpClientError, types::Proxy};
+use request::{HeaderExt, RequestBuilderExt};
+use router_env::{instrument, logger, tracing};
+/// client module
+pub mod client;
+/// metrics module
+pub mod metrics;
+/// request module
+pub mod request;
+use std::{error::Error, time::Duration};
+
+use common_utils::request::RequestContent;
+pub use common_utils::request::{ContentType, Method, RequestBuilder};
+use error_stack::ResultExt;
+
+#[allow(missing_docs)]
+#[instrument(skip_all)]
+pub async fn send_request(
+    client_proxy: &Proxy,
+    request: Request,
+    option_timeout_secs: Option<u64>,
+) -> CustomResult<reqwest::Response, HttpClientError> {
+    logger::info!(method=?request.method, headers=?request.headers, payload=?request.body, ?request);
+
+    let url = url::Url::parse(&request.url).change_context(HttpClientError::UrlParsingFailed)?;
+
+    let client = client::create_client(client_proxy, request.certificate, request.certificate_key)?;
+
+    let headers = request.headers.construct_header_map()?;
+    let metrics_tag = router_env::metric_attributes!((
+        consts::METRICS_HOST_TAG_NAME,
+        url.host_str().unwrap_or_default().to_owned()
+    ));
+    let request = {
+        match request.method {
+            Method::Get => client.get(url),
+            Method::Post => {
+                let client = client.post(url);
+                match request.body {
+                    Some(RequestContent::Json(payload)) => client.json(&payload),
+                    Some(RequestContent::FormData(form)) => client.multipart(form),
+                    Some(RequestContent::FormUrlEncoded(payload)) => client.form(&payload),
+                    Some(RequestContent::Xml(payload)) => {
+                        let body = quick_xml::se::to_string(&payload)
+                            .change_context(HttpClientError::BodySerializationFailed)?;
+                        client.body(body).header("Content-Type", "application/xml")
+                    }
+                    Some(RequestContent::RawBytes(payload)) => client.body(payload),
+                    None => client,
+                }
+            }
+            Method::Put => {
+                let client = client.put(url);
+                match request.body {
+                    Some(RequestContent::Json(payload)) => client.json(&payload),
+                    Some(RequestContent::FormData(form)) => client.multipart(form),
+                    Some(RequestContent::FormUrlEncoded(payload)) => client.form(&payload),
+                    Some(RequestContent::Xml(payload)) => {
+                        let body = quick_xml::se::to_string(&payload)
+                            .change_context(HttpClientError::BodySerializationFailed)?;
+                        client.body(body).header("Content-Type", "application/xml")
+                    }
+                    Some(RequestContent::RawBytes(payload)) => client.body(payload),
+                    None => client,
+                }
+            }
+            Method::Patch => {
+                let client = client.patch(url);
+                match request.body {
+                    Some(RequestContent::Json(payload)) => client.json(&payload),
+                    Some(RequestContent::FormData(form)) => client.multipart(form),
+                    Some(RequestContent::FormUrlEncoded(payload)) => client.form(&payload),
+                    Some(RequestContent::Xml(payload)) => {
+                        let body = quick_xml::se::to_string(&payload)
+                            .change_context(HttpClientError::BodySerializationFailed)?;
+                        client.body(body).header("Content-Type", "application/xml")
+                    }
+                    Some(RequestContent::RawBytes(payload)) => client.body(payload),
+                    None => client,
+                }
+            }
+            Method::Delete => client.delete(url),
+        }
+        .add_headers(headers)
+        .timeout(Duration::from_secs(
+            option_timeout_secs.unwrap_or(consts::REQUEST_TIME_OUT),
+        ))
+    };
+
+    // We cannot clone the request type, because it has Form trait which is not cloneable. So we are cloning the request builder here.
+    let cloned_send_request = request.try_clone().map(|cloned_request| async {
+        cloned_request
+            .send()
+            .await
+            .map_err(|error| match error {
+                error if error.is_timeout() => {
+                    metrics::REQUEST_BUILD_FAILURE.add(1, metrics_tag);
+                    HttpClientError::RequestTimeoutReceived
+                }
+                error if is_connection_closed_before_message_could_complete(&error) => {
+                    metrics::REQUEST_BUILD_FAILURE.add(1, metrics_tag);
+                    HttpClientError::ConnectionClosedIncompleteMessage
+                }
+                _ => HttpClientError::RequestNotSent(error.to_string()),
+            })
+            .attach_printable("Unable to send request to connector")
+    });
+
+    let send_request = async {
+        request
+            .send()
+            .await
+            .map_err(|error| match error {
+                error if error.is_timeout() => {
+                    metrics::REQUEST_BUILD_FAILURE.add(1, metrics_tag);
+                    HttpClientError::RequestTimeoutReceived
+                }
+                error if is_connection_closed_before_message_could_complete(&error) => {
+                    metrics::REQUEST_BUILD_FAILURE.add(1, metrics_tag);
+                    HttpClientError::ConnectionClosedIncompleteMessage
+                }
+                _ => HttpClientError::RequestNotSent(error.to_string()),
+            })
+            .attach_printable("Unable to send request to connector")
+    };
+
+    let response = common_utils::metrics::utils::record_operation_time(
+        send_request,
+        &metrics::EXTERNAL_REQUEST_TIME,
+        metrics_tag,
+    )
+    .await;
+    // Retry once if the response is connection closed.
+    //
+    // This is just due to the racy nature of networking.
+    // hyper has a connection pool of idle connections, and it selected one to send your request.
+    // Most of the time, hyper will receive the server’s FIN and drop the dead connection from its pool.
+    // But occasionally, a connection will be selected from the pool
+    // and written to at the same time the server is deciding to close the connection.
+    // Since hyper already wrote some of the request,
+    // it can’t really retry it automatically on a new connection, since the server may have acted already
+    match response {
+        Ok(response) => Ok(response),
+        Err(error)
+            if error.current_context() == &HttpClientError::ConnectionClosedIncompleteMessage =>
+        {
+            metrics::AUTO_RETRY_CONNECTION_CLOSED.add(1, metrics_tag);
+            match cloned_send_request {
+                Some(cloned_request) => {
+                    logger::info!(
+                        "Retrying request due to connection closed before message could complete"
+                    );
+                    common_utils::metrics::utils::record_operation_time(
+                        cloned_request,
+                        &metrics::EXTERNAL_REQUEST_TIME,
+                        metrics_tag,
+                    )
+                    .await
+                }
+                None => {
+                    logger::info!("Retrying request due to connection closed before message could complete failed as request is not cloneable");
+                    Err(error)
+                }
+            }
+        }
+        err @ Err(_) => err,
+    }
+}
+
+fn is_connection_closed_before_message_could_complete(error: &reqwest::Error) -> bool {
+    let mut source = error.source();
+    while let Some(err) = source {
+        if let Some(hyper_err) = err.downcast_ref::<hyper::Error>() {
+            if hyper_err.is_incomplete_message() {
+                return true;
+            }
+        }
+        source = err.source();
+    }
+    false
+}
diff --git a/crates/external_services/src/http_client/client.rs b/crates/external_services/src/http_client/client.rs
new file mode 100644
index 00000000000..0696c9e76b9
--- /dev/null
+++ b/crates/external_services/src/http_client/client.rs
@@ -0,0 +1,131 @@
+use std::time::Duration;
+
+use base64::Engine;
+use common_utils::consts::BASE64_ENGINE;
+pub use common_utils::errors::CustomResult;
+use error_stack::ResultExt;
+use hyperswitch_interfaces::{errors::HttpClientError, types::Proxy};
+use masking::ExposeInterface;
+use once_cell::sync::OnceCell;
+
+static DEFAULT_CLIENT: OnceCell<reqwest::Client> = OnceCell::new();
+
+// We may need to use outbound proxy to connect to external world.
+// Precedence will be the environment variables, followed by the config.
+#[allow(missing_docs)]
+pub fn create_client(
+    proxy_config: &Proxy,
+    client_certificate: Option<masking::Secret<String>>,
+    client_certificate_key: Option<masking::Secret<String>>,
+) -> CustomResult<reqwest::Client, HttpClientError> {
+    match (client_certificate, client_certificate_key) {
+        (Some(encoded_certificate), Some(encoded_certificate_key)) => {
+            let client_builder = get_client_builder(proxy_config)?;
+
+            let identity = create_identity_from_certificate_and_key(
+                encoded_certificate.clone(),
+                encoded_certificate_key,
+            )?;
+            let certificate_list = create_certificate(encoded_certificate)?;
+            let client_builder = certificate_list
+                .into_iter()
+                .fold(client_builder, |client_builder, certificate| {
+                    client_builder.add_root_certificate(certificate)
+                });
+            client_builder
+                .identity(identity)
+                .use_rustls_tls()
+                .build()
+                .change_context(HttpClientError::ClientConstructionFailed)
+                .attach_printable("Failed to construct client with certificate and certificate key")
+        }
+        _ => get_base_client(proxy_config),
+    }
+}
+
+#[allow(missing_docs)]
+pub fn get_client_builder(
+    proxy_config: &Proxy,
+) -> CustomResult<reqwest::ClientBuilder, HttpClientError> {
+    let mut client_builder = reqwest::Client::builder()
+        .redirect(reqwest::redirect::Policy::none())
+        .pool_idle_timeout(Duration::from_secs(
+            proxy_config
+                .idle_pool_connection_timeout
+                .unwrap_or_default(),
+        ));
+
+    let proxy_exclusion_config =
+        reqwest::NoProxy::from_string(&proxy_config.bypass_proxy_hosts.clone().unwrap_or_default());
+
+    // Proxy all HTTPS traffic through the configured HTTPS proxy
+    if let Some(url) = proxy_config.https_url.as_ref() {
+        client_builder = client_builder.proxy(
+            reqwest::Proxy::https(url)
+                .change_context(HttpClientError::InvalidProxyConfiguration)
+                .attach_printable("HTTPS proxy configuration error")?
+                .no_proxy(proxy_exclusion_config.clone()),
+        );
+    }
+
+    // Proxy all HTTP traffic through the configured HTTP proxy
+    if let Some(url) = proxy_config.http_url.as_ref() {
+        client_builder = client_builder.proxy(
+            reqwest::Proxy::http(url)
+                .change_context(HttpClientError::InvalidProxyConfiguration)
+                .attach_printable("HTTP proxy configuration error")?
+                .no_proxy(proxy_exclusion_config),
+        );
+    }
+
+    Ok(client_builder)
+}
+
+#[allow(missing_docs)]
+pub fn create_identity_from_certificate_and_key(
+    encoded_certificate: masking::Secret<String>,
+    encoded_certificate_key: masking::Secret<String>,
+) -> Result<reqwest::Identity, error_stack::Report<HttpClientError>> {
+    let decoded_certificate = BASE64_ENGINE
+        .decode(encoded_certificate.expose())
+        .change_context(HttpClientError::CertificateDecodeFailed)?;
+
+    let decoded_certificate_key = BASE64_ENGINE
+        .decode(encoded_certificate_key.expose())
+        .change_context(HttpClientError::CertificateDecodeFailed)?;
+
+    let certificate = String::from_utf8(decoded_certificate)
+        .change_context(HttpClientError::CertificateDecodeFailed)?;
+
+    let certificate_key = String::from_utf8(decoded_certificate_key)
+        .change_context(HttpClientError::CertificateDecodeFailed)?;
+
+    let key_chain = format!("{}{}", certificate_key, certificate);
+    reqwest::Identity::from_pem(key_chain.as_bytes())
+        .change_context(HttpClientError::CertificateDecodeFailed)
+}
+
+#[allow(missing_docs)]
+pub fn create_certificate(
+    encoded_certificate: masking::Secret<String>,
+) -> Result<Vec<reqwest::Certificate>, error_stack::Report<HttpClientError>> {
+    let decoded_certificate = BASE64_ENGINE
+        .decode(encoded_certificate.expose())
+        .change_context(HttpClientError::CertificateDecodeFailed)?;
+
+    let certificate = String::from_utf8(decoded_certificate)
+        .change_context(HttpClientError::CertificateDecodeFailed)?;
+    reqwest::Certificate::from_pem_bundle(certificate.as_bytes())
+        .change_context(HttpClientError::CertificateDecodeFailed)
+}
+
+fn get_base_client(proxy_config: &Proxy) -> CustomResult<reqwest::Client, HttpClientError> {
+    Ok(DEFAULT_CLIENT
+        .get_or_try_init(|| {
+            get_client_builder(proxy_config)?
+                .build()
+                .change_context(HttpClientError::ClientConstructionFailed)
+                .attach_printable("Failed to construct base client")
+        })?
+        .clone())
+}
diff --git a/crates/external_services/src/http_client/metrics.rs b/crates/external_services/src/http_client/metrics.rs
new file mode 100644
index 00000000000..5b6f48334f2
--- /dev/null
+++ b/crates/external_services/src/http_client/metrics.rs
@@ -0,0 +1,9 @@
+use router_env::{counter_metric, global_meter, histogram_metric_f64};
+
+global_meter!(GLOBAL_METER, "ROUTER_API");
+
+counter_metric!(REQUEST_BUILD_FAILURE, GLOBAL_METER);
+
+histogram_metric_f64!(EXTERNAL_REQUEST_TIME, GLOBAL_METER);
+
+counter_metric!(AUTO_RETRY_CONNECTION_CLOSED, GLOBAL_METER);
diff --git a/crates/external_services/src/http_client/request.rs b/crates/external_services/src/http_client/request.rs
new file mode 100644
index 00000000000..cf2727f0668
--- /dev/null
+++ b/crates/external_services/src/http_client/request.rs
@@ -0,0 +1,45 @@
+use std::str::FromStr;
+
+use common_utils::request::Headers;
+pub use common_utils::{errors::CustomResult, request::ContentType};
+use error_stack::ResultExt;
+use hyperswitch_interfaces::errors::HttpClientError;
+pub use masking::{Mask, Maskable};
+use router_env::{instrument, tracing};
+
+#[allow(missing_docs)]
+pub trait HeaderExt {
+    fn construct_header_map(self) -> CustomResult<reqwest::header::HeaderMap, HttpClientError>;
+}
+
+impl HeaderExt for Headers {
+    fn construct_header_map(self) -> CustomResult<reqwest::header::HeaderMap, HttpClientError> {
+        use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
+
+        self.into_iter().try_fold(
+            HeaderMap::new(),
+            |mut header_map, (header_name, header_value)| {
+                let header_name = HeaderName::from_str(&header_name)
+                    .change_context(HttpClientError::HeaderMapConstructionFailed)?;
+                let header_value = header_value.into_inner();
+                let header_value = HeaderValue::from_str(&header_value)
+                    .change_context(HttpClientError::HeaderMapConstructionFailed)?;
+                header_map.append(header_name, header_value);
+                Ok(header_map)
+            },
+        )
+    }
+}
+
+#[allow(missing_docs)]
+pub trait RequestBuilderExt {
+    fn add_headers(self, headers: reqwest::header::HeaderMap) -> Self;
+}
+
+impl RequestBuilderExt for reqwest::RequestBuilder {
+    #[instrument(skip_all)]
+    fn add_headers(mut self, headers: reqwest::header::HeaderMap) -> Self {
+        self = self.headers(headers);
+        self
+    }
+}
diff --git a/crates/external_services/src/hubspot_proxy.rs b/crates/external_services/src/hubspot_proxy.rs
new file mode 100644
index 00000000000..17817676410
--- /dev/null
+++ b/crates/external_services/src/hubspot_proxy.rs
@@ -0,0 +1,78 @@
+use masking::Secret;
+
+/// Lead source constant for Hubspot
+pub const HUBSPOT_LEAD_SOURCE: &str = "Hyperswitch Dashboard";
+
+/// Struct representing a request to Hubspot
+#[derive(Clone, Debug, serde::Serialize, Default)]
+pub struct HubspotRequest {
+    /// Indicates whether Hubspot should be used.
+    #[serde(rename = "useHubspot")]
+    pub use_hubspot: bool,
+
+    /// The country of the user or company.
+    pub country: String,
+
+    /// The ID of the Hubspot form being submitted.
+    #[serde(rename = "hubspotFormId")]
+    pub hubspot_form_id: String,
+
+    /// The first name of the user.
+    pub firstname: Secret<String>,
+
+    /// The last name of the user.
+    pub lastname: Secret<String>,
+
+    /// The email address of the user.
+    pub email: Secret<String>,
+
+    /// The name of the company.
+    #[serde(rename = "companyName")]
+    pub company_name: String,
+
+    /// The source of the lead, typically set to "Hyperswitch Dashboard".
+    pub lead_source: String,
+
+    /// The website URL of the company.
+    pub website: String,
+
+    /// The phone number of the user.
+    pub phone: Secret<String>,
+
+    /// The role or designation of the user.
+    pub role: String,
+
+    /// The monthly GMV (Gross Merchandise Value) of the company.
+    #[serde(rename = "monthlyGMV")]
+    pub monthly_gmv: String,
+
+    /// Notes from the business development team.
+    pub bd_notes: String,
+
+    /// Additional message or comments.
+    pub message: String,
+}
+
+#[allow(missing_docs)]
+impl HubspotRequest {
+    pub fn new(
+        country: String,
+        hubspot_form_id: String,
+        firstname: Secret<String>,
+        email: Secret<String>,
+        company_name: String,
+        website: String,
+    ) -> Self {
+        Self {
+            use_hubspot: true,
+            country,
+            hubspot_form_id,
+            firstname,
+            email,
+            company_name,
+            lead_source: HUBSPOT_LEAD_SOURCE.to_string(),
+            website,
+            ..Default::default()
+        }
+    }
+}
diff --git a/crates/external_services/src/lib.rs b/crates/external_services/src/lib.rs
index 304ce248317..f3b1b330e16 100644
--- a/crates/external_services/src/lib.rs
+++ b/crates/external_services/src/lib.rs
@@ -17,8 +17,17 @@ pub mod no_encryption;
 /// Building grpc clients to communicate with the server
 pub mod grpc_client;
 
+/// http_client module
+pub mod http_client;
+
+/// hubspot_proxy module
+pub mod hubspot_proxy;
+
 pub mod managers;
 
+/// crm module
+pub mod crm;
+
 /// Crate specific constants
 #[cfg(feature = "aws_kms")]
 pub mod consts {
diff --git a/crates/hyperswitch_interfaces/src/crm.rs b/crates/hyperswitch_interfaces/src/crm.rs
new file mode 100644
index 00000000000..40eb56e9e6b
--- /dev/null
+++ b/crates/hyperswitch_interfaces/src/crm.rs
@@ -0,0 +1,69 @@
+use common_enums::CountryAlpha2;
+use common_utils::{
+    errors::CustomResult,
+    request::{Request, RequestContent},
+};
+use masking::Secret;
+
+use super::types::Proxy;
+use crate::errors::HttpClientError;
+
+/// Crm Payload structure
+#[derive(Clone, Debug, serde::Serialize, Default)]
+pub struct CrmPayload {
+    /// The legal name of the business.
+    pub legal_business_name: Option<String>,
+
+    /// A label or tag associated with the business.
+    pub business_label: Option<String>,
+
+    /// The location of the business, represented as a country code (ISO Alpha-2 format).
+    pub business_location: Option<CountryAlpha2>,
+
+    /// The display name of the business.
+    pub display_name: Option<String>,
+
+    /// The email address of the point of contact (POC) for the business.
+    pub poc_email: Option<Secret<String>>,
+
+    /// The type of business (e.g., LLC, Corporation, etc.).
+    pub business_type: Option<String>,
+
+    /// A unique identifier for the business.
+    pub business_identifier: Option<String>,
+
+    /// The website URL of the business.
+    pub business_website: Option<String>,
+
+    /// The name of the point of contact (POC) for the business.
+    pub poc_name: Option<Secret<String>>,
+
+    /// The contact number of the point of contact (POC) for the business.
+    pub poc_contact: Option<Secret<String>>,
+
+    /// Additional comments or notes about the business.
+    pub comments: Option<String>,
+
+    /// Indicates whether the Crm process for the business is completed.
+    pub is_completed: bool,
+
+    /// The name of the country where the business is located.
+    pub business_country_name: Option<String>,
+}
+
+/// Trait defining the interface for encryption management
+#[async_trait::async_trait]
+pub trait CrmInterface: Send + Sync {
+    /// Make body for the request
+    async fn make_body(&self, details: CrmPayload) -> RequestContent;
+
+    /// Encrypt the given input data
+    async fn make_request(&self, body: RequestContent, origin_base_url: String) -> Request;
+
+    /// Decrypt the given input data
+    async fn send_request(
+        &self,
+        proxy: &Proxy,
+        request: Request,
+    ) -> CustomResult<reqwest::Response, HttpClientError>;
+}
diff --git a/crates/hyperswitch_interfaces/src/errors.rs b/crates/hyperswitch_interfaces/src/errors.rs
index 466186ae368..1a11c2d761d 100644
--- a/crates/hyperswitch_interfaces/src/errors.rs
+++ b/crates/hyperswitch_interfaces/src/errors.rs
@@ -1,5 +1,6 @@
 //! Errors interface
 
+use common_enums::ApiClientError;
 use common_utils::errors::ErrorSwitch;
 use hyperswitch_domain_models::errors::api_error_response::ApiErrorResponse;
 
@@ -156,3 +157,73 @@ impl ErrorSwitch<ApiErrorResponse> for ConnectorError {
         }
     }
 }
+
+// http client errors
+#[allow(missing_docs, missing_debug_implementations)]
+#[derive(Debug, Clone, thiserror::Error, PartialEq)]
+pub enum HttpClientError {
+    #[error("Header map construction failed")]
+    HeaderMapConstructionFailed,
+    #[error("Invalid proxy configuration")]
+    InvalidProxyConfiguration,
+    #[error("Client construction failed")]
+    ClientConstructionFailed,
+    #[error("Certificate decode failed")]
+    CertificateDecodeFailed,
+    #[error("Request body serialization failed")]
+    BodySerializationFailed,
+    #[error("Unexpected state reached/Invariants conflicted")]
+    UnexpectedState,
+
+    #[error("Failed to parse URL")]
+    UrlParsingFailed,
+    #[error("URL encoding of request payload failed")]
+    UrlEncodingFailed,
+    #[error("Failed to send request to connector {0}")]
+    RequestNotSent(String),
+    #[error("Failed to decode response")]
+    ResponseDecodingFailed,
+
+    #[error("Server responded with Request Timeout")]
+    RequestTimeoutReceived,
+
+    #[error("connection closed before a message could complete")]
+    ConnectionClosedIncompleteMessage,
+
+    #[error("Server responded with Internal Server Error")]
+    InternalServerErrorReceived,
+    #[error("Server responded with Bad Gateway")]
+    BadGatewayReceived,
+    #[error("Server responded with Service Unavailable")]
+    ServiceUnavailableReceived,
+    #[error("Server responded with Gateway Timeout")]
+    GatewayTimeoutReceived,
+    #[error("Server responded with unexpected response")]
+    UnexpectedServerResponse,
+}
+
+impl ErrorSwitch<ApiClientError> for HttpClientError {
+    fn switch(&self) -> ApiClientError {
+        match self {
+            Self::HeaderMapConstructionFailed => ApiClientError::HeaderMapConstructionFailed,
+            Self::InvalidProxyConfiguration => ApiClientError::InvalidProxyConfiguration,
+            Self::ClientConstructionFailed => ApiClientError::ClientConstructionFailed,
+            Self::CertificateDecodeFailed => ApiClientError::CertificateDecodeFailed,
+            Self::BodySerializationFailed => ApiClientError::BodySerializationFailed,
+            Self::UnexpectedState => ApiClientError::UnexpectedState,
+            Self::UrlParsingFailed => ApiClientError::UrlParsingFailed,
+            Self::UrlEncodingFailed => ApiClientError::UrlEncodingFailed,
+            Self::RequestNotSent(reason) => ApiClientError::RequestNotSent(reason.clone()),
+            Self::ResponseDecodingFailed => ApiClientError::ResponseDecodingFailed,
+            Self::RequestTimeoutReceived => ApiClientError::RequestTimeoutReceived,
+            Self::ConnectionClosedIncompleteMessage => {
+                ApiClientError::ConnectionClosedIncompleteMessage
+            }
+            Self::InternalServerErrorReceived => ApiClientError::InternalServerErrorReceived,
+            Self::BadGatewayReceived => ApiClientError::BadGatewayReceived,
+            Self::ServiceUnavailableReceived => ApiClientError::ServiceUnavailableReceived,
+            Self::GatewayTimeoutReceived => ApiClientError::GatewayTimeoutReceived,
+            Self::UnexpectedServerResponse => ApiClientError::UnexpectedServerResponse,
+        }
+    }
+}
diff --git a/crates/hyperswitch_interfaces/src/lib.rs b/crates/hyperswitch_interfaces/src/lib.rs
index a9eb2997c89..b9c16f6d82f 100644
--- a/crates/hyperswitch_interfaces/src/lib.rs
+++ b/crates/hyperswitch_interfaces/src/lib.rs
@@ -23,3 +23,6 @@ pub mod metrics;
 pub mod secrets_interface;
 pub mod types;
 pub mod webhooks;
+
+/// Crm interface
+pub mod crm;
diff --git a/crates/hyperswitch_interfaces/src/types.rs b/crates/hyperswitch_interfaces/src/types.rs
index 27f50609da2..e41e8cd9bcf 100644
--- a/crates/hyperswitch_interfaces/src/types.rs
+++ b/crates/hyperswitch_interfaces/src/types.rs
@@ -282,3 +282,31 @@ pub type BillingConnectorInvoiceSyncTypeV2 = dyn ConnectorIntegrationV2<
     BillingConnectorInvoiceSyncRequest,
     BillingConnectorInvoiceSyncResponse,
 >;
+
+/// Proxy configuration structure
+#[derive(Debug, serde::Deserialize, Clone)]
+#[serde(default)]
+pub struct Proxy {
+    /// The URL of the HTTP proxy server.
+    pub http_url: Option<String>,
+
+    /// The URL of the HTTPS proxy server.
+    pub https_url: Option<String>,
+
+    /// The timeout duration (in seconds) for idle connections in the proxy pool.
+    pub idle_pool_connection_timeout: Option<u64>,
+
+    /// A comma-separated list of hosts that should bypass the proxy.
+    pub bypass_proxy_hosts: Option<String>,
+}
+
+impl Default for Proxy {
+    fn default() -> Self {
+        Self {
+            http_url: Default::default(),
+            https_url: Default::default(),
+            idle_pool_connection_timeout: Some(90),
+            bypass_proxy_hosts: Default::default(),
+        }
+    }
+}
diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml
index 30b520eb58b..5395e538c29 100644
--- a/crates/router/Cargo.toml
+++ b/crates/router/Cargo.toml
@@ -85,7 +85,6 @@ num-traits = "0.2.19"
 once_cell = "1.19.0"
 openidconnect = "3.5.0"                                                                                                      # TODO: remove reqwest
 openssl = "0.10.70"
-quick-xml = { version = "0.31.0", features = ["serialize"] }
 rand = "0.8.5"
 rand_chacha = "0.3.1"
 rdkafka = "0.36.2"
diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs
index ac3a446b01f..dee7dfbb03c 100644
--- a/crates/router/src/configs/defaults.rs
+++ b/crates/router/src/configs/defaults.rs
@@ -49,18 +49,6 @@ impl Default for super::settings::Database {
         }
     }
 }
-
-impl Default for super::settings::Proxy {
-    fn default() -> Self {
-        Self {
-            http_url: Default::default(),
-            https_url: Default::default(),
-            idle_pool_connection_timeout: Some(90),
-            bypass_proxy_hosts: Default::default(),
-        }
-    }
-}
-
 impl Default for super::settings::Locker {
     fn default() -> Self {
         Self {
diff --git a/crates/router/src/configs/secrets_transformers.rs b/crates/router/src/configs/secrets_transformers.rs
index 3ae171545bc..308cbe417bb 100644
--- a/crates/router/src/configs/secrets_transformers.rs
+++ b/crates/router/src/configs/secrets_transformers.rs
@@ -525,6 +525,7 @@ pub(crate) async fn fetch_raw_secrets(
         decision: conf.decision,
         locker_based_open_banking_connectors: conf.locker_based_open_banking_connectors,
         grpc_client: conf.grpc_client,
+        crm: conf.crm,
         #[cfg(feature = "v2")]
         cell_information: conf.cell_information,
         network_tokenization_supported_card_networks: conf
diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs
index b0fd269bf79..f7120ff4545 100644
--- a/crates/router/src/configs/settings.rs
+++ b/crates/router/src/configs/settings.rs
@@ -13,6 +13,7 @@ use error_stack::ResultExt;
 #[cfg(feature = "email")]
 use external_services::email::EmailSettings;
 use external_services::{
+    crm::CrmManagerConfig,
     file_storage::FileStorageConfig,
     grpc_client::GrpcClientSettings,
     managers::{
@@ -21,8 +22,11 @@ use external_services::{
     },
 };
 pub use hyperswitch_interfaces::configs::Connectors;
-use hyperswitch_interfaces::secrets_interface::secret_state::{
-    RawSecret, SecretState, SecretStateContainer, SecuredSecret,
+use hyperswitch_interfaces::{
+    secrets_interface::secret_state::{
+        RawSecret, SecretState, SecretStateContainer, SecuredSecret,
+    },
+    types::Proxy,
 };
 use masking::Secret;
 pub use payment_methods::configs::settings::{
@@ -96,6 +100,7 @@ pub struct Settings<S: SecretState> {
     #[cfg(feature = "email")]
     pub email: EmailSettings,
     pub user: UserSettings,
+    pub crm: CrmManagerConfig,
     pub cors: CorsSettings,
     pub mandates: Mandates,
     pub zero_mandates: ZeroMandates,
@@ -714,15 +719,6 @@ pub struct Jwekey {
     pub tunnel_private_key: Secret<String>,
 }
 
-#[derive(Debug, Deserialize, Clone)]
-#[serde(default)]
-pub struct Proxy {
-    pub http_url: Option<String>,
-    pub https_url: Option<String>,
-    pub idle_pool_connection_timeout: Option<u64>,
-    pub bypass_proxy_hosts: Option<String>,
-}
-
 #[derive(Debug, Deserialize, Clone)]
 #[serde(default)]
 pub struct Server {
@@ -988,6 +984,10 @@ impl Settings<SecuredSecret> {
             .validate()
             .map_err(|err| ApplicationError::InvalidConfigurationValueError(err.to_string()))?;
 
+        self.crm
+            .validate()
+            .map_err(|err| ApplicationError::InvalidConfigurationValueError(err.to_string()))?;
+
         self.lock_settings.validate()?;
         self.events.validate()?;
 
diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs
index 43041ef01b6..f728e6aedc8 100644
--- a/crates/router/src/consts.rs
+++ b/crates/router/src/consts.rs
@@ -54,7 +54,6 @@ pub(crate) const API_KEY_LENGTH: usize = 64;
 // OID (Object Identifier) for the merchant ID field extension.
 pub(crate) const MERCHANT_ID_FIELD_EXTENSION_ID: &str = "1.2.840.113635.100.6.32";
 
-pub(crate) const METRICS_HOST_TAG_NAME: &str = "host";
 pub const MAX_ROUTING_CONFIGS_PER_MERCHANT: usize = 100;
 pub const ROUTING_CONFIG_ID_LENGTH: usize = 10;
 
diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs
index 27c8a9fb8ff..694bfe08cb9 100644
--- a/crates/router/src/core/admin.rs
+++ b/crates/router/src/core/admin.rs
@@ -15,6 +15,7 @@ use diesel_models::configs;
 #[cfg(all(any(feature = "v1", feature = "v2"), feature = "olap"))]
 use diesel_models::{business_profile::CardTestingGuardConfig, organization::OrganizationBridge};
 use error_stack::{report, FutureExt, ResultExt};
+use external_services::http_client::client;
 use hyperswitch_domain_models::merchant_connector_account::{
     FromRequestEncryptableMerchantConnectorAccount, UpdateEncryptableMerchantConnectorAccount,
 };
@@ -39,7 +40,7 @@ use crate::{
     routes::{metrics, SessionState},
     services::{
         self,
-        api::{self as service_api, client},
+        api::{self as service_api},
         authentication, pm_auth as payment_initiation_service,
     },
     types::{
diff --git a/crates/router/src/core/connector_onboarding/paypal.rs b/crates/router/src/core/connector_onboarding/paypal.rs
index 98aef1e3064..407f62f1ec9 100644
--- a/crates/router/src/core/connector_onboarding/paypal.rs
+++ b/crates/router/src/core/connector_onboarding/paypal.rs
@@ -1,6 +1,7 @@
 use api_models::{admin::MerchantConnectorUpdate, connector_onboarding as api};
 use common_utils::ext_traits::Encode;
 use error_stack::ResultExt;
+pub use external_services::http_client;
 use masking::{ExposeInterface, PeekInterface, Secret};
 
 use crate::{
@@ -8,7 +9,7 @@ use crate::{
         admin,
         errors::{ApiErrorResponse, RouterResult},
     },
-    services::{send_request, ApplicationResponse, Request},
+    services::{ApplicationResponse, Request},
     types::{self as oss_types, api as oss_api_types, api::connector_onboarding as types},
     utils::connector_onboarding as utils,
     SessionState,
@@ -47,7 +48,7 @@ pub async fn get_action_url_from_paypal(
         return_url,
     ))
     .await?;
-    let referral_response = send_request(&state, referral_request, None)
+    let referral_response = http_client::send_request(&state.conf.proxy, referral_request, None)
         .await
         .change_context(ApiErrorResponse::InternalServerError)
         .attach_printable("Failed to send request to paypal referrals")?;
@@ -97,10 +98,11 @@ pub async fn sync_merchant_onboarding_status(
     let merchant_details_request =
         utils::paypal::build_paypal_get_request(merchant_details_url, access_token.token.expose())?;
 
-    let merchant_details_response = send_request(&state, merchant_details_request, None)
-        .await
-        .change_context(ApiErrorResponse::InternalServerError)
-        .attach_printable("Failed to send request to paypal merchant details")?;
+    let merchant_details_response =
+        http_client::send_request(&state.conf.proxy, merchant_details_request, None)
+            .await
+            .change_context(ApiErrorResponse::InternalServerError)
+            .attach_printable("Failed to send request to paypal merchant details")?;
 
     let parsed_response: types::paypal::SellerStatusDetailsResponse = merchant_details_response
         .json()
@@ -121,10 +123,11 @@ async fn find_paypal_merchant_by_tracking_id(
         merchant_onboarding_status_url(state.clone(), tracking_id),
         access_token.token.peek().to_string(),
     )?;
-    let seller_status_response = send_request(&state, seller_status_request, None)
-        .await
-        .change_context(ApiErrorResponse::InternalServerError)
-        .attach_printable("Failed to send request to paypal onboarding status")?;
+    let seller_status_response =
+        http_client::send_request(&state.conf.proxy, seller_status_request, None)
+            .await
+            .change_context(ApiErrorResponse::InternalServerError)
+            .attach_printable("Failed to send request to paypal onboarding status")?;
 
     if seller_status_response.status().is_success() {
         return Ok(Some(
diff --git a/crates/router/src/core/recon.rs b/crates/router/src/core/recon.rs
index 17e86dc41ca..16418ad83a9 100644
--- a/crates/router/src/core/recon.rs
+++ b/crates/router/src/core/recon.rs
@@ -17,6 +17,7 @@ use crate::{
         storage,
         transformers::ForeignTryFrom,
     },
+    utils::user as user_utils,
     SessionState,
 };
 
@@ -80,7 +81,7 @@ pub async fn send_recon_request(
         state
             .email_client
             .compose_and_send_email(
-                email_types::get_base_url(&state),
+                user_utils::get_base_url(&state),
                 Box::new(email_contents),
                 state.conf.proxy.https_url.as_ref(),
             )
@@ -211,7 +212,7 @@ pub async fn recon_merchant_account_update(
             let _ = state
                 .email_client
                 .compose_and_send_email(
-                    email_types::get_base_url(&state),
+                    user_utils::get_base_url(&state),
                     Box::new(email_contents),
                     state.conf.proxy.https_url.as_ref(),
                 )
diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs
index 9802edfd346..1097f57b198 100644
--- a/crates/router/src/core/user.rs
+++ b/crates/router/src/core/user.rs
@@ -32,8 +32,6 @@ use user_api::dashboard_metadata::SetMetaDataRequest;
 #[cfg(feature = "v1")]
 use super::admin;
 use super::errors::{StorageErrorExt, UserErrors, UserResponse, UserResult};
-#[cfg(feature = "email")]
-use crate::services::email::types as email_types;
 #[cfg(feature = "v1")]
 use crate::types::transformers::ForeignFrom;
 use crate::{
@@ -51,6 +49,8 @@ use crate::{
         user::{theme as theme_utils, two_factor_auth as tfa_utils},
     },
 };
+#[cfg(feature = "email")]
+use crate::{services::email::types as email_types, utils::user as user_utils};
 
 pub mod dashboard_metadata;
 #[cfg(feature = "dummy_connector")]
@@ -99,7 +99,7 @@ pub async fn signup_with_merchant_id(
     let send_email_result = state
         .email_client
         .compose_and_send_email(
-            email_types::get_base_url(&state),
+            user_utils::get_base_url(&state),
             Box::new(email_contents),
             state.conf.proxy.https_url.as_ref(),
         )
@@ -319,7 +319,7 @@ pub async fn connect_account(
         let send_email_result = state
             .email_client
             .compose_and_send_email(
-                email_types::get_base_url(&state),
+                user_utils::get_base_url(&state),
                 Box::new(email_contents),
                 state.conf.proxy.https_url.as_ref(),
             )
@@ -374,7 +374,7 @@ pub async fn connect_account(
         let magic_link_result = state
             .email_client
             .compose_and_send_email(
-                email_types::get_base_url(&state),
+                user_utils::get_base_url(&state),
                 Box::new(magic_link_email),
                 state.conf.proxy.https_url.as_ref(),
             )
@@ -390,7 +390,7 @@ pub async fn connect_account(
             let welcome_email_result = state
                 .email_client
                 .compose_and_send_email(
-                    email_types::get_base_url(&state),
+                    user_utils::get_base_url(&state),
                     Box::new(welcome_to_community_email),
                     state.conf.proxy.https_url.as_ref(),
                 )
@@ -525,7 +525,7 @@ pub async fn forgot_password(
     state
         .email_client
         .compose_and_send_email(
-            email_types::get_base_url(&state),
+            user_utils::get_base_url(&state),
             Box::new(email_contents),
             state.conf.proxy.https_url.as_ref(),
         )
@@ -937,7 +937,7 @@ async fn handle_existing_user_invitation(
         is_email_sent = state
             .email_client
             .compose_and_send_email(
-                email_types::get_base_url(state),
+                user_utils::get_base_url(state),
                 Box::new(email_contents),
                 state.conf.proxy.https_url.as_ref(),
             )
@@ -1092,7 +1092,7 @@ async fn handle_new_user_invitation(
         let send_email_result = state
             .email_client
             .compose_and_send_email(
-                email_types::get_base_url(state),
+                user_utils::get_base_url(state),
                 Box::new(email_contents),
                 state.conf.proxy.https_url.as_ref(),
             )
@@ -1247,7 +1247,7 @@ pub async fn resend_invite(
     state
         .email_client
         .compose_and_send_email(
-            email_types::get_base_url(&state),
+            user_utils::get_base_url(&state),
             Box::new(email_contents),
             state.conf.proxy.https_url.as_ref(),
         )
@@ -1967,7 +1967,7 @@ pub async fn send_verification_mail(
     state
         .email_client
         .compose_and_send_email(
-            email_types::get_base_url(&state),
+            user_utils::get_base_url(&state),
             Box::new(email_contents),
             state.conf.proxy.https_url.as_ref(),
         )
diff --git a/crates/router/src/core/user/dashboard_metadata.rs b/crates/router/src/core/user/dashboard_metadata.rs
index a4068fc4fad..ece4c601694 100644
--- a/crates/router/src/core/user/dashboard_metadata.rs
+++ b/crates/router/src/core/user/dashboard_metadata.rs
@@ -8,10 +8,10 @@ use diesel_models::{
     enums::DashboardMetadata as DBEnum, user::dashboard_metadata::DashboardMetadata,
 };
 use error_stack::{report, ResultExt};
+use hyperswitch_interfaces::crm::CrmPayload;
 #[cfg(feature = "email")]
 use masking::ExposeInterface;
 use masking::PeekInterface;
-#[cfg(feature = "email")]
 use router_env::logger;
 
 use crate::{
@@ -19,7 +19,7 @@ use crate::{
     routes::{app::ReqState, SessionState},
     services::{authentication::UserFromToken, ApplicationResponse},
     types::domain::{self, user::dashboard_metadata as types, MerchantKeyStore},
-    utils::user::dashboard_metadata as utils,
+    utils::user::{self as user_utils, dashboard_metadata as utils},
 };
 #[cfg(feature = "email")]
 use crate::{services::email::types as email_types, utils::user::theme as theme_utils};
@@ -501,7 +501,7 @@ async fn insert_metadata(
                     .await?;
                     let email_contents = email_types::BizEmailProd::new(
                         state,
-                        data,
+                        data.clone(),
                         theme.as_ref().map(|theme| theme.theme_id.clone()),
                         theme
                             .map(|theme| theme.email_config())
@@ -510,7 +510,7 @@ async fn insert_metadata(
                     let send_email_result = state
                         .email_client
                         .compose_and_send_email(
-                            email_types::get_base_url(state),
+                            user_utils::get_base_url(state),
                             Box::new(email_contents),
                             state.conf.proxy.https_url.as_ref(),
                         )
@@ -519,6 +519,43 @@ async fn insert_metadata(
                 }
             }
 
+            // Hubspot integration
+            let hubspot_body = state
+                .crm_client
+                .make_body(CrmPayload {
+                    legal_business_name: data.legal_business_name,
+                    business_label: data.business_label,
+                    business_location: data.business_location,
+                    display_name: data.display_name,
+                    poc_email: data.poc_email,
+                    business_type: data.business_type,
+                    business_identifier: data.business_identifier,
+                    business_website: data.business_website,
+                    poc_name: data.poc_name,
+                    poc_contact: data.poc_contact,
+                    comments: data.comments,
+                    is_completed: data.is_completed,
+                    business_country_name: data.business_country_name,
+                })
+                .await;
+            let base_url = user_utils::get_base_url(state);
+            let hubspot_request = state
+                .crm_client
+                .make_request(hubspot_body, base_url.to_string())
+                .await;
+
+            let _ = state
+                .crm_client
+                .send_request(&state.conf.proxy, hubspot_request)
+                .await
+                .inspect_err(|err| {
+                    logger::error!(
+                        "An error occurred while sending data to hubspot for user_id {}: {:?}",
+                        user.user_id,
+                        err
+                    );
+                });
+
             metadata
         }
         types::MetaData::SPTestPayment(data) => {
diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs
index 9cb83192f6f..82a91f468c5 100644
--- a/crates/router/src/routes/app.rs
+++ b/crates/router/src/routes/app.rs
@@ -17,6 +17,7 @@ use external_services::{
     grpc_client::{GrpcClients, GrpcHeaders},
 };
 use hyperswitch_interfaces::{
+    crm::CrmInterface,
     encryption_interface::EncryptionManagementInterface,
     secrets_interface::secret_state::{RawSecret, SecuredSecret},
 };
@@ -124,6 +125,7 @@ pub struct SessionState {
     pub grpc_client: Arc<GrpcClients>,
     pub theme_storage_client: Arc<dyn FileStorageInterface>,
     pub locale: String,
+    pub crm_client: Arc<dyn CrmInterface>,
 }
 impl scheduler::SchedulerSessionState for SessionState {
     fn get_db(&self) -> Box<dyn SchedulerInterface> {
@@ -235,6 +237,7 @@ pub struct AppState {
     pub encryption_client: Arc<dyn EncryptionManagementInterface>,
     pub grpc_client: Arc<GrpcClients>,
     pub theme_storage_client: Arc<dyn FileStorageInterface>,
+    pub crm_client: Arc<dyn CrmInterface>,
 }
 impl scheduler::SchedulerAppState for AppState {
     fn get_tenants(&self) -> Vec<id_type::TenantId> {
@@ -396,6 +399,7 @@ impl AppState {
 
             let file_storage_client = conf.file_storage.get_file_storage_client().await;
             let theme_storage_client = conf.theme.storage.get_file_storage_client().await;
+            let crm_client = conf.crm.get_crm_client().await;
 
             let grpc_client = conf.grpc_client.get_grpc_client_interface().await;
 
@@ -418,6 +422,7 @@ impl AppState {
                 encryption_client,
                 grpc_client,
                 theme_storage_client,
+                crm_client,
             }
         })
         .await
@@ -511,6 +516,7 @@ impl AppState {
             grpc_client: Arc::clone(&self.grpc_client),
             theme_storage_client: self.theme_storage_client.clone(),
             locale: locale.unwrap_or(common_utils::consts::DEFAULT_LOCALE.to_string()),
+            crm_client: self.crm_client.clone(),
         })
     }
 }
diff --git a/crates/router/src/routes/metrics.rs b/crates/router/src/routes/metrics.rs
index 63ef0b876f3..e8aeef3e406 100644
--- a/crates/router/src/routes/metrics.rs
+++ b/crates/router/src/routes/metrics.rs
@@ -11,7 +11,6 @@ counter_metric!(KV_MISS, GLOBAL_METER); // No. of KV misses
 // API Level Metrics
 counter_metric!(REQUESTS_RECEIVED, GLOBAL_METER);
 histogram_metric_f64!(REQUEST_TIME, GLOBAL_METER);
-histogram_metric_f64!(EXTERNAL_REQUEST_TIME, GLOBAL_METER);
 
 // Operation Level Metrics
 counter_metric!(PAYMENT_OPS_COUNT, GLOBAL_METER);
@@ -99,7 +98,6 @@ counter_metric!(APPLE_PAY_MANUAL_FLOW_FAILED_PAYMENT, GLOBAL_METER);
 counter_metric!(APPLE_PAY_SIMPLIFIED_FLOW_FAILED_PAYMENT, GLOBAL_METER);
 
 // Metrics for Payment Auto Retries
-counter_metric!(AUTO_RETRY_CONNECTION_CLOSED, GLOBAL_METER);
 counter_metric!(AUTO_RETRY_ELIGIBLE_REQUEST_COUNT, GLOBAL_METER);
 counter_metric!(AUTO_RETRY_GSM_MISS_COUNT, GLOBAL_METER);
 counter_metric!(AUTO_RETRY_GSM_FETCH_FAILURE_COUNT, GLOBAL_METER);
diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs
index a9193160263..3fef01a8c21 100644
--- a/crates/router/src/services/api.rs
+++ b/crates/router/src/services/api.rs
@@ -3,7 +3,6 @@ pub mod generic_link_response;
 pub mod request;
 use std::{
     collections::{HashMap, HashSet},
-    error::Error,
     fmt::Debug,
     future::Future,
     str,
@@ -52,7 +51,6 @@ use serde::Serialize;
 use serde_json::json;
 use tera::{Context, Error as TeraError, Tera};
 
-use self::request::{HeaderExt, RequestBuilderExt};
 use super::{
     authentication::AuthenticateAndFetch,
     connector_integration_interface::BoxedConnectorIntegrationInterface,
@@ -431,178 +429,6 @@ pub async fn call_connector_api(
     handle_response(response).await
 }
 
-#[instrument(skip_all)]
-pub async fn send_request(
-    state: &SessionState,
-    request: Request,
-    option_timeout_secs: Option<u64>,
-) -> CustomResult<reqwest::Response, errors::ApiClientError> {
-    logger::info!(method=?request.method, headers=?request.headers, payload=?request.body, ?request);
-
-    let url =
-        url::Url::parse(&request.url).change_context(errors::ApiClientError::UrlParsingFailed)?;
-
-    let client = client::create_client(
-        &state.conf.proxy,
-        request.certificate,
-        request.certificate_key,
-    )?;
-
-    let headers = request.headers.construct_header_map()?;
-    let metrics_tag = router_env::metric_attributes!((
-        consts::METRICS_HOST_TAG_NAME,
-        url.host_str().unwrap_or_default().to_owned()
-    ));
-    let request = {
-        match request.method {
-            Method::Get => client.get(url),
-            Method::Post => {
-                let client = client.post(url);
-                match request.body {
-                    Some(RequestContent::Json(payload)) => client.json(&payload),
-                    Some(RequestContent::FormData(form)) => client.multipart(form),
-                    Some(RequestContent::FormUrlEncoded(payload)) => client.form(&payload),
-                    Some(RequestContent::Xml(payload)) => {
-                        let body = quick_xml::se::to_string(&payload)
-                            .change_context(errors::ApiClientError::BodySerializationFailed)?;
-                        client.body(body).header("Content-Type", "application/xml")
-                    }
-                    Some(RequestContent::RawBytes(payload)) => client.body(payload),
-                    None => client,
-                }
-            }
-            Method::Put => {
-                let client = client.put(url);
-                match request.body {
-                    Some(RequestContent::Json(payload)) => client.json(&payload),
-                    Some(RequestContent::FormData(form)) => client.multipart(form),
-                    Some(RequestContent::FormUrlEncoded(payload)) => client.form(&payload),
-                    Some(RequestContent::Xml(payload)) => {
-                        let body = quick_xml::se::to_string(&payload)
-                            .change_context(errors::ApiClientError::BodySerializationFailed)?;
-                        client.body(body).header("Content-Type", "application/xml")
-                    }
-                    Some(RequestContent::RawBytes(payload)) => client.body(payload),
-                    None => client,
-                }
-            }
-            Method::Patch => {
-                let client = client.patch(url);
-                match request.body {
-                    Some(RequestContent::Json(payload)) => client.json(&payload),
-                    Some(RequestContent::FormData(form)) => client.multipart(form),
-                    Some(RequestContent::FormUrlEncoded(payload)) => client.form(&payload),
-                    Some(RequestContent::Xml(payload)) => {
-                        let body = quick_xml::se::to_string(&payload)
-                            .change_context(errors::ApiClientError::BodySerializationFailed)?;
-                        client.body(body).header("Content-Type", "application/xml")
-                    }
-                    Some(RequestContent::RawBytes(payload)) => client.body(payload),
-                    None => client,
-                }
-            }
-            Method::Delete => client.delete(url),
-        }
-        .add_headers(headers)
-        .timeout(Duration::from_secs(
-            option_timeout_secs.unwrap_or(consts::REQUEST_TIME_OUT),
-        ))
-    };
-
-    // We cannot clone the request type, because it has Form trait which is not cloneable. So we are cloning the request builder here.
-    let cloned_send_request = request.try_clone().map(|cloned_request| async {
-        cloned_request
-            .send()
-            .await
-            .map_err(|error| match error {
-                error if error.is_timeout() => {
-                    metrics::REQUEST_BUILD_FAILURE.add(1, &[]);
-                    errors::ApiClientError::RequestTimeoutReceived
-                }
-                error if is_connection_closed_before_message_could_complete(&error) => {
-                    metrics::REQUEST_BUILD_FAILURE.add(1, &[]);
-                    errors::ApiClientError::ConnectionClosedIncompleteMessage
-                }
-                _ => errors::ApiClientError::RequestNotSent(error.to_string()),
-            })
-            .attach_printable("Unable to send request to connector")
-    });
-
-    let send_request = async {
-        request
-            .send()
-            .await
-            .map_err(|error| match error {
-                error if error.is_timeout() => {
-                    metrics::REQUEST_BUILD_FAILURE.add(1, &[]);
-                    errors::ApiClientError::RequestTimeoutReceived
-                }
-                error if is_connection_closed_before_message_could_complete(&error) => {
-                    metrics::REQUEST_BUILD_FAILURE.add(1, &[]);
-                    errors::ApiClientError::ConnectionClosedIncompleteMessage
-                }
-                _ => errors::ApiClientError::RequestNotSent(error.to_string()),
-            })
-            .attach_printable("Unable to send request to connector")
-    };
-
-    let response = common_utils::metrics::utils::record_operation_time(
-        send_request,
-        &metrics::EXTERNAL_REQUEST_TIME,
-        metrics_tag,
-    )
-    .await;
-    // Retry once if the response is connection closed.
-    //
-    // This is just due to the racy nature of networking.
-    // hyper has a connection pool of idle connections, and it selected one to send your request.
-    // Most of the time, hyper will receive the server’s FIN and drop the dead connection from its pool.
-    // But occasionally, a connection will be selected from the pool
-    // and written to at the same time the server is deciding to close the connection.
-    // Since hyper already wrote some of the request,
-    // it can’t really retry it automatically on a new connection, since the server may have acted already
-    match response {
-        Ok(response) => Ok(response),
-        Err(error)
-            if error.current_context()
-                == &errors::ApiClientError::ConnectionClosedIncompleteMessage =>
-        {
-            metrics::AUTO_RETRY_CONNECTION_CLOSED.add(1, &[]);
-            match cloned_send_request {
-                Some(cloned_request) => {
-                    logger::info!(
-                        "Retrying request due to connection closed before message could complete"
-                    );
-                    common_utils::metrics::utils::record_operation_time(
-                        cloned_request,
-                        &metrics::EXTERNAL_REQUEST_TIME,
-                        metrics_tag,
-                    )
-                    .await
-                }
-                None => {
-                    logger::info!("Retrying request due to connection closed before message could complete failed as request is not cloneable");
-                    Err(error)
-                }
-            }
-        }
-        err @ Err(_) => err,
-    }
-}
-
-fn is_connection_closed_before_message_could_complete(error: &reqwest::Error) -> bool {
-    let mut source = error.source();
-    while let Some(err) = source {
-        if let Some(hyper_err) = err.downcast_ref::<hyper::Error>() {
-            if hyper_err.is_incomplete_message() {
-                return true;
-            }
-        }
-        source = err.source();
-    }
-    false
-}
-
 #[instrument(skip_all)]
 async fn handle_response(
     response: CustomResult<reqwest::Response, errors::ApiClientError>,
diff --git a/crates/router/src/services/api/client.rs b/crates/router/src/services/api/client.rs
index 096c5ed45d1..d690ebd3277 100644
--- a/crates/router/src/services/api/client.rs
+++ b/crates/router/src/services/api/client.rs
@@ -1,139 +1,20 @@
 use std::time::Duration;
 
-use base64::Engine;
+use common_utils::errors::ReportSwitchExt;
 use error_stack::ResultExt;
+pub use external_services::http_client::{self, client};
 use http::{HeaderValue, Method};
-use masking::{ExposeInterface, PeekInterface};
-use once_cell::sync::OnceCell;
+use hyperswitch_interfaces::types::Proxy;
+use masking::PeekInterface;
 use reqwest::multipart::Form;
 use router_env::tracing_actix_web::RequestId;
 
 use super::{request::Maskable, Request};
 use crate::{
-    configs::settings::Proxy,
-    consts::BASE64_ENGINE,
     core::errors::{ApiClientError, CustomResult},
     routes::SessionState,
 };
 
-static DEFAULT_CLIENT: OnceCell<reqwest::Client> = OnceCell::new();
-
-fn get_client_builder(
-    proxy_config: &Proxy,
-) -> CustomResult<reqwest::ClientBuilder, ApiClientError> {
-    let mut client_builder = reqwest::Client::builder()
-        .redirect(reqwest::redirect::Policy::none())
-        .pool_idle_timeout(Duration::from_secs(
-            proxy_config
-                .idle_pool_connection_timeout
-                .unwrap_or_default(),
-        ));
-
-    let proxy_exclusion_config =
-        reqwest::NoProxy::from_string(&proxy_config.bypass_proxy_hosts.clone().unwrap_or_default());
-
-    // Proxy all HTTPS traffic through the configured HTTPS proxy
-    if let Some(url) = proxy_config.https_url.as_ref() {
-        client_builder = client_builder.proxy(
-            reqwest::Proxy::https(url)
-                .change_context(ApiClientError::InvalidProxyConfiguration)
-                .attach_printable("HTTPS proxy configuration error")?
-                .no_proxy(proxy_exclusion_config.clone()),
-        );
-    }
-
-    // Proxy all HTTP traffic through the configured HTTP proxy
-    if let Some(url) = proxy_config.http_url.as_ref() {
-        client_builder = client_builder.proxy(
-            reqwest::Proxy::http(url)
-                .change_context(ApiClientError::InvalidProxyConfiguration)
-                .attach_printable("HTTP proxy configuration error")?
-                .no_proxy(proxy_exclusion_config),
-        );
-    }
-
-    Ok(client_builder)
-}
-
-fn get_base_client(proxy_config: &Proxy) -> CustomResult<reqwest::Client, ApiClientError> {
-    Ok(DEFAULT_CLIENT
-        .get_or_try_init(|| {
-            get_client_builder(proxy_config)?
-                .build()
-                .change_context(ApiClientError::ClientConstructionFailed)
-                .attach_printable("Failed to construct base client")
-        })?
-        .clone())
-}
-
-// We may need to use outbound proxy to connect to external world.
-// Precedence will be the environment variables, followed by the config.
-pub fn create_client(
-    proxy_config: &Proxy,
-    client_certificate: Option<masking::Secret<String>>,
-    client_certificate_key: Option<masking::Secret<String>>,
-) -> CustomResult<reqwest::Client, ApiClientError> {
-    match (client_certificate, client_certificate_key) {
-        (Some(encoded_certificate), Some(encoded_certificate_key)) => {
-            let client_builder = get_client_builder(proxy_config)?;
-
-            let identity = create_identity_from_certificate_and_key(
-                encoded_certificate.clone(),
-                encoded_certificate_key,
-            )?;
-            let certificate_list = create_certificate(encoded_certificate)?;
-            let client_builder = certificate_list
-                .into_iter()
-                .fold(client_builder, |client_builder, certificate| {
-                    client_builder.add_root_certificate(certificate)
-                });
-            client_builder
-                .identity(identity)
-                .use_rustls_tls()
-                .build()
-                .change_context(ApiClientError::ClientConstructionFailed)
-                .attach_printable("Failed to construct client with certificate and certificate key")
-        }
-        _ => get_base_client(proxy_config),
-    }
-}
-
-pub fn create_identity_from_certificate_and_key(
-    encoded_certificate: masking::Secret<String>,
-    encoded_certificate_key: masking::Secret<String>,
-) -> Result<reqwest::Identity, error_stack::Report<ApiClientError>> {
-    let decoded_certificate = BASE64_ENGINE
-        .decode(encoded_certificate.expose())
-        .change_context(ApiClientError::CertificateDecodeFailed)?;
-
-    let decoded_certificate_key = BASE64_ENGINE
-        .decode(encoded_certificate_key.expose())
-        .change_context(ApiClientError::CertificateDecodeFailed)?;
-
-    let certificate = String::from_utf8(decoded_certificate)
-        .change_context(ApiClientError::CertificateDecodeFailed)?;
-
-    let certificate_key = String::from_utf8(decoded_certificate_key)
-        .change_context(ApiClientError::CertificateDecodeFailed)?;
-
-    let key_chain = format!("{}{}", certificate_key, certificate);
-    reqwest::Identity::from_pem(key_chain.as_bytes())
-        .change_context(ApiClientError::CertificateDecodeFailed)
-}
-
-pub fn create_certificate(
-    encoded_certificate: masking::Secret<String>,
-) -> Result<Vec<reqwest::Certificate>, error_stack::Report<ApiClientError>> {
-    let decoded_certificate = BASE64_ENGINE
-        .decode(encoded_certificate.expose())
-        .change_context(ApiClientError::CertificateDecodeFailed)?;
-
-    let certificate = String::from_utf8(decoded_certificate)
-        .change_context(ApiClientError::CertificateDecodeFailed)?;
-    reqwest::Certificate::from_pem_bundle(certificate.as_bytes())
-        .change_context(ApiClientError::CertificateDecodeFailed)
-}
-
 pub trait RequestBuilder: Send + Sync {
     fn json(&mut self, body: serde_json::Value);
     fn url_encoded_form(&mut self, body: serde_json::Value);
@@ -196,7 +77,8 @@ pub struct ProxyClient {
 
 impl ProxyClient {
     pub fn new(proxy_config: &Proxy) -> CustomResult<Self, ApiClientError> {
-        let client = get_client_builder(proxy_config)?
+        let client = client::get_client_builder(proxy_config)
+            .switch()?
             .build()
             .change_context(ApiClientError::InvalidProxyConfiguration)?;
         Ok(Self {
@@ -213,9 +95,10 @@ impl ProxyClient {
     ) -> CustomResult<reqwest::Client, ApiClientError> {
         match (client_certificate, client_certificate_key) {
             (Some(certificate), Some(certificate_key)) => {
-                let client_builder = get_client_builder(&self.proxy_config)?;
+                let client_builder = client::get_client_builder(&self.proxy_config).switch()?;
                 let identity =
-                    create_identity_from_certificate_and_key(certificate, certificate_key)?;
+                    client::create_identity_from_certificate_and_key(certificate, certificate_key)
+                        .switch()?;
                 Ok(client_builder
                     .identity(identity)
                     .build()
@@ -314,7 +197,9 @@ impl ApiClient for ProxyClient {
         option_timeout_secs: Option<u64>,
         _forward_to_kafka: bool,
     ) -> CustomResult<reqwest::Response, ApiClientError> {
-        crate::services::send_request(state, request, option_timeout_secs).await
+        http_client::send_request(&state.conf.proxy, request, option_timeout_secs)
+            .await
+            .switch()
     }
 
     fn add_request_id(&mut self, request_id: RequestId) {
diff --git a/crates/router/src/services/api/request.rs b/crates/router/src/services/api/request.rs
index ed53da0d971..7cf35ff0d87 100644
--- a/crates/router/src/services/api/request.rs
+++ b/crates/router/src/services/api/request.rs
@@ -1,48 +1,2 @@
-use std::str::FromStr;
-
 pub use common_utils::request::ContentType;
-use common_utils::request::Headers;
-use error_stack::ResultExt;
 pub use masking::{Mask, Maskable};
-use router_env::{instrument, tracing};
-
-use crate::core::errors::{self, CustomResult};
-
-pub(super) trait HeaderExt {
-    fn construct_header_map(
-        self,
-    ) -> CustomResult<reqwest::header::HeaderMap, errors::ApiClientError>;
-}
-
-impl HeaderExt for Headers {
-    fn construct_header_map(
-        self,
-    ) -> CustomResult<reqwest::header::HeaderMap, errors::ApiClientError> {
-        use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
-
-        self.into_iter().try_fold(
-            HeaderMap::new(),
-            |mut header_map, (header_name, header_value)| {
-                let header_name = HeaderName::from_str(&header_name)
-                    .change_context(errors::ApiClientError::HeaderMapConstructionFailed)?;
-                let header_value = header_value.into_inner();
-                let header_value = HeaderValue::from_str(&header_value)
-                    .change_context(errors::ApiClientError::HeaderMapConstructionFailed)?;
-                header_map.append(header_name, header_value);
-                Ok(header_map)
-            },
-        )
-    }
-}
-
-pub(super) trait RequestBuilderExt {
-    fn add_headers(self, headers: reqwest::header::HeaderMap) -> Self;
-}
-
-impl RequestBuilderExt for reqwest::RequestBuilder {
-    #[instrument(skip_all)]
-    fn add_headers(mut self, headers: reqwest::header::HeaderMap) -> Self {
-        self = self.headers(headers);
-        self
-    }
-}
diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs
index 0328ea988d4..73c42818439 100644
--- a/crates/router/src/services/email/types.rs
+++ b/crates/router/src/services/email/types.rs
@@ -320,15 +320,6 @@ pub fn get_link_with_token(
 
     email_url
 }
-
-pub fn get_base_url(state: &SessionState) -> &str {
-    if !state.conf.multitenancy.enabled {
-        &state.conf.user.base_url
-    } else {
-        &state.tenant.user.control_center_url
-    }
-}
-
 pub struct VerifyEmail {
     pub recipient_email: domain::UserEmail,
     pub settings: std::sync::Arc<configs::Settings>,
@@ -576,7 +567,7 @@ impl BizEmailProd {
                 state.conf.email.prod_intent_recipient_email.clone(),
             )?,
             settings: state.conf.clone(),
-            user_name: data.poc_name.unwrap_or_default().into(),
+            user_name: data.poc_name.unwrap_or_default(),
             poc_email: data.poc_email.unwrap_or_default(),
             legal_business_name: data.legal_business_name.unwrap_or_default(),
             business_location: data
diff --git a/crates/router/src/services/openidconnect.rs b/crates/router/src/services/openidconnect.rs
index c4dd0df6e13..f9a76a0ada2 100644
--- a/crates/router/src/services/openidconnect.rs
+++ b/crates/router/src/services/openidconnect.rs
@@ -1,4 +1,6 @@
+use common_utils::errors::ErrorSwitch;
 use error_stack::ResultExt;
+use external_services::http_client::client;
 use masking::{ExposeInterface, Secret};
 use oidc::TokenResponse;
 use openidconnect::{self as oidc, core as oidc_core};
@@ -9,7 +11,6 @@ use crate::{
     consts,
     core::errors::{UserErrors, UserResult},
     routes::SessionState,
-    services::api::client,
     types::domain::user::UserEmail,
 };
 
@@ -156,7 +157,7 @@ async fn get_oidc_reqwest_client(
     request: oidc::HttpRequest,
 ) -> Result<oidc::HttpResponse, ApiClientError> {
     let client = client::create_client(&state.conf.proxy, None, None)
-        .map_err(|e| e.current_context().to_owned())?;
+        .map_err(|e| e.current_context().switch())?;
 
     let mut request_builder = client
         .request(request.method, request.url)
diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs
index ea3edd8dfc7..18e227912ef 100644
--- a/crates/router/src/utils/user.rs
+++ b/crates/router/src/utils/user.rs
@@ -380,3 +380,11 @@ pub fn generate_env_specific_merchant_id(value: String) -> UserResult<id_type::M
         Ok(id_type::MerchantId::new_from_unix_timestamp())
     }
 }
+
+pub fn get_base_url(state: &SessionState) -> &str {
+    if !state.conf.multitenancy.enabled {
+        &state.conf.user.base_url
+    } else {
+        &state.tenant.user.control_center_url
+    }
+}
diff --git a/crates/router/src/workflows/api_key_expiry.rs b/crates/router/src/workflows/api_key_expiry.rs
index 4df78847b39..51e862681ae 100644
--- a/crates/router/src/workflows/api_key_expiry.rs
+++ b/crates/router/src/workflows/api_key_expiry.rs
@@ -9,9 +9,12 @@ use crate::{
     consts, errors,
     logger::error,
     routes::{metrics, SessionState},
-    services::email::types::{self as email_types, ApiKeyExpiryReminder},
+    services::email::types::ApiKeyExpiryReminder,
     types::{api, domain::UserEmail, storage},
-    utils::{user::theme as theme_utils, OptionExt},
+    utils::{
+        user::{self as user_utils, theme as theme_utils},
+        OptionExt,
+    },
 };
 
 pub struct ApiKeyExpiryWorkflow;
@@ -110,7 +113,7 @@ impl ProcessTrackerWorkflow<SessionState> for ApiKeyExpiryWorkflow {
             .email_client
             .clone()
             .compose_and_send_email(
-                email_types::get_base_url(state),
+                user_utils::get_base_url(state),
                 Box::new(email_contents),
                 state.conf.proxy.https_url.as_ref(),
             )

## Type of Change
<!-- Put an `x` in the boxes that apply -->

- [ ] Bugfix
- [ ] New feature
- [x] Enhancement
- [ ] Refactoring
- [ ] Dependency updates
- [ ] Documentation
- [ ] CI/CD

## Description
<!-- Describe your changes in detail -->

This PR includes 
- HubSpot proxy integration for `prod_intent` triggered from the dashboard
- New CRM interface with two implementations: NoCrm and HubspotProxy. 
- Includes refactoring to move `http_client send_request`  and `client` from the `router` module to `external_services` to support this integration.

### Additional Changes

- [ ] This PR modifies the API contract
- [ ] This PR modifies the database schema
- [ ] This PR modifies application configuration/environment variables

<!--
Provide links to the files with corresponding changes.

Following are the paths where you can find config files:
1. `config`
2. `crates/router/src/configs`
3. `loadtest/config`
-->


## Motivation and Context
<!--
Why is this change required? What problem does it solve?
If it fixes an open issue, please link to the issue here.

If you don't have an issue, we'd recommend starting with one first so the PR
can focus on the implementation (unless it is an obvious bug or documentation fix
that will have little conversation).
-->
Add hubspot tracking in dashboard prod intent 


## How did you test it?
<!--
Did you write an integration/unit/API test to verify the code changes?
Or did you test this change manually (provide relevant screenshots)?
-->
Through dashboard FE
- SIgnin in dashboard 
- Create a prod intent for a merchant 
- This will trigger hubspot tracking from BE and this should be visible in hubspot dashboard 

## Checklist
<!-- Put an `x` in the boxes that apply -->

- [x] I formatted the code `cargo +nightly fmt --all`
- [x] I addressed lints thrown by `cargo clippy`
- [ ] I reviewed the submitted code
- [ ] I added unit tests for my changes where possible
