Bug: feat(users): Introduce themes in user APIs



Currently there is no themes context in the user and email related APIs where the themes should be reflected.

We should change the APIs which would have the theme context.

diff --git a/config/config.example.toml b/config/config.example.toml
index 5aa9e543fb2..0fe6b8caa2c 100644
--- a/config/config.example.toml
+++ b/config/config.example.toml
@@ -797,5 +797,12 @@ host = "localhost" # Client Host
 port = 7000        # Client Port
 service = "dynamo" # Service name
 
-[theme_storage]
+[theme.storage]
 file_storage_backend = "file_system" # Theme storage backend to be used
+
+[theme.email_config]
+entity_name = "Hyperswitch"                      # Name of the entity to be showed in emails
+entity_logo_url = "https://example.com/logo.svg" # Logo URL of the entity to be used in emails
+foreground_color = "#000000"                     # Foreground color of email text
+primary_color = "#006DF9"                        # Primary color of email body
+background_color = "#FFFFFF"                     # Background color of email body
diff --git a/config/deployments/env_specific.toml b/config/deployments/env_specific.toml
index 5cadc66ddfc..848a2305ae4 100644
--- a/config/deployments/env_specific.toml
+++ b/config/deployments/env_specific.toml
@@ -328,9 +328,16 @@ host = "localhost" # Client Host
 port = 7000        # Client Port
 service = "dynamo" # Service name
 
-[theme_storage]
+[theme.storage]
 file_storage_backend = "aws_s3" # Theme storage backend to be used
 
-[theme_storage.aws_s3]
+[theme.storage.aws_s3]
 region = "bucket_region" # AWS region where the S3 bucket for theme storage is located
 bucket_name = "bucket"   # AWS S3 bucket name for theme storage
+
+[theme.email_config]
+entity_name = "Hyperswitch"                      # Name of the entity to be showed in emails
+entity_logo_url = "https://example.com/logo.svg" # Logo URL of the entity to be used in emails
+foreground_color = "#000000"                     # Foreground color of email text
+primary_color = "#006DF9"                        # Primary color of email body
+background_color = "#FFFFFF"                     # Background color of email body
diff --git a/config/development.toml b/config/development.toml
index fa577a78e6a..077e09f0463 100644
--- a/config/development.toml
+++ b/config/development.toml
@@ -797,5 +797,12 @@ host = "localhost"
 port = 7000
 service = "dynamo"
 
-[theme_storage]
-file_storage_backend = "file_system"
+[theme.storage]
+file_storage_backend = "file_system" # Theme storage backend to be used
+
+[theme.email_config]
+entity_name = "Hyperswitch"                      # Name of the entity to be showed in emails
+entity_logo_url = "https://example.com/logo.svg" # Logo URL of the entity to be used in emails
+foreground_color = "#000000"                     # Foreground color of email text
+primary_color = "#006DF9"                        # Primary color of email body
+background_color = "#FFFFFF"                     # Background color of email body
diff --git a/config/docker_compose.toml b/config/docker_compose.toml
index 3cc690203e6..dbd5286b290 100644
--- a/config/docker_compose.toml
+++ b/config/docker_compose.toml
@@ -694,5 +694,12 @@ prod_intent_recipient_email = "business@example.com" # Recipient email for prod
 email_role_arn = ""        # The amazon resource name ( arn ) of the role which has permission to send emails
 sts_role_session_name = "" # An identifier for the assumed role session, used to uniquely identify a session.
 
-[theme_storage]
+[theme.storage]
 file_storage_backend = "file_system" # Theme storage backend to be used
+
+[theme.email_config]
+entity_name = "Hyperswitch"                      # Name of the entity to be showed in emails
+entity_logo_url = "https://example.com/logo.svg" # Logo URL of the entity to be used in emails
+foreground_color = "#000000"                     # Foreground color of email text
+primary_color = "#006DF9"                        # Primary color of email body
+background_color = "#FFFFFF"                     # Background color of email body
diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs
index e61696803a2..7b5911cf1a8 100644
--- a/crates/api_models/src/user.rs
+++ b/crates/api_models/src/user.rs
@@ -150,6 +150,7 @@ pub struct GetUserDetailsResponse {
     pub recovery_codes_left: Option<usize>,
     pub profile_id: id_type::ProfileId,
     pub entity_type: EntityType,
+    pub theme_id: Option<String>,
 }
 
 #[derive(Debug, serde::Deserialize, serde::Serialize)]
@@ -345,8 +346,9 @@ pub struct SsoSignInRequest {
 }
 
 #[derive(Debug, serde::Deserialize, serde::Serialize)]
-pub struct AuthIdQueryParam {
+pub struct AuthIdAndThemeIdQueryParam {
     pub auth_id: Option<String>,
+    pub theme_id: Option<String>,
 }
 
 #[derive(Debug, serde::Deserialize, serde::Serialize)]
diff --git a/crates/api_models/src/user/theme.rs b/crates/api_models/src/user/theme.rs
index 23218c4a163..c79da307b9a 100644
--- a/crates/api_models/src/user/theme.rs
+++ b/crates/api_models/src/user/theme.rs
@@ -1,6 +1,9 @@
 use actix_multipart::form::{bytes::Bytes, text::Text, MultipartForm};
 use common_enums::EntityType;
-use common_utils::{id_type, types::theme::ThemeLineage};
+use common_utils::{
+    id_type,
+    types::theme::{EmailThemeConfig, ThemeLineage},
+};
 use masking::Secret;
 use serde::{Deserialize, Serialize};
 
@@ -13,6 +16,7 @@ pub struct GetThemeResponse {
     pub org_id: Option<id_type::OrganizationId>,
     pub merchant_id: Option<id_type::MerchantId>,
     pub profile_id: Option<id_type::ProfileId>,
+    pub email_config: EmailThemeConfig,
     pub theme_data: ThemeData,
 }
 
@@ -35,12 +39,14 @@ pub struct CreateThemeRequest {
     pub lineage: ThemeLineage,
     pub theme_name: String,
     pub theme_data: ThemeData,
+    pub email_config: Option<EmailThemeConfig>,
 }
 
 #[derive(Serialize, Deserialize, Debug)]
 pub struct UpdateThemeRequest {
     pub lineage: ThemeLineage,
     pub theme_data: ThemeData,
+    // TODO: Add support to update email config
 }
 
 // All the below structs are for the theme.json file,
diff --git a/crates/common_utils/src/types/theme.rs b/crates/common_utils/src/types/theme.rs
index 239cffc40d4..05ae26b565a 100644
--- a/crates/common_utils/src/types/theme.rs
+++ b/crates/common_utils/src/types/theme.rs
@@ -1,4 +1,5 @@
 use common_enums::EntityType;
+use serde::{Deserialize, Serialize};
 
 use crate::{
     events::{ApiEventMetric, ApiEventsType},
@@ -7,15 +8,14 @@ use crate::{
 
 /// Enum for having all the required lineage for every level.
 /// Currently being used for theme related APIs and queries.
-#[derive(Debug, serde::Deserialize, serde::Serialize)]
+#[derive(Debug, Deserialize, Serialize)]
 #[serde(tag = "entity_type", rename_all = "snake_case")]
 pub enum ThemeLineage {
-    // TODO: Add back Tenant variant when we introduce Tenant Variant in EntityType
-    // /// Tenant lineage variant
-    // Tenant {
-    //     /// tenant_id: String
-    //     tenant_id: String,
-    // },
+    /// Tenant lineage variant
+    Tenant {
+        /// tenant_id: TenantId
+        tenant_id: id_type::TenantId,
+    },
     /// Org lineage variant
     Organization {
         /// tenant_id: TenantId
@@ -48,9 +48,35 @@ pub enum ThemeLineage {
 impl_api_event_type!(Miscellaneous, (ThemeLineage));
 
 impl ThemeLineage {
+    /// Constructor for ThemeLineage
+    pub fn new(
+        entity_type: EntityType,
+        tenant_id: id_type::TenantId,
+        org_id: id_type::OrganizationId,
+        merchant_id: id_type::MerchantId,
+        profile_id: id_type::ProfileId,
+    ) -> Self {
+        match entity_type {
+            EntityType::Tenant => Self::Tenant { tenant_id },
+            EntityType::Organization => Self::Organization { tenant_id, org_id },
+            EntityType::Merchant => Self::Merchant {
+                tenant_id,
+                org_id,
+                merchant_id,
+            },
+            EntityType::Profile => Self::Profile {
+                tenant_id,
+                org_id,
+                merchant_id,
+                profile_id,
+            },
+        }
+    }
+
     /// Get the entity_type from the lineage
     pub fn entity_type(&self) -> EntityType {
         match self {
+            Self::Tenant { .. } => EntityType::Tenant,
             Self::Organization { .. } => EntityType::Organization,
             Self::Merchant { .. } => EntityType::Merchant,
             Self::Profile { .. } => EntityType::Profile,
@@ -60,7 +86,8 @@ impl ThemeLineage {
     /// Get the tenant_id from the lineage
     pub fn tenant_id(&self) -> &id_type::TenantId {
         match self {
-            Self::Organization { tenant_id, .. }
+            Self::Tenant { tenant_id }
+            | Self::Organization { tenant_id, .. }
             | Self::Merchant { tenant_id, .. }
             | Self::Profile { tenant_id, .. } => tenant_id,
         }
@@ -69,6 +96,7 @@ impl ThemeLineage {
     /// Get the org_id from the lineage
     pub fn org_id(&self) -> Option<&id_type::OrganizationId> {
         match self {
+            Self::Tenant { .. } => None,
             Self::Organization { org_id, .. }
             | Self::Merchant { org_id, .. }
             | Self::Profile { org_id, .. } => Some(org_id),
@@ -78,7 +106,7 @@ impl ThemeLineage {
     /// Get the merchant_id from the lineage
     pub fn merchant_id(&self) -> Option<&id_type::MerchantId> {
         match self {
-            Self::Organization { .. } => None,
+            Self::Tenant { .. } | Self::Organization { .. } => None,
             Self::Merchant { merchant_id, .. } | Self::Profile { merchant_id, .. } => {
                 Some(merchant_id)
             }
@@ -88,8 +116,72 @@ impl ThemeLineage {
     /// Get the profile_id from the lineage
     pub fn profile_id(&self) -> Option<&id_type::ProfileId> {
         match self {
-            Self::Organization { .. } | Self::Merchant { .. } => None,
+            Self::Tenant { .. } | Self::Organization { .. } | Self::Merchant { .. } => None,
             Self::Profile { profile_id, .. } => Some(profile_id),
         }
     }
+
+    /// Get higher lineages from the current lineage
+    pub fn get_same_and_higher_lineages(self) -> Vec<Self> {
+        match &self {
+            Self::Tenant { .. } => vec![self],
+            Self::Organization { tenant_id, .. } => vec![
+                Self::Tenant {
+                    tenant_id: tenant_id.clone(),
+                },
+                self,
+            ],
+            Self::Merchant {
+                tenant_id, org_id, ..
+            } => vec![
+                Self::Tenant {
+                    tenant_id: tenant_id.clone(),
+                },
+                Self::Organization {
+                    tenant_id: tenant_id.clone(),
+                    org_id: org_id.clone(),
+                },
+                self,
+            ],
+            Self::Profile {
+                tenant_id,
+                org_id,
+                merchant_id,
+                ..
+            } => vec![
+                Self::Tenant {
+                    tenant_id: tenant_id.clone(),
+                },
+                Self::Organization {
+                    tenant_id: tenant_id.clone(),
+                    org_id: org_id.clone(),
+                },
+                Self::Merchant {
+                    tenant_id: tenant_id.clone(),
+                    org_id: org_id.clone(),
+                    merchant_id: merchant_id.clone(),
+                },
+                self,
+            ],
+        }
+    }
+}
+
+/// Struct for holding the theme settings for email
+#[derive(Debug, Clone, Default, Deserialize, Serialize)]
+pub struct EmailThemeConfig {
+    /// The entity name to be used in the email
+    pub entity_name: String,
+
+    /// The URL of the entity logo to be used in the email
+    pub entity_logo_url: String,
+
+    /// The primary color to be used in the email
+    pub primary_color: String,
+
+    /// The foreground color to be used in the email
+    pub foreground_color: String,
+
+    /// The background color to be used in the email
+    pub background_color: String,
 }
diff --git a/crates/diesel_models/src/query/user/theme.rs b/crates/diesel_models/src/query/user/theme.rs
index a26e401ffbb..945ce1a9bb2 100644
--- a/crates/diesel_models/src/query/user/theme.rs
+++ b/crates/diesel_models/src/query/user/theme.rs
@@ -1,13 +1,22 @@
+use async_bb8_diesel::AsyncRunQueryDsl;
 use common_utils::types::theme::ThemeLineage;
 use diesel::{
     associations::HasTable,
+    debug_query,
     pg::Pg,
+    result::Error as DieselError,
     sql_types::{Bool, Nullable},
-    BoolExpressionMethods, ExpressionMethods,
+    BoolExpressionMethods, ExpressionMethods, NullableExpressionMethods, QueryDsl,
 };
+use error_stack::{report, ResultExt};
+use router_env::logger;
 
 use crate::{
-    query::generics,
+    errors::DatabaseError,
+    query::generics::{
+        self,
+        db_metrics::{track_database_call, DatabaseOperation},
+    },
     schema::themes::dsl,
     user::theme::{Theme, ThemeNew},
     PgPooledConn, StorageResult,
@@ -27,15 +36,14 @@ impl Theme {
             + 'static,
     > {
         match lineage {
-            // TODO: Add back Tenant variant when we introduce Tenant Variant in EntityType
-            // ThemeLineage::Tenant { tenant_id } => Box::new(
-            //     dsl::tenant_id
-            //         .eq(tenant_id)
-            //         .and(dsl::org_id.is_null())
-            //         .and(dsl::merchant_id.is_null())
-            //         .and(dsl::profile_id.is_null())
-            //         .nullable(),
-            // ),
+            ThemeLineage::Tenant { tenant_id } => Box::new(
+                dsl::tenant_id
+                    .eq(tenant_id)
+                    .and(dsl::org_id.is_null())
+                    .and(dsl::merchant_id.is_null())
+                    .and(dsl::profile_id.is_null())
+                    .nullable(),
+            ),
             ThemeLineage::Organization { tenant_id, org_id } => Box::new(
                 dsl::tenant_id
                     .eq(tenant_id)
@@ -77,6 +85,41 @@ impl Theme {
         .await
     }
 
+    pub async fn find_most_specific_theme_in_lineage(
+        conn: &PgPooledConn,
+        lineage: ThemeLineage,
+    ) -> StorageResult<Self> {
+        let query = <Self as HasTable>::table().into_boxed();
+
+        let query =
+            lineage
+                .get_same_and_higher_lineages()
+                .into_iter()
+                .fold(query, |mut query, lineage| {
+                    query = query.or_filter(Self::lineage_filter(lineage));
+                    query
+                });
+
+        logger::debug!(query = %debug_query::<Pg,_>(&query).to_string());
+
+        let data: Vec<Self> = match track_database_call::<Self, _, _>(
+            query.get_results_async(conn),
+            DatabaseOperation::Filter,
+        )
+        .await
+        {
+            Ok(value) => Ok(value),
+            Err(err) => match err {
+                DieselError::NotFound => Err(report!(err)).change_context(DatabaseError::NotFound),
+                _ => Err(report!(err)).change_context(DatabaseError::Others),
+            },
+        }?;
+
+        data.into_iter()
+            .min_by_key(|theme| theme.entity_type)
+            .ok_or(report!(DatabaseError::NotFound))
+    }
+
     pub async fn find_by_lineage(
         conn: &PgPooledConn,
         lineage: ThemeLineage,
diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs
index c30562de991..bff808f6c50 100644
--- a/crates/diesel_models/src/schema.rs
+++ b/crates/diesel_models/src/schema.rs
@@ -1317,6 +1317,15 @@ diesel::table! {
         entity_type -> Varchar,
         #[max_length = 64]
         theme_name -> Varchar,
+        #[max_length = 64]
+        email_primary_color -> Varchar,
+        #[max_length = 64]
+        email_foreground_color -> Varchar,
+        #[max_length = 64]
+        email_background_color -> Varchar,
+        #[max_length = 64]
+        email_entity_name -> Varchar,
+        email_entity_logo_url -> Text,
     }
 }
 
diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs
index 12bb19c0a7f..211df46e7ac 100644
--- a/crates/diesel_models/src/schema_v2.rs
+++ b/crates/diesel_models/src/schema_v2.rs
@@ -1264,6 +1264,15 @@ diesel::table! {
         entity_type -> Varchar,
         #[max_length = 64]
         theme_name -> Varchar,
+        #[max_length = 64]
+        email_primary_color -> Varchar,
+        #[max_length = 64]
+        email_foreground_color -> Varchar,
+        #[max_length = 64]
+        email_background_color -> Varchar,
+        #[max_length = 64]
+        email_entity_name -> Varchar,
+        email_entity_logo_url -> Text,
     }
 }
 
diff --git a/crates/diesel_models/src/user/theme.rs b/crates/diesel_models/src/user/theme.rs
index e43a182126b..46cc90a45ec 100644
--- a/crates/diesel_models/src/user/theme.rs
+++ b/crates/diesel_models/src/user/theme.rs
@@ -1,5 +1,8 @@
 use common_enums::EntityType;
-use common_utils::{date_time, id_type, types::theme::ThemeLineage};
+use common_utils::{
+    date_time, id_type,
+    types::theme::{EmailThemeConfig, ThemeLineage},
+};
 use diesel::{Identifiable, Insertable, Queryable, Selectable};
 use time::PrimitiveDateTime;
 
@@ -17,6 +20,11 @@ pub struct Theme {
     pub last_modified_at: PrimitiveDateTime,
     pub entity_type: EntityType,
     pub theme_name: String,
+    pub email_primary_color: String,
+    pub email_foreground_color: String,
+    pub email_background_color: String,
+    pub email_entity_name: String,
+    pub email_entity_logo_url: String,
 }
 
 #[derive(Clone, Debug, Insertable, router_derive::DebugAsDisplay)]
@@ -31,10 +39,20 @@ pub struct ThemeNew {
     pub last_modified_at: PrimitiveDateTime,
     pub entity_type: EntityType,
     pub theme_name: String,
+    pub email_primary_color: String,
+    pub email_foreground_color: String,
+    pub email_background_color: String,
+    pub email_entity_name: String,
+    pub email_entity_logo_url: String,
 }
 
 impl ThemeNew {
-    pub fn new(theme_id: String, theme_name: String, lineage: ThemeLineage) -> Self {
+    pub fn new(
+        theme_id: String,
+        theme_name: String,
+        lineage: ThemeLineage,
+        email_config: EmailThemeConfig,
+    ) -> Self {
         let now = date_time::now();
 
         Self {
@@ -47,6 +65,23 @@ impl ThemeNew {
             entity_type: lineage.entity_type(),
             created_at: now,
             last_modified_at: now,
+            email_primary_color: email_config.primary_color,
+            email_foreground_color: email_config.foreground_color,
+            email_background_color: email_config.background_color,
+            email_entity_name: email_config.entity_name,
+            email_entity_logo_url: email_config.entity_logo_url,
+        }
+    }
+}
+
+impl Theme {
+    pub fn email_config(&self) -> EmailThemeConfig {
+        EmailThemeConfig {
+            primary_color: self.email_primary_color.clone(),
+            foreground_color: self.email_foreground_color.clone(),
+            background_color: self.email_background_color.clone(),
+            entity_name: self.email_entity_name.clone(),
+            entity_logo_url: self.email_entity_logo_url.clone(),
         }
     }
 }
diff --git a/crates/router/src/configs/secrets_transformers.rs b/crates/router/src/configs/secrets_transformers.rs
index 3ab56266b55..3862f70536f 100644
--- a/crates/router/src/configs/secrets_transformers.rs
+++ b/crates/router/src/configs/secrets_transformers.rs
@@ -545,6 +545,6 @@ pub(crate) async fn fetch_raw_secrets(
             .network_tokenization_supported_card_networks,
         network_tokenization_service,
         network_tokenization_supported_connectors: conf.network_tokenization_supported_connectors,
-        theme_storage: conf.theme_storage,
+        theme: conf.theme,
     }
 }
diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs
index 1da4a33f5f3..4ab2a8fd5c7 100644
--- a/crates/router/src/configs/settings.rs
+++ b/crates/router/src/configs/settings.rs
@@ -6,7 +6,7 @@ use std::{
 #[cfg(feature = "olap")]
 use analytics::{opensearch::OpenSearchConfig, ReportConfig};
 use api_models::{enums, payment_methods::RequiredFieldInfo};
-use common_utils::{ext_traits::ConfigExt, id_type};
+use common_utils::{ext_traits::ConfigExt, id_type, types::theme::EmailThemeConfig};
 use config::{Environment, File};
 use error_stack::ResultExt;
 #[cfg(feature = "email")]
@@ -128,7 +128,7 @@ pub struct Settings<S: SecretState> {
     pub network_tokenization_supported_card_networks: NetworkTokenizationSupportedCardNetworks,
     pub network_tokenization_service: Option<SecretStateContainer<NetworkTokenizationService, S>>,
     pub network_tokenization_supported_connectors: NetworkTokenizationSupportedConnectors,
-    pub theme_storage: FileStorageConfig,
+    pub theme: ThemeSettings,
 }
 
 #[derive(Debug, Deserialize, Clone, Default)]
@@ -887,7 +887,8 @@ impl Settings<SecuredSecret> {
             .validate()
             .map_err(|err| ApplicationError::InvalidConfigurationValueError(err.into()))?;
 
-        self.theme_storage
+        self.theme
+            .storage
             .validate()
             .map_err(|err| ApplicationError::InvalidConfigurationValueError(err.to_string()))?;
 
@@ -992,6 +993,12 @@ impl Default for CellInformation {
     }
 }
 
+#[derive(Debug, Deserialize, Clone, Default)]
+pub struct ThemeSettings {
+    pub storage: FileStorageConfig,
+    pub email_config: EmailThemeConfig,
+}
+
 fn deserialize_hashmap_inner<K, V>(
     value: HashMap<String, String>,
 ) -> Result<HashMap<K, HashSet<V>>, String>
diff --git a/crates/router/src/core/errors/user.rs b/crates/router/src/core/errors/user.rs
index 33477df6330..fa5f185ab82 100644
--- a/crates/router/src/core/errors/user.rs
+++ b/crates/router/src/core/errors/user.rs
@@ -106,6 +106,8 @@ pub enum UserErrors {
     ThemeAlreadyExists,
     #[error("Invalid field: {0} in lineage")]
     InvalidThemeLineage(String),
+    #[error("Missing required field: email_config")]
+    MissingEmailConfig,
 }
 
 impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorResponse> for UserErrors {
@@ -275,6 +277,9 @@ impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorRespon
             Self::InvalidThemeLineage(_) => {
                 AER::BadRequest(ApiError::new(sub_code, 55, self.get_error_message(), None))
             }
+            Self::MissingEmailConfig => {
+                AER::BadRequest(ApiError::new(sub_code, 56, self.get_error_message(), None))
+            }
         }
     }
 }
@@ -341,6 +346,7 @@ impl UserErrors {
             Self::InvalidThemeLineage(field_name) => {
                 format!("Invalid field: {} in lineage", field_name)
             }
+            Self::MissingEmailConfig => "Missing required field: email_config".to_string(),
         }
     }
 }
diff --git a/crates/router/src/core/recon.rs b/crates/router/src/core/recon.rs
index 13cf4c488ec..c2f289d4cc0 100644
--- a/crates/router/src/core/recon.rs
+++ b/crates/router/src/core/recon.rs
@@ -1,12 +1,14 @@
 use api_models::recon as recon_api;
 #[cfg(feature = "email")]
-use common_utils::ext_traits::AsyncExt;
+use common_utils::{ext_traits::AsyncExt, types::theme::ThemeLineage};
 use error_stack::ResultExt;
 #[cfg(feature = "email")]
 use masking::{ExposeInterface, PeekInterface, Secret};
 
 #[cfg(feature = "email")]
-use crate::{consts, services::email::types as email_types, types::domain};
+use crate::{
+    consts, services::email::types as email_types, types::domain, utils::user::theme as theme_utils,
+};
 use crate::{
     core::errors::{self, RouterResponse, UserErrors, UserResponse},
     services::{api as service_api, authentication},
@@ -35,6 +37,21 @@ pub async fn send_recon_request(
         let user_in_db = &auth_data.user;
         let merchant_id = auth_data.merchant_account.get_id().clone();
 
+        let theme = theme_utils::get_most_specific_theme_using_lineage(
+            &state.clone(),
+            ThemeLineage::Merchant {
+                tenant_id: state.tenant.tenant_id.clone(),
+                org_id: auth_data.merchant_account.get_org_id().clone(),
+                merchant_id: merchant_id.clone(),
+            },
+        )
+        .await
+        .change_context(errors::ApiErrorResponse::InternalServerError)
+        .attach_printable(format!(
+            "Failed to fetch theme for merchant_id = {:?}",
+            merchant_id
+        ))?;
+
         let user_email = user_in_db.email.clone();
         let email_contents = email_types::ProFeatureRequest {
             feature_name: consts::RECON_FEATURE_TAG.to_string(),
@@ -55,6 +72,10 @@ pub async fn send_recon_request(
                 consts::EMAIL_SUBJECT_DASHBOARD_FEATURE_REQUEST,
                 user_email.expose().peek()
             ),
+            theme_id: theme.as_ref().map(|theme| theme.theme_id.clone()),
+            theme_config: theme
+                .map(|theme| theme.email_config())
+                .unwrap_or(state.conf.theme.email_config.clone()),
         };
         state
             .email_client
@@ -141,7 +162,7 @@ pub async fn recon_merchant_account_update(
     let updated_merchant_account = db
         .update_merchant(
             key_manager_state,
-            auth.merchant_account,
+            auth.merchant_account.clone(),
             updated_merchant_account,
             &auth.key_store,
         )
@@ -154,6 +175,22 @@ pub async fn recon_merchant_account_update(
     #[cfg(feature = "email")]
     {
         let user_email = &req.user_email.clone();
+
+        let theme = theme_utils::get_most_specific_theme_using_lineage(
+            &state.clone(),
+            ThemeLineage::Merchant {
+                tenant_id: state.tenant.tenant_id,
+                org_id: auth.merchant_account.get_org_id().clone(),
+                merchant_id: merchant_id.clone(),
+            },
+        )
+        .await
+        .change_context(errors::ApiErrorResponse::InternalServerError)
+        .attach_printable(format!(
+            "Failed to fetch theme for merchant_id = {:?}",
+            merchant_id
+        ))?;
+
         let email_contents = email_types::ReconActivation {
             recipient_email: domain::UserEmail::from_pii_email(user_email.clone())
                 .change_context(errors::ApiErrorResponse::InternalServerError)
@@ -164,6 +201,10 @@ pub async fn recon_merchant_account_update(
                 .change_context(errors::ApiErrorResponse::InternalServerError)
                 .attach_printable("Failed to form username")?,
             subject: consts::EMAIL_SUBJECT_APPROVAL_RECON_REQUEST,
+            theme_id: theme.as_ref().map(|theme| theme.theme_id.clone()),
+            theme_config: theme
+                .map(|theme| theme.email_config())
+                .unwrap_or(state.conf.theme.email_config.clone()),
         };
         if req.recon_status == enums::ReconStatus::Active {
             let _ = state
diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs
index 629476b2591..642473bdf8e 100644
--- a/crates/router/src/core/user.rs
+++ b/crates/router/src/core/user.rs
@@ -42,7 +42,10 @@ use crate::{
     routes::{app::ReqState, SessionState},
     services::{authentication as auth, authorization::roles, openidconnect, ApplicationResponse},
     types::{domain, transformers::ForeignInto},
-    utils::{self, user::two_factor_auth as tfa_utils},
+    utils::{
+        self,
+        user::{theme as theme_utils, two_factor_auth as tfa_utils},
+    },
 };
 
 pub mod dashboard_metadata;
@@ -55,6 +58,7 @@ pub async fn signup_with_merchant_id(
     state: SessionState,
     request: user_api::SignUpWithMerchantIdRequest,
     auth_id: Option<String>,
+    theme_id: Option<String>,
 ) -> UserResponse<user_api::SignUpWithMerchantIdResponse> {
     let new_user = domain::NewUser::try_from(request.clone())?;
     new_user
@@ -75,12 +79,18 @@ pub async fn signup_with_merchant_id(
         )
         .await?;
 
+    let theme = theme_utils::get_theme_using_optional_theme_id(&state, theme_id).await?;
+
     let email_contents = email_types::ResetPassword {
         recipient_email: user_from_db.get_email().try_into()?,
         user_name: domain::UserName::new(user_from_db.get_name())?,
         settings: state.conf.clone(),
         subject: consts::user::EMAIL_SUBJECT_RESET_PASSWORD,
         auth_id,
+        theme_id: theme.as_ref().map(|theme| theme.theme_id.clone()),
+        theme_config: theme
+            .map(|theme| theme.email_config())
+            .unwrap_or(state.conf.theme.email_config.clone()),
     };
 
     let send_email_result = state
@@ -112,6 +122,13 @@ pub async fn get_user_details(
     .await
     .change_context(UserErrors::InternalServerError)?;
 
+    let theme = theme_utils::get_most_specific_theme_using_token_and_min_entity(
+        &state,
+        &user_from_token,
+        EntityType::Profile,
+    )
+    .await?;
+
     Ok(ApplicationResponse::Json(
         user_api::GetUserDetailsResponse {
             merchant_id: user_from_token.merchant_id,
@@ -125,6 +142,7 @@ pub async fn get_user_details(
             recovery_codes_left: user.get_recovery_codes().map(|codes| codes.len()),
             profile_id: user_from_token.profile_id,
             entity_type: role_info.get_entity_type(),
+            theme_id: theme.map(|theme| theme.theme_id),
         },
     ))
 }
@@ -194,6 +212,7 @@ pub async fn connect_account(
     state: SessionState,
     request: user_api::ConnectAccountRequest,
     auth_id: Option<String>,
+    theme_id: Option<String>,
 ) -> UserResponse<user_api::ConnectAccountResponse> {
     let find_user = state
         .global_store
@@ -203,12 +222,18 @@ pub async fn connect_account(
     if let Ok(found_user) = find_user {
         let user_from_db: domain::UserFromStorage = found_user.into();
 
+        let theme = theme_utils::get_theme_using_optional_theme_id(&state, theme_id).await?;
+
         let email_contents = email_types::MagicLink {
             recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?,
             settings: state.conf.clone(),
             user_name: domain::UserName::new(user_from_db.get_name())?,
             subject: consts::user::EMAIL_SUBJECT_MAGIC_LINK,
             auth_id,
+            theme_id: theme.as_ref().map(|theme| theme.theme_id.clone()),
+            theme_config: theme
+                .map(|theme| theme.email_config())
+                .unwrap_or(state.conf.theme.email_config.clone()),
         };
 
         let send_email_result = state
@@ -253,11 +278,17 @@ pub async fn connect_account(
             )
             .await?;
 
+        let theme = theme_utils::get_theme_using_optional_theme_id(&state, theme_id).await?;
+
         let magic_link_email = email_types::VerifyEmail {
             recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?,
             settings: state.conf.clone(),
             subject: consts::user::EMAIL_SUBJECT_SIGNUP,
             auth_id,
+            theme_id: theme.as_ref().map(|theme| theme.theme_id.clone()),
+            theme_config: theme
+                .map(|theme| theme.email_config())
+                .unwrap_or(state.conf.theme.email_config.clone()),
         };
 
         let magic_link_result = state
@@ -270,20 +301,22 @@ pub async fn connect_account(
 
         logger::info!(?magic_link_result);
 
-        let welcome_to_community_email = email_types::WelcomeToCommunity {
-            recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?,
-            subject: consts::user::EMAIL_SUBJECT_WELCOME_TO_COMMUNITY,
-        };
+        if state.tenant.tenant_id.get_string_repr() == common_utils::consts::DEFAULT_TENANT {
+            let welcome_to_community_email = email_types::WelcomeToCommunity {
+                recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?,
+                subject: consts::user::EMAIL_SUBJECT_WELCOME_TO_COMMUNITY,
+            };
 
-        let welcome_email_result = state
-            .email_client
-            .compose_and_send_email(
-                Box::new(welcome_to_community_email),
-                state.conf.proxy.https_url.as_ref(),
-            )
-            .await;
+            let welcome_email_result = state
+                .email_client
+                .compose_and_send_email(
+                    Box::new(welcome_to_community_email),
+                    state.conf.proxy.https_url.as_ref(),
+                )
+                .await;
 
-        logger::info!(?welcome_email_result);
+            logger::info!(?welcome_email_result);
+        }
 
         return Ok(ApplicationResponse::Json(
             user_api::ConnectAccountResponse {
@@ -371,6 +404,7 @@ pub async fn forgot_password(
     state: SessionState,
     request: user_api::ForgotPasswordRequest,
     auth_id: Option<String>,
+    theme_id: Option<String>,
 ) -> UserResponse<()> {
     let user_email = domain::UserEmail::from_pii_email(request.email)?;
 
@@ -387,12 +421,18 @@ pub async fn forgot_password(
         })
         .map(domain::UserFromStorage::from)?;
 
+    let theme = theme_utils::get_theme_using_optional_theme_id(&state, theme_id).await?;
+
     let email_contents = email_types::ResetPassword {
         recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?,
         settings: state.conf.clone(),
         user_name: domain::UserName::new(user_from_db.get_name())?,
         subject: consts::user::EMAIL_SUBJECT_RESET_PASSWORD,
         auth_id,
+        theme_id: theme.as_ref().map(|theme| theme.theme_id.clone()),
+        theme_config: theme
+            .map(|theme| theme.email_config())
+            .unwrap_or(state.conf.theme.email_config.clone()),
     };
 
     state
@@ -782,6 +822,13 @@ async fn handle_existing_user_invitation(
             },
         };
 
+        let theme = theme_utils::get_most_specific_theme_using_token_and_min_entity(
+            state,
+            user_from_token,
+            role_info.get_entity_type(),
+        )
+        .await?;
+
         let email_contents = email_types::InviteUser {
             recipient_email: invitee_email,
             user_name: domain::UserName::new(invitee_user_from_db.get_name())?,
@@ -789,6 +836,10 @@ async fn handle_existing_user_invitation(
             subject: consts::user::EMAIL_SUBJECT_INVITATION,
             entity,
             auth_id: auth_id.clone(),
+            theme_id: theme.as_ref().map(|theme| theme.theme_id.clone()),
+            theme_config: theme
+                .map(|theme| theme.email_config())
+                .unwrap_or(state.conf.theme.email_config.clone()),
         };
 
         is_email_sent = state
@@ -927,6 +978,13 @@ async fn handle_new_user_invitation(
             },
         };
 
+        let theme = theme_utils::get_most_specific_theme_using_token_and_min_entity(
+            state,
+            user_from_token,
+            role_info.get_entity_type(),
+        )
+        .await?;
+
         let email_contents = email_types::InviteUser {
             recipient_email: invitee_email,
             user_name: domain::UserName::new(new_user.get_name())?,
@@ -934,6 +992,10 @@ async fn handle_new_user_invitation(
             subject: consts::user::EMAIL_SUBJECT_INVITATION,
             entity,
             auth_id: auth_id.clone(),
+            theme_id: theme.as_ref().map(|theme| theme.theme_id.clone()),
+            theme_config: theme
+                .map(|theme| theme.email_config())
+                .unwrap_or(state.conf.theme.email_config.clone()),
         };
         let send_email_result = state
             .email_client
@@ -1055,6 +1117,21 @@ pub async fn resend_invite(
         .get_entity_id_and_type()
         .ok_or(UserErrors::InternalServerError)?;
 
+    let invitee_role_info = roles::RoleInfo::from_role_id_and_org_id(
+        &state,
+        &user_role.role_id,
+        &user_from_token.org_id,
+    )
+    .await
+    .change_context(UserErrors::InternalServerError)?;
+
+    let theme = theme_utils::get_most_specific_theme_using_token_and_min_entity(
+        &state,
+        &user_from_token,
+        invitee_role_info.get_entity_type(),
+    )
+    .await?;
+
     let email_contents = email_types::InviteUser {
         recipient_email: invitee_email,
         user_name: domain::UserName::new(user.get_name())?,
@@ -1065,6 +1142,10 @@ pub async fn resend_invite(
             entity_type,
         },
         auth_id: auth_id.clone(),
+        theme_id: theme.as_ref().map(|theme| theme.theme_id.clone()),
+        theme_config: theme
+            .map(|theme| theme.email_config())
+            .unwrap_or(state.conf.theme.email_config.clone()),
     };
 
     state
@@ -1666,6 +1747,7 @@ pub async fn send_verification_mail(
     state: SessionState,
     req: user_api::SendVerifyEmailRequest,
     auth_id: Option<String>,
+    theme_id: Option<String>,
 ) -> UserResponse<()> {
     let user_email = domain::UserEmail::try_from(req.email)?;
     let user = state
@@ -1684,11 +1766,17 @@ pub async fn send_verification_mail(
         return Err(UserErrors::UserAlreadyVerified.into());
     }
 
+    let theme = theme_utils::get_theme_using_optional_theme_id(&state, theme_id).await?;
+
     let email_contents = email_types::VerifyEmail {
         recipient_email: domain::UserEmail::from_pii_email(user.email)?,
         settings: state.conf.clone(),
         subject: consts::user::EMAIL_SUBJECT_SIGNUP,
         auth_id,
+        theme_id: theme.as_ref().map(|theme| theme.theme_id.clone()),
+        theme_config: theme
+            .map(|theme| theme.email_config())
+            .unwrap_or(state.conf.theme.email_config.clone()),
     };
 
     state
diff --git a/crates/router/src/core/user/dashboard_metadata.rs b/crates/router/src/core/user/dashboard_metadata.rs
index a64b0f39dc0..88380363b4e 100644
--- a/crates/router/src/core/user/dashboard_metadata.rs
+++ b/crates/router/src/core/user/dashboard_metadata.rs
@@ -1,4 +1,6 @@
 use api_models::user::dashboard_metadata::{self as api, GetMultipleMetaDataPayload};
+#[cfg(feature = "email")]
+use common_enums::EntityType;
 use diesel_models::{
     enums::DashboardMetadata as DBEnum, user::dashboard_metadata::DashboardMetadata,
 };
@@ -8,8 +10,6 @@ use masking::ExposeInterface;
 #[cfg(feature = "email")]
 use router_env::logger;
 
-#[cfg(feature = "email")]
-use crate::services::email::types as email_types;
 use crate::{
     core::errors::{UserErrors, UserResponse, UserResult},
     routes::{app::ReqState, SessionState},
@@ -17,6 +17,8 @@ use crate::{
     types::domain::{self, user::dashboard_metadata as types, MerchantKeyStore},
     utils::user::dashboard_metadata as utils,
 };
+#[cfg(feature = "email")]
+use crate::{services::email::types as email_types, utils::user::theme as theme_utils};
 
 pub async fn set_metadata(
     state: SessionState,
@@ -476,7 +478,21 @@ async fn insert_metadata(
                     .expose();
 
                 if utils::is_prod_email_required(&data, user_email) {
-                    let email_contents = email_types::BizEmailProd::new(state, data)?;
+                    let theme = theme_utils::get_most_specific_theme_using_token_and_min_entity(
+                        state,
+                        &user,
+                        EntityType::Merchant,
+                    )
+                    .await?;
+
+                    let email_contents = email_types::BizEmailProd::new(
+                        state,
+                        data,
+                        theme.as_ref().map(|theme| theme.theme_id.clone()),
+                        theme
+                            .map(|theme| theme.email_config())
+                            .unwrap_or(state.conf.theme.email_config.clone()),
+                    )?;
                     let send_email_result = state
                         .email_client
                         .compose_and_send_email(
diff --git a/crates/router/src/core/user/theme.rs b/crates/router/src/core/user/theme.rs
index dbf11256a50..27b342d9053 100644
--- a/crates/router/src/core/user/theme.rs
+++ b/crates/router/src/core/user/theme.rs
@@ -38,6 +38,7 @@ pub async fn get_theme_using_lineage(
         .change_context(UserErrors::InternalServerError)?;
 
     Ok(ApplicationResponse::Json(theme_api::GetThemeResponse {
+        email_config: theme.email_config(),
         theme_id: theme.theme_id,
         theme_name: theme.theme_name,
         entity_type: theme.entity_type,
@@ -71,6 +72,7 @@ pub async fn get_theme_using_theme_id(
         .change_context(UserErrors::InternalServerError)?;
 
     Ok(ApplicationResponse::Json(theme_api::GetThemeResponse {
+        email_config: theme.email_config(),
         theme_id: theme.theme_id,
         theme_name: theme.theme_name,
         entity_type: theme.entity_type,
@@ -113,10 +115,19 @@ pub async fn create_theme(
 ) -> UserResponse<theme_api::GetThemeResponse> {
     theme_utils::validate_lineage(&state, &request.lineage).await?;
 
+    let email_config = if cfg!(feature = "email") {
+        request.email_config.ok_or(UserErrors::MissingEmailConfig)?
+    } else {
+        request
+            .email_config
+            .unwrap_or(state.conf.theme.email_config.clone())
+    };
+
     let new_theme = ThemeNew::new(
         Uuid::new_v4().to_string(),
         request.theme_name,
         request.lineage,
+        email_config,
     );
 
     let db_theme = state
@@ -147,6 +158,7 @@ pub async fn create_theme(
         .change_context(UserErrors::InternalServerError)?;
 
     Ok(ApplicationResponse::Json(theme_api::GetThemeResponse {
+        email_config: db_theme.email_config(),
         theme_id: db_theme.theme_id,
         entity_type: db_theme.entity_type,
         tenant_id: db_theme.tenant_id,
@@ -195,6 +207,7 @@ pub async fn update_theme(
         .change_context(UserErrors::InternalServerError)?;
 
     Ok(ApplicationResponse::Json(theme_api::GetThemeResponse {
+        email_config: db_theme.email_config(),
         theme_id: db_theme.theme_id,
         entity_type: db_theme.entity_type,
         tenant_id: db_theme.tenant_id,
diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs
index 2fd30a3610b..a7e71284aaf 100644
--- a/crates/router/src/db/kafka_store.rs
+++ b/crates/router/src/db/kafka_store.rs
@@ -3755,6 +3755,15 @@ impl ThemeInterface for KafkaStore {
         self.diesel_store.find_theme_by_theme_id(theme_id).await
     }
 
+    async fn find_most_specific_theme_in_lineage(
+        &self,
+        lineage: ThemeLineage,
+    ) -> CustomResult<diesel_models::user::theme::Theme, errors::StorageError> {
+        self.diesel_store
+            .find_most_specific_theme_in_lineage(lineage)
+            .await
+    }
+
     async fn find_theme_by_lineage(
         &self,
         lineage: ThemeLineage,
diff --git a/crates/router/src/db/user/theme.rs b/crates/router/src/db/user/theme.rs
index 9b55a98d0ad..51c0a52b3a0 100644
--- a/crates/router/src/db/user/theme.rs
+++ b/crates/router/src/db/user/theme.rs
@@ -21,6 +21,11 @@ pub trait ThemeInterface {
         theme_id: String,
     ) -> CustomResult<storage::Theme, errors::StorageError>;
 
+    async fn find_most_specific_theme_in_lineage(
+        &self,
+        lineage: ThemeLineage,
+    ) -> CustomResult<storage::Theme, errors::StorageError>;
+
     async fn find_theme_by_lineage(
         &self,
         lineage: ThemeLineage,
@@ -56,6 +61,16 @@ impl ThemeInterface for Store {
             .map_err(|error| report!(errors::StorageError::from(error)))
     }
 
+    async fn find_most_specific_theme_in_lineage(
+        &self,
+        lineage: ThemeLineage,
+    ) -> CustomResult<storage::Theme, errors::StorageError> {
+        let conn = connection::pg_connection_read(self).await?;
+        storage::Theme::find_most_specific_theme_in_lineage(&conn, lineage)
+            .await
+            .map_err(|error| report!(errors::StorageError::from(error)))
+    }
+
     async fn find_theme_by_lineage(
         &self,
         lineage: ThemeLineage,
@@ -80,13 +95,12 @@ impl ThemeInterface for Store {
 
 fn check_theme_with_lineage(theme: &storage::Theme, lineage: &ThemeLineage) -> bool {
     match lineage {
-        // TODO: Add back Tenant variant when we introduce Tenant Variant in EntityType
-        // ThemeLineage::Tenant { tenant_id } => {
-        //     &theme.tenant_id == tenant_id
-        //         && theme.org_id.is_none()
-        //         && theme.merchant_id.is_none()
-        //         && theme.profile_id.is_none()
-        // }
+        ThemeLineage::Tenant { tenant_id } => {
+            &theme.tenant_id == tenant_id
+                && theme.org_id.is_none()
+                && theme.merchant_id.is_none()
+                && theme.profile_id.is_none()
+        }
         ThemeLineage::Organization { tenant_id, org_id } => {
             &theme.tenant_id == tenant_id
                 && theme
@@ -174,6 +188,11 @@ impl ThemeInterface for MockDb {
             last_modified_at: new_theme.last_modified_at,
             entity_type: new_theme.entity_type,
             theme_name: new_theme.theme_name,
+            email_primary_color: new_theme.email_primary_color,
+            email_foreground_color: new_theme.email_foreground_color,
+            email_background_color: new_theme.email_background_color,
+            email_entity_name: new_theme.email_entity_name,
+            email_entity_logo_url: new_theme.email_entity_logo_url,
         };
         themes.push(theme.clone());
 
@@ -198,6 +217,27 @@ impl ThemeInterface for MockDb {
             )
     }
 
+    async fn find_most_specific_theme_in_lineage(
+        &self,
+        lineage: ThemeLineage,
+    ) -> CustomResult<storage::Theme, errors::StorageError> {
+        let themes = self.themes.lock().await;
+        let lineages = lineage.get_same_and_higher_lineages();
+
+        themes
+            .iter()
+            .filter(|theme| {
+                lineages
+                    .iter()
+                    .any(|lineage| check_theme_with_lineage(theme, lineage))
+            })
+            .min_by_key(|theme| theme.entity_type)
+            .ok_or(
+                errors::StorageError::ValueNotFound("No theme found in lineage".to_string()).into(),
+            )
+            .cloned()
+    }
+
     async fn find_theme_by_lineage(
         &self,
         lineage: ThemeLineage,
diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs
index ca7c4847ad4..5f04581fbb3 100644
--- a/crates/router/src/routes/app.rs
+++ b/crates/router/src/routes/app.rs
@@ -369,7 +369,7 @@ impl AppState {
             let email_client = Arc::new(create_email_client(&conf).await);
 
             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 theme_storage_client = conf.theme.storage.get_file_storage_client().await;
 
             let grpc_client = conf.grpc_client.get_grpc_client_interface().await;
 
diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs
index c32746c8718..d075048ddb6 100644
--- a/crates/router/src/routes/user.rs
+++ b/crates/router/src/routes/user.rs
@@ -41,18 +41,23 @@ pub async fn user_signup_with_merchant_id(
     state: web::Data<AppState>,
     http_req: HttpRequest,
     json_payload: web::Json<user_api::SignUpWithMerchantIdRequest>,
-    query: web::Query<user_api::AuthIdQueryParam>,
+    query: web::Query<user_api::AuthIdAndThemeIdQueryParam>,
 ) -> HttpResponse {
     let flow = Flow::UserSignUpWithMerchantId;
     let req_payload = json_payload.into_inner();
-    let auth_id = query.into_inner().auth_id;
+    let query_params = query.into_inner();
     Box::pin(api::server_wrap(
         flow.clone(),
         state,
         &http_req,
         req_payload.clone(),
         |state, _, req_body, _| {
-            user_core::signup_with_merchant_id(state, req_body, auth_id.clone())
+            user_core::signup_with_merchant_id(
+                state,
+                req_body,
+                query_params.auth_id.clone(),
+                query_params.theme_id.clone(),
+            )
         },
         &auth::AdminApiAuth,
         api_locking::LockAction::NotApplicable,
@@ -107,17 +112,24 @@ pub async fn user_connect_account(
     state: web::Data<AppState>,
     http_req: HttpRequest,
     json_payload: web::Json<user_api::ConnectAccountRequest>,
-    query: web::Query<user_api::AuthIdQueryParam>,
+    query: web::Query<user_api::AuthIdAndThemeIdQueryParam>,
 ) -> HttpResponse {
     let flow = Flow::UserConnectAccount;
     let req_payload = json_payload.into_inner();
-    let auth_id = query.into_inner().auth_id;
+    let query_params = query.into_inner();
     Box::pin(api::server_wrap(
         flow.clone(),
         state,
         &http_req,
         req_payload.clone(),
-        |state, _: (), req_body, _| user_core::connect_account(state, req_body, auth_id.clone()),
+        |state, _: (), req_body, _| {
+            user_core::connect_account(
+                state,
+                req_body,
+                query_params.auth_id.clone(),
+                query_params.theme_id.clone(),
+            )
+        },
         &auth::NoAuth,
         api_locking::LockAction::NotApplicable,
     ))
@@ -381,16 +393,23 @@ pub async fn forgot_password(
     state: web::Data<AppState>,
     req: HttpRequest,
     payload: web::Json<user_api::ForgotPasswordRequest>,
-    query: web::Query<user_api::AuthIdQueryParam>,
+    query: web::Query<user_api::AuthIdAndThemeIdQueryParam>,
 ) -> HttpResponse {
     let flow = Flow::ForgotPassword;
-    let auth_id = query.into_inner().auth_id;
+    let query_params = query.into_inner();
     Box::pin(api::server_wrap(
         flow,
         state.clone(),
         &req,
         payload.into_inner(),
-        |state, _: (), payload, _| user_core::forgot_password(state, payload, auth_id.clone()),
+        |state, _: (), payload, _| {
+            user_core::forgot_password(
+                state,
+                payload,
+                query_params.auth_id.clone(),
+                query_params.theme_id.clone(),
+            )
+        },
         &auth::NoAuth,
         api_locking::LockAction::NotApplicable,
     ))
@@ -420,7 +439,7 @@ pub async fn invite_multiple_user(
     state: web::Data<AppState>,
     req: HttpRequest,
     payload: web::Json<Vec<user_api::InviteUserRequest>>,
-    auth_id_query_param: web::Query<user_api::AuthIdQueryParam>,
+    auth_id_query_param: web::Query<user_api::AuthIdAndThemeIdQueryParam>,
 ) -> HttpResponse {
     let flow = Flow::InviteMultipleUser;
     let auth_id = auth_id_query_param.into_inner().auth_id;
@@ -445,7 +464,7 @@ pub async fn resend_invite(
     state: web::Data<AppState>,
     req: HttpRequest,
     payload: web::Json<user_api::ReInviteUserRequest>,
-    query: web::Query<user_api::AuthIdQueryParam>,
+    query: web::Query<user_api::AuthIdAndThemeIdQueryParam>,
 ) -> HttpResponse {
     let flow = Flow::ReInviteUser;
     let auth_id = query.into_inner().auth_id;
@@ -512,17 +531,22 @@ pub async fn verify_email_request(
     state: web::Data<AppState>,
     http_req: HttpRequest,
     json_payload: web::Json<user_api::SendVerifyEmailRequest>,
-    query: web::Query<user_api::AuthIdQueryParam>,
+    query: web::Query<user_api::AuthIdAndThemeIdQueryParam>,
 ) -> HttpResponse {
     let flow = Flow::VerifyEmailRequest;
-    let auth_id = query.into_inner().auth_id;
+    let query_params = query.into_inner();
     Box::pin(api::server_wrap(
         flow,
         state.clone(),
         &http_req,
         json_payload.into_inner(),
         |state, _: (), req_body, _| {
-            user_core::send_verification_mail(state, req_body, auth_id.clone())
+            user_core::send_verification_mail(
+                state,
+                req_body,
+                query_params.auth_id.clone(),
+                query_params.theme_id.clone(),
+            )
         },
         &auth::NoAuth,
         api_locking::LockAction::NotApplicable,
diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs
index 66e730f0824..128250a61aa 100644
--- a/crates/router/src/services/email/types.rs
+++ b/crates/router/src/services/email/types.rs
@@ -1,6 +1,6 @@
 use api_models::user::dashboard_metadata::ProdIntent;
 use common_enums::EntityType;
-use common_utils::{errors::CustomResult, pii};
+use common_utils::{errors::CustomResult, pii, types::theme::EmailThemeConfig};
 use error_stack::ResultExt;
 use external_services::email::{EmailContents, EmailData, EmailError};
 use masking::{ExposeInterface, Secret};
@@ -212,13 +212,17 @@ pub fn get_link_with_token(
     token: impl std::fmt::Display,
     action: impl std::fmt::Display,
     auth_id: &Option<impl std::fmt::Display>,
+    theme_id: &Option<impl std::fmt::Display>,
 ) -> String {
-    let email_url = format!("{base_url}/user/{action}?token={token}");
+    let mut email_url = format!("{base_url}/user/{action}?token={token}");
     if let Some(auth_id) = auth_id {
-        format!("{email_url}&auth_id={auth_id}")
-    } else {
-        email_url
+        email_url = format!("{email_url}&auth_id={auth_id}");
     }
+    if let Some(theme_id) = theme_id {
+        email_url = format!("{email_url}&theme_id={theme_id}");
+    }
+
+    email_url
 }
 
 pub struct VerifyEmail {
@@ -226,6 +230,8 @@ pub struct VerifyEmail {
     pub settings: std::sync::Arc<configs::Settings>,
     pub subject: &'static str,
     pub auth_id: Option<String>,
+    pub theme_id: Option<String>,
+    pub theme_config: EmailThemeConfig,
 }
 
 /// Currently only HTML is supported
@@ -246,6 +252,7 @@ impl EmailData for VerifyEmail {
             token,
             "verify_email",
             &self.auth_id,
+            &self.theme_id,
         );
 
         let body = html::get_html_body(EmailBody::Verify {
@@ -266,6 +273,8 @@ pub struct ResetPassword {
     pub settings: std::sync::Arc<configs::Settings>,
     pub subject: &'static str,
     pub auth_id: Option<String>,
+    pub theme_id: Option<String>,
+    pub theme_config: EmailThemeConfig,
 }
 
 #[async_trait::async_trait]
@@ -285,6 +294,7 @@ impl EmailData for ResetPassword {
             token,
             "set_password",
             &self.auth_id,
+            &self.theme_id,
         );
 
         let body = html::get_html_body(EmailBody::Reset {
@@ -306,6 +316,8 @@ pub struct MagicLink {
     pub settings: std::sync::Arc<configs::Settings>,
     pub subject: &'static str,
     pub auth_id: Option<String>,
+    pub theme_id: Option<String>,
+    pub theme_config: EmailThemeConfig,
 }
 
 #[async_trait::async_trait]
@@ -325,6 +337,7 @@ impl EmailData for MagicLink {
             token,
             "verify_email",
             &self.auth_id,
+            &self.theme_id,
         );
 
         let body = html::get_html_body(EmailBody::MagicLink {
@@ -347,6 +360,8 @@ pub struct InviteUser {
     pub subject: &'static str,
     pub entity: Entity,
     pub auth_id: Option<String>,
+    pub theme_id: Option<String>,
+    pub theme_config: EmailThemeConfig,
 }
 
 #[async_trait::async_trait]
@@ -366,6 +381,7 @@ impl EmailData for InviteUser {
             token,
             "accept_invite_from_email",
             &self.auth_id,
+            &self.theme_id,
         );
         let body = html::get_html_body(EmailBody::AcceptInviteFromEmail {
             link: invite_user_link,
@@ -384,6 +400,8 @@ pub struct ReconActivation {
     pub recipient_email: domain::UserEmail,
     pub user_name: domain::UserName,
     pub subject: &'static str,
+    pub theme_id: Option<String>,
+    pub theme_config: EmailThemeConfig,
 }
 
 #[async_trait::async_trait]
@@ -410,10 +428,17 @@ pub struct BizEmailProd {
     pub business_website: String,
     pub settings: std::sync::Arc<configs::Settings>,
     pub subject: &'static str,
+    pub theme_id: Option<String>,
+    pub theme_config: EmailThemeConfig,
 }
 
 impl BizEmailProd {
-    pub fn new(state: &SessionState, data: ProdIntent) -> UserResult<Self> {
+    pub fn new(
+        state: &SessionState,
+        data: ProdIntent,
+        theme_id: Option<String>,
+        theme_config: EmailThemeConfig,
+    ) -> UserResult<Self> {
         Ok(Self {
             recipient_email: domain::UserEmail::from_pii_email(
                 state.conf.email.prod_intent_recipient_email.clone(),
@@ -428,6 +453,8 @@ impl BizEmailProd {
                 .unwrap_or(common_enums::CountryAlpha2::AD)
                 .to_string(),
             business_website: data.business_website.unwrap_or_default(),
+            theme_id,
+            theme_config,
         })
     }
 }
@@ -458,6 +485,8 @@ pub struct ProFeatureRequest {
     pub user_name: domain::UserName,
     pub user_email: domain::UserEmail,
     pub subject: String,
+    pub theme_id: Option<String>,
+    pub theme_config: EmailThemeConfig,
 }
 
 #[async_trait::async_trait]
@@ -486,6 +515,8 @@ pub struct ApiKeyExpiryReminder {
     pub expires_in: u8,
     pub api_key_name: String,
     pub prefix: String,
+    pub theme_id: Option<String>,
+    pub theme_config: EmailThemeConfig,
 }
 
 #[async_trait::async_trait]
diff --git a/crates/router/src/utils/user/theme.rs b/crates/router/src/utils/user/theme.rs
index 13452380d9d..1c8b76989ae 100644
--- a/crates/router/src/utils/user/theme.rs
+++ b/crates/router/src/utils/user/theme.rs
@@ -1,12 +1,15 @@
 use std::path::PathBuf;
 
-use common_utils::{id_type, types::theme::ThemeLineage};
+use common_enums::EntityType;
+use common_utils::{ext_traits::AsyncExt, id_type, types::theme::ThemeLineage};
+use diesel_models::user::theme::Theme;
 use error_stack::ResultExt;
 use hyperswitch_domain_models::merchant_key_store::MerchantKeyStore;
 
 use crate::{
     core::errors::{StorageErrorExt, UserErrors, UserResult},
     routes::SessionState,
+    services::authentication::UserFromToken,
 };
 
 fn get_theme_dir_key(theme_id: &str) -> PathBuf {
@@ -54,6 +57,10 @@ pub async fn upload_file_to_theme_bucket(
 
 pub async fn validate_lineage(state: &SessionState, lineage: &ThemeLineage) -> UserResult<()> {
     match lineage {
+        ThemeLineage::Tenant { tenant_id } => {
+            validate_tenant(state, tenant_id)?;
+            Ok(())
+        }
         ThemeLineage::Organization { tenant_id, org_id } => {
             validate_tenant(state, tenant_id)?;
             validate_org(state, org_id).await?;
@@ -96,8 +103,8 @@ async fn validate_org(state: &SessionState, org_id: &id_type::OrganizationId) ->
         .store
         .find_organization_by_org_id(org_id)
         .await
-        .to_not_found_response(UserErrors::InvalidThemeLineage("org_id".to_string()))?;
-    Ok(())
+        .to_not_found_response(UserErrors::InvalidThemeLineage("org_id".to_string()))
+        .map(|_| ())
 }
 
 async fn validate_merchant_and_get_key_store(
@@ -153,6 +160,67 @@ async fn validate_profile(
             profile_id,
         )
         .await
-        .to_not_found_response(UserErrors::InvalidThemeLineage("profile_id".to_string()))?;
-    Ok(())
+        .to_not_found_response(UserErrors::InvalidThemeLineage("profile_id".to_string()))
+        .map(|_| ())
+}
+
+pub async fn get_most_specific_theme_using_token_and_min_entity(
+    state: &SessionState,
+    user_from_token: &UserFromToken,
+    min_entity: EntityType,
+) -> UserResult<Option<Theme>> {
+    get_most_specific_theme_using_lineage(
+        state,
+        ThemeLineage::new(
+            min_entity,
+            user_from_token
+                .tenant_id
+                .clone()
+                .unwrap_or(state.tenant.tenant_id.clone()),
+            user_from_token.org_id.clone(),
+            user_from_token.merchant_id.clone(),
+            user_from_token.profile_id.clone(),
+        ),
+    )
+    .await
+}
+
+pub async fn get_most_specific_theme_using_lineage(
+    state: &SessionState,
+    lineage: ThemeLineage,
+) -> UserResult<Option<Theme>> {
+    match state
+        .global_store
+        .find_most_specific_theme_in_lineage(lineage)
+        .await
+    {
+        Ok(theme) => Ok(Some(theme)),
+        Err(e) => {
+            if e.current_context().is_db_not_found() {
+                Ok(None)
+            } else {
+                Err(e.change_context(UserErrors::InternalServerError))
+            }
+        }
+    }
+}
+
+pub async fn get_theme_using_optional_theme_id(
+    state: &SessionState,
+    theme_id: Option<String>,
+) -> UserResult<Option<Theme>> {
+    match theme_id
+        .async_map(|theme_id| state.global_store.find_theme_by_theme_id(theme_id))
+        .await
+        .transpose()
+    {
+        Ok(theme) => Ok(theme),
+        Err(e) => {
+            if e.current_context().is_db_not_found() {
+                Ok(None)
+            } else {
+                Err(e.change_context(UserErrors::InternalServerError))
+            }
+        }
+    }
 }
diff --git a/crates/router/src/workflows/api_key_expiry.rs b/crates/router/src/workflows/api_key_expiry.rs
index bf6100179bf..cdc4959c456 100644
--- a/crates/router/src/workflows/api_key_expiry.rs
+++ b/crates/router/src/workflows/api_key_expiry.rs
@@ -1,4 +1,4 @@
-use common_utils::{errors::ValidationError, ext_traits::ValueExt};
+use common_utils::{errors::ValidationError, ext_traits::ValueExt, types::theme::ThemeLineage};
 use diesel_models::{
     enums as storage_enums, process_tracker::business_status, ApiKeyExpiryTrackingData,
 };
@@ -11,7 +11,7 @@ use crate::{
     routes::{metrics, SessionState},
     services::email::types::ApiKeyExpiryReminder,
     types::{api, domain::UserEmail, storage},
-    utils::OptionExt,
+    utils::{user::theme as theme_utils, OptionExt},
 };
 
 pub struct ApiKeyExpiryWorkflow;
@@ -48,6 +48,7 @@ impl ProcessTrackerWorkflow<SessionState> for ApiKeyExpiryWorkflow {
 
         let email_id = merchant_account
             .merchant_details
+            .clone()
             .parse_value::<api::MerchantDetails>("MerchantDetails")?
             .primary_email
             .ok_or(errors::ProcessTrackerError::EValidationError(
@@ -73,6 +74,20 @@ impl ProcessTrackerWorkflow<SessionState> for ApiKeyExpiryWorkflow {
             )
             .ok_or(errors::ProcessTrackerError::EApiErrorResponse)?;
 
+        let theme = theme_utils::get_most_specific_theme_using_lineage(
+            state,
+            ThemeLineage::Merchant {
+                tenant_id: state.tenant.tenant_id.clone(),
+                org_id: merchant_account.get_org_id().clone(),
+                merchant_id: merchant_account.get_id().clone(),
+            },
+        )
+        .await
+        .map_err(|err| {
+            logger::error!(?err, "Failed to get theme");
+            errors::ProcessTrackerError::EApiErrorResponse
+        })?;
+
         let email_contents = ApiKeyExpiryReminder {
             recipient_email: UserEmail::from_pii_email(email_id).map_err(|error| {
                 logger::error!(
@@ -85,6 +100,10 @@ impl ProcessTrackerWorkflow<SessionState> for ApiKeyExpiryWorkflow {
             expires_in: *expires_in,
             api_key_name,
             prefix,
+            theme_id: theme.as_ref().map(|theme| theme.theme_id.clone()),
+            theme_config: theme
+                .map(|theme| theme.email_config())
+                .unwrap_or(state.conf.theme.email_config.clone()),
         };
 
         state
diff --git a/migrations/2024-12-05-131123_add-email-theme-data-in-themes/down.sql b/migrations/2024-12-05-131123_add-email-theme-data-in-themes/down.sql
new file mode 100644
index 00000000000..34732b5dd69
--- /dev/null
+++ b/migrations/2024-12-05-131123_add-email-theme-data-in-themes/down.sql
@@ -0,0 +1,6 @@
+-- This file should undo anything in `up.sql`
+ALTER TABLE themes DROP COLUMN IF EXISTS email_primary_color;
+ALTER TABLE themes DROP COLUMN IF EXISTS email_foreground_color;
+ALTER TABLE themes DROP COLUMN IF EXISTS email_background_color;
+ALTER TABLE themes DROP COLUMN IF EXISTS email_entity_name;
+ALTER TABLE themes DROP COLUMN IF EXISTS email_entity_logo_url;
diff --git a/migrations/2024-12-05-131123_add-email-theme-data-in-themes/up.sql b/migrations/2024-12-05-131123_add-email-theme-data-in-themes/up.sql
new file mode 100644
index 00000000000..1004302ab7f
--- /dev/null
+++ b/migrations/2024-12-05-131123_add-email-theme-data-in-themes/up.sql
@@ -0,0 +1,6 @@
+-- Your SQL goes here
+ALTER TABLE themes ADD COLUMN IF NOT EXISTS email_primary_color VARCHAR(64) NOT NULL DEFAULT '#006DF9';
+ALTER TABLE themes ADD COLUMN IF NOT EXISTS email_foreground_color VARCHAR(64) NOT NULL DEFAULT '#000000';
+ALTER TABLE themes ADD COLUMN IF NOT EXISTS email_background_color VARCHAR(64) NOT NULL DEFAULT '#FFFFFF';
+ALTER TABLE themes ADD COLUMN IF NOT EXISTS email_entity_name VARCHAR(64) NOT NULL DEFAULT 'Hyperswitch';
+ALTER TABLE themes ADD COLUMN IF NOT EXISTS email_entity_logo_url TEXT NOT NULL DEFAULT 'https://app.hyperswitch.io/email-assets/HyperswitchLogo.png';

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

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

## Description
<!-- Describe your changes in detail -->
- This PR will introduce theme context in user APIs and email related APIs.
  - The User APIs which are affected are
    1. User Info
    2. Invite
    3. Resend Invite
    4. Reset Password
    5. Connect Account
    6. Signup with merchant id
  - Non User flows
    1. Request for Recon
    2. Recon merchant update
    3. API Key expiry reminder

### Additional Changes

- [x] This PR modifies the API contract
- [x] This PR modifies the database schema
- [x] 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`
4. `crates/router/src/configs`
5. `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).
-->
Closes #6780

## 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)?
-->
1. User Info API will send `theme_id` in the response if a theme exists in your lineage.
    ```
    curl --location 'http://localhost:8080/user' \
    --header 'Cookie: login_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMzAwMjAwN2UtZWE3NC00NzVlLWJmNWQtMzYxNzIwNTk4YzA3IiwibWVyY2hhbnRfaWQiOiJtZXJjaGFudF8xNzMzNDkyMzU3Iiwicm9sZV9pZCI6Im9yZ19hZG1pbiIsImV4cCI6MTczMzY2NTMzOSwib3JnX2lkIjoib3JnX3Z5VDVDb3BEdnpPT3g5V211TTdMIiwicHJvZmlsZV9pZCI6InByb180cHE4ZVVFREJDc1YyaDN5Mk9iSSIsInRlbmFudF9pZCI6InB1YmxpYyJ9.YsKkaY8aFDWz_1CLrZEZWNxmO2rSCk-yTe77OmHP_ZE'
    ```
    ```json
    {
        "merchant_id": "merchant_1733492357",
        "name": "name",
        "email": "email",
        "verification_days_left": null,
        "role_id": "org_admin",
        "org_id": "org_vyT5CopDvzOOx9WmuM7L",
        "is_two_factor_auth_setup": false,
        "recovery_codes_left": null,
        "profile_id": "pro_4pq8eUEDBCsV2h3y2ObI",
        "entity_type": "organization",
        "theme_id": "7792a4e5-bd7c-498d-8478-c4e0a9a0a984"
    }
    ```
2. All the other user specific APIs mentioned above will have `theme_id` in the return URL and also accept `theme_id` in the request.
3. Other flows will reflect the email themes once the email templates are changed to use the theme data.


## 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`
- [x] I reviewed the submitted code
- [ ] I added unit tests for my changes where possible
