Bug: [FEATURE] provision for secure payment links



### Feature Description

Provide open and secure payment links for different use cases
- Open links
   - Accessible by anyone
   - Can be opened in new tabs
   - Can be iframed by any host
   - SPMs are never listed
- Secure links
   - Can be opened only in iframes by allowed_domains (configurable)
   - SPMs can be listed (configurable)

### Possible Implementation

When payment links creation is requested, create two links
- Open
   - Always create this link
- Secure
   - Create only if allowed_domains are set in the profile
   - If created, store these domains in DB

When payment links render is requested
- Open - open as is (status vs initiate)
- Secure
   - Validate if the request is coming from an iframe
   - Validate Origin + Referer headers
   - Client side script to not load the SDK incase iframe is loaded in top


### Have you spent some time checking if this feature request has been raised before?

- [X] I checked and didn't find a similar issue

### Have you read the Contributing Guidelines?

- [X] I have read the [Contributing Guidelines](https://github.com/juspay/hyperswitch/blob/main/docs/CONTRIBUTING.md)

### Are you willing to submit a PR?

Yes, I am willing to submit a PR!
diff --git a/Cargo.lock b/Cargo.lock
index 66b38d09c1f..cf5864e96e7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2014,6 +2014,7 @@ dependencies = [
  "error-stack",
  "fake",
  "futures 0.3.30",
+ "globset",
  "hex",
  "http 0.2.12",
  "masking",
@@ -6063,7 +6064,6 @@ dependencies = [
  "events",
  "external_services",
  "futures 0.3.30",
- "globset",
  "hex",
  "http 0.2.12",
  "hyper 0.14.28",
diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs
index 4f1143efaa8..0ddca226f5b 100644
--- a/crates/api_models/src/admin.rs
+++ b/crates/api_models/src/admin.rs
@@ -1729,7 +1729,7 @@ impl BusinessGenericLinkConfig {
             .map(|host_domain| link_utils::validate_strict_domain(&host_domain))
             .unwrap_or(true);
         if !host_domain_valid {
-            return Err("Invalid host domain name received");
+            return Err("Invalid host domain name received in payout_link_config");
         }
 
         let are_allowed_domains_valid = self
@@ -1738,7 +1738,7 @@ impl BusinessGenericLinkConfig {
             .iter()
             .all(|allowed_domain| link_utils::validate_wildcard_domain(allowed_domain));
         if !are_allowed_domains_valid {
-            return Err("Invalid allowed domain names received");
+            return Err("Invalid allowed domain names received in payout_link_config");
         }
 
         Ok(())
@@ -1755,6 +1755,37 @@ pub struct BusinessPaymentLinkConfig {
     pub default_config: Option<PaymentLinkConfigRequest>,
     /// list of configs for multi theme setup
     pub business_specific_configs: Option<HashMap<String, PaymentLinkConfigRequest>>,
+    /// A list of allowed domains (glob patterns) where this link can be embedded / opened from
+    #[schema(value_type = Option<HashSet<String>>)]
+    pub allowed_domains: Option<HashSet<String>>,
+}
+
+impl BusinessPaymentLinkConfig {
+    pub fn validate(&self) -> Result<(), &str> {
+        let host_domain_valid = self
+            .domain_name
+            .clone()
+            .map(|host_domain| link_utils::validate_strict_domain(&host_domain))
+            .unwrap_or(true);
+        if !host_domain_valid {
+            return Err("Invalid host domain name received in payment_link_config");
+        }
+
+        let are_allowed_domains_valid = self
+            .allowed_domains
+            .clone()
+            .map(|allowed_domains| {
+                allowed_domains
+                    .iter()
+                    .all(|allowed_domain| link_utils::validate_wildcard_domain(allowed_domain))
+            })
+            .unwrap_or(true);
+        if !are_allowed_domains_valid {
+            return Err("Invalid allowed domain names received in payment_link_config");
+        }
+
+        Ok(())
+    }
 }
 
 #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, ToSchema)]
@@ -1793,6 +1824,8 @@ pub struct PaymentLinkConfig {
     pub display_sdk_only: bool,
     /// Enable saved payment method option for payment link
     pub enabled_saved_payment_method: bool,
+    /// A list of allowed domains (glob patterns) where this link can be embedded / opened from
+    pub allowed_domains: Option<HashSet<String>>,
 }
 
 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs
index a466288b416..cc29d5330ce 100644
--- a/crates/api_models/src/payments.rs
+++ b/crates/api_models/src/payments.rs
@@ -5300,7 +5300,11 @@ pub struct RetrievePaymentLinkRequest {
 
 #[derive(Clone, Debug, serde::Serialize, PartialEq, ToSchema)]
 pub struct PaymentLinkResponse {
+    /// URL for rendering the open payment link
     pub link: String,
+    /// URL for rendering the secure payment link
+    pub secure_link: Option<String>,
+    /// Identifier for the payment link
     pub payment_link_id: String,
 }
 
@@ -5311,7 +5315,7 @@ pub struct RetrievePaymentLinkResponse {
     /// Identifier for Merchant
     #[schema(value_type = String)]
     pub merchant_id: id_type::MerchantId,
-    /// Payment Link
+    /// Open payment link (without any security checks and listing SPMs)
     pub link_to_pay: String,
     /// The payment amount. Amount for the payment in the lowest denomination of the currency
     #[schema(value_type = i64, example = 6540)]
@@ -5328,6 +5332,8 @@ pub struct RetrievePaymentLinkResponse {
     pub status: PaymentLinkStatus,
     #[schema(value_type = Option<Currency>)]
     pub currency: Option<api_enums::Currency>,
+    /// Secure payment link (with security checks and listing saved payment methods)
+    pub secure_link: Option<String>,
 }
 
 #[derive(Clone, Debug, serde::Deserialize, ToSchema, serde::Serialize)]
@@ -5339,8 +5345,8 @@ pub struct PaymentLinkInitiateRequest {
 
 #[derive(Debug, serde::Serialize)]
 #[serde(untagged)]
-pub enum PaymentLinkData<'a> {
-    PaymentLinkDetails(&'a PaymentLinkDetails),
+pub enum PaymentLinkData {
+    PaymentLinkDetails(PaymentLinkDetails),
     PaymentLinkStatusDetails(PaymentLinkStatusDetails),
 }
 
@@ -5362,7 +5368,13 @@ pub struct PaymentLinkDetails {
     pub merchant_description: Option<String>,
     pub sdk_layout: String,
     pub display_sdk_only: bool,
+}
+
+#[derive(Debug, serde::Serialize, Clone)]
+pub struct SecurePaymentLinkDetails {
     pub enabled_saved_payment_method: bool,
+    #[serde(flatten)]
+    pub payment_link_details: PaymentLinkDetails,
 }
 
 #[derive(Debug, serde::Serialize)]
diff --git a/crates/common_utils/Cargo.toml b/crates/common_utils/Cargo.toml
index 4939c0f1609..40b9c105626 100644
--- a/crates/common_utils/Cargo.toml
+++ b/crates/common_utils/Cargo.toml
@@ -25,6 +25,7 @@ bytes = "1.6.0"
 diesel = "2.1.5"
 error-stack = "0.4.1"
 futures = { version = "0.3.30", optional = true }
+globset = "0.4.14"
 hex = "0.4.3"
 http = "0.2.12"
 md5 = "0.7.0"
diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs
index e52d24c9baa..75ba14586b1 100644
--- a/crates/common_utils/src/consts.rs
+++ b/crates/common_utils/src/consts.rs
@@ -1,5 +1,7 @@
 //! Commonly used constants
 
+use std::collections::HashSet;
+
 /// Number of characters in a generated ID
 pub const ID_LENGTH: usize = 20;
 
@@ -81,6 +83,9 @@ pub const DEFAULT_DISPLAY_SDK_ONLY: bool = false;
 /// Default bool to enable saved payment method
 pub const DEFAULT_ENABLE_SAVED_PAYMENT_METHOD: bool = false;
 
+/// Default allowed domains for payment links
+pub const DEFAULT_ALLOWED_DOMAINS: Option<HashSet<String>> = None;
+
 /// Default ttl for Extended card info  in redis (in seconds)
 pub const DEFAULT_TTL_FOR_EXTENDED_CARD_INFO: u16 = 15 * 60;
 
diff --git a/crates/common_utils/src/validation.rs b/crates/common_utils/src/validation.rs
index 1362f3b5f74..9403e6720a9 100644
--- a/crates/common_utils/src/validation.rs
+++ b/crates/common_utils/src/validation.rs
@@ -1,6 +1,9 @@
 //! Custom validations for some shared types.
 
+use std::collections::HashSet;
+
 use error_stack::report;
+use globset::Glob;
 use once_cell::sync::Lazy;
 use regex::Regex;
 #[cfg(feature = "logs")]
@@ -57,6 +60,27 @@ pub fn validate_email(email: &str) -> CustomResult<(), ValidationError> {
     Ok(())
 }
 
+/// Checks whether a given domain matches against a list of valid domain glob patterns
+pub fn validate_domain_against_allowed_domains(
+    domain: &str,
+    allowed_domains: HashSet<String>,
+) -> bool {
+    allowed_domains.iter().any(|allowed_domain| {
+        Glob::new(allowed_domain)
+            .map(|glob| glob.compile_matcher().is_match(domain))
+            .map_err(|err| {
+                let err_msg = format!(
+                    "Invalid glob pattern for configured allowed_domain [{:?}]! - {:?}",
+                    allowed_domain, err
+                );
+                #[cfg(feature = "logs")]
+                logger::error!(err_msg);
+                err_msg
+            })
+            .unwrap_or(false)
+    })
+}
+
 #[cfg(test)]
 mod tests {
     use fake::{faker::internet::en::SafeEmail, Fake};
diff --git a/crates/diesel_models/src/payment_link.rs b/crates/diesel_models/src/payment_link.rs
index d5887143019..347a730eb68 100644
--- a/crates/diesel_models/src/payment_link.rs
+++ b/crates/diesel_models/src/payment_link.rs
@@ -24,6 +24,7 @@ pub struct PaymentLink {
     pub payment_link_config: Option<serde_json::Value>,
     pub description: Option<String>,
     pub profile_id: Option<String>,
+    pub secure_link: Option<String>,
 }
 
 #[derive(
@@ -54,4 +55,5 @@ pub struct PaymentLinkNew {
     pub payment_link_config: Option<serde_json::Value>,
     pub description: Option<String>,
     pub profile_id: Option<String>,
+    pub secure_link: Option<String>,
 }
diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs
index 544a99bccb5..526a19ed5bf 100644
--- a/crates/diesel_models/src/schema.rs
+++ b/crates/diesel_models/src/schema.rs
@@ -922,6 +922,8 @@ diesel::table! {
         description -> Nullable<Varchar>,
         #[max_length = 64]
         profile_id -> Nullable<Varchar>,
+        #[max_length = 255]
+        secure_link -> Nullable<Varchar>,
     }
 }
 
diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs
index e60e2da0a47..bac1ca82262 100644
--- a/crates/diesel_models/src/schema_v2.rs
+++ b/crates/diesel_models/src/schema_v2.rs
@@ -928,6 +928,8 @@ diesel::table! {
         description -> Nullable<Varchar>,
         #[max_length = 64]
         profile_id -> Nullable<Varchar>,
+        #[max_length = 255]
+        secure_link -> Nullable<Varchar>,
     }
 }
 
diff --git a/crates/hyperswitch_domain_models/src/api.rs b/crates/hyperswitch_domain_models/src/api.rs
index 39f3f04c548..d62516234ea 100644
--- a/crates/hyperswitch_domain_models/src/api.rs
+++ b/crates/hyperswitch_domain_models/src/api.rs
@@ -71,6 +71,7 @@ pub enum GenericLinksData {
     PayoutLink(GenericLinkFormData),
     PayoutLinkStatus(GenericLinkStatusData),
     PaymentMethodCollectStatus(GenericLinkStatusData),
+    SecurePaymentLink(PaymentLinkFormData),
 }
 
 impl Display for GenericLinksData {
@@ -84,6 +85,7 @@ impl Display for GenericLinksData {
                 Self::PayoutLink(_) => "PayoutLink",
                 Self::PayoutLinkStatus(_) => "PayoutLinkStatus",
                 Self::PaymentMethodCollectStatus(_) => "PaymentMethodCollectStatus",
+                Self::SecurePaymentLink(_) => "SecurePaymentLink",
             }
         )
     }
diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml
index e7a0db07e8e..30cc7287b5a 100644
--- a/crates/router/Cargo.toml
+++ b/crates/router/Cargo.toml
@@ -61,7 +61,6 @@ dyn-clone = "1.0.17"
 encoding_rs = "0.8.33"
 error-stack = "0.4.1"
 futures = "0.3.30"
-globset = "0.4.14"
 hex = "0.4.3"
 http = "0.2.12"
 hyper = "0.14.28"
diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs
index 51fffea3b53..fc34ec2f7db 100644
--- a/crates/router/src/core/admin.rs
+++ b/crates/router/src/core/admin.rs
@@ -3139,12 +3139,15 @@ pub async fn update_business_profile(
     let payment_link_config = request
         .payment_link_config
         .as_ref()
-        .map(|pl_metadata| {
-            pl_metadata.encode_to_value().change_context(
+        .map(|payment_link_conf| match payment_link_conf.validate() {
+            Ok(_) => payment_link_conf.encode_to_value().change_context(
                 errors::ApiErrorResponse::InvalidDataValue {
                     field_name: "payment_link_config",
                 },
-            )
+            ),
+            Err(e) => Err(report!(errors::ApiErrorResponse::InvalidRequestData {
+                message: e.to_string()
+            })),
         })
         .transpose()?;
 
diff --git a/crates/router/src/core/payment_link.rs b/crates/router/src/core/payment_link.rs
index a6d50fe6156..ef39a34a11e 100644
--- a/crates/router/src/core/payment_link.rs
+++ b/crates/router/src/core/payment_link.rs
@@ -1,14 +1,21 @@
-use api_models::{admin as admin_types, payments::PaymentLinkStatusWrap};
+pub mod validator;
+use actix_web::http::header;
+use api_models::{
+    admin::PaymentLinkConfig,
+    payments::{PaymentLinkData, PaymentLinkStatusWrap},
+};
 use common_utils::{
     consts::{
-        DEFAULT_BACKGROUND_COLOR, DEFAULT_DISPLAY_SDK_ONLY, DEFAULT_ENABLE_SAVED_PAYMENT_METHOD,
-        DEFAULT_MERCHANT_LOGO, DEFAULT_PRODUCT_IMG, DEFAULT_SDK_LAYOUT, DEFAULT_SESSION_EXPIRY,
+        DEFAULT_ALLOWED_DOMAINS, DEFAULT_BACKGROUND_COLOR, DEFAULT_DISPLAY_SDK_ONLY,
+        DEFAULT_ENABLE_SAVED_PAYMENT_METHOD, DEFAULT_MERCHANT_LOGO, DEFAULT_PRODUCT_IMG,
+        DEFAULT_SDK_LAYOUT, DEFAULT_SESSION_EXPIRY,
     },
     ext_traits::{OptionExt, ValueExt},
     types::{AmountConvertor, MinorUnit, StringMajorUnitForCore},
 };
-use error_stack::ResultExt;
+use error_stack::{report, ResultExt};
 use futures::future;
+use hyperswitch_domain_models::api::{GenericLinks, GenericLinksData};
 use masking::{PeekInterface, Secret};
 use router_env::logger;
 use time::PrimitiveDateTime;
@@ -20,7 +27,9 @@ use crate::{
     routes::SessionState,
     services,
     types::{
-        api::payment_link::PaymentLinkResponseExt, domain, storage::enums as storage_enums,
+        api::payment_link::PaymentLinkResponseExt,
+        domain,
+        storage::{enums as storage_enums, payment_link::PaymentLink},
         transformers::ForeignFrom,
     },
 };
@@ -49,17 +58,17 @@ pub async fn retrieve_payment_link(
     Ok(services::ApplicationResponse::Json(response))
 }
 
-pub async fn initiate_payment_link_flow(
-    state: SessionState,
+pub async fn form_payment_link_data(
+    state: &SessionState,
     merchant_account: domain::MerchantAccount,
     key_store: domain::MerchantKeyStore,
     merchant_id: common_utils::id_type::MerchantId,
     payment_id: String,
-) -> RouterResponse<services::PaymentLinkFormData> {
+) -> RouterResult<(PaymentLink, PaymentLinkData, PaymentLinkConfig)> {
     let db = &*state.store;
     let payment_intent = db
         .find_payment_intent_by_payment_id_merchant_id(
-            &(&state).into(),
+            &(state).into(),
             &payment_id,
             &merchant_id,
             &key_store,
@@ -84,21 +93,24 @@ pub async fn initiate_payment_link_flow(
         .await
         .to_not_found_response(errors::ApiErrorResponse::PaymentLinkNotFound)?;
 
-    let payment_link_config = if let Some(pl_config_value) = payment_link.payment_link_config {
-        extract_payment_link_config(pl_config_value)?
-    } else {
-        admin_types::PaymentLinkConfig {
-            theme: DEFAULT_BACKGROUND_COLOR.to_string(),
-            logo: DEFAULT_MERCHANT_LOGO.to_string(),
-            seller_name: merchant_name_from_merchant_account,
-            sdk_layout: DEFAULT_SDK_LAYOUT.to_owned(),
-            display_sdk_only: DEFAULT_DISPLAY_SDK_ONLY,
-            enabled_saved_payment_method: DEFAULT_ENABLE_SAVED_PAYMENT_METHOD,
-        }
-    };
+    let payment_link_config =
+        if let Some(pl_config_value) = payment_link.payment_link_config.clone() {
+            extract_payment_link_config(pl_config_value)?
+        } else {
+            PaymentLinkConfig {
+                theme: DEFAULT_BACKGROUND_COLOR.to_string(),
+                logo: DEFAULT_MERCHANT_LOGO.to_string(),
+                seller_name: merchant_name_from_merchant_account,
+                sdk_layout: DEFAULT_SDK_LAYOUT.to_owned(),
+                display_sdk_only: DEFAULT_DISPLAY_SDK_ONLY,
+                enabled_saved_payment_method: DEFAULT_ENABLE_SAVED_PAYMENT_METHOD,
+                allowed_domains: DEFAULT_ALLOWED_DOMAINS,
+            }
+        };
 
     let profile_id = payment_link
         .profile_id
+        .clone()
         .or(payment_intent.profile_id)
         .ok_or(errors::ApiErrorResponse::InternalServerError)
         .attach_printable("Profile id missing in payment link and payment intent")?;
@@ -143,7 +155,6 @@ pub async fn initiate_payment_link_flow(
 
     // converting first letter of merchant name to upperCase
     let merchant_name = capitalize_first_char(&payment_link_config.seller_name);
-    let css_script = get_color_scheme_css(payment_link_config.clone());
     let payment_link_status = check_payment_link_status(session_expiry);
 
     let is_terminal_state = check_payment_link_invalid_conditions(
@@ -205,78 +216,185 @@ pub async fn initiate_payment_link_flow(
             return_url: return_url.clone(),
         };
 
-        logger::info!(
-            "payment link data, for building payment link status page {:?}",
-            payment_details
-        );
-        let js_script = get_js_script(
-            &api_models::payments::PaymentLinkData::PaymentLinkStatusDetails(payment_details),
-        )?;
-        let payment_link_error_data = services::PaymentLinkStatusData {
-            js_script,
-            css_script,
-        };
-        return Ok(services::ApplicationResponse::PaymentLinkForm(Box::new(
-            services::api::PaymentLinkAction::PaymentLinkStatus(payment_link_error_data),
-        )));
+        return Ok((
+            payment_link,
+            PaymentLinkData::PaymentLinkStatusDetails(payment_details),
+            payment_link_config,
+        ));
     };
 
-    let payment_details = api_models::payments::PaymentLinkDetails {
-        amount,
-        currency,
-        payment_id: payment_intent.payment_id,
-        merchant_name,
-        order_details,
-        return_url,
-        session_expiry,
-        pub_key: merchant_account.publishable_key,
-        client_secret,
-        merchant_logo: payment_link_config.logo.clone(),
-        max_items_visible_after_collapse: 3,
-        theme: payment_link_config.theme.clone(),
-        merchant_description: payment_intent.description,
-        sdk_layout: payment_link_config.sdk_layout.clone(),
-        display_sdk_only: payment_link_config.display_sdk_only,
-        enabled_saved_payment_method: payment_link_config.enabled_saved_payment_method,
-    };
+    let payment_link_details =
+        PaymentLinkData::PaymentLinkDetails(api_models::payments::PaymentLinkDetails {
+            amount,
+            currency,
+            payment_id: payment_intent.payment_id,
+            merchant_name,
+            order_details,
+            return_url,
+            session_expiry,
+            pub_key: merchant_account.publishable_key,
+            client_secret,
+            merchant_logo: payment_link_config.logo.clone(),
+            max_items_visible_after_collapse: 3,
+            theme: payment_link_config.theme.clone(),
+            merchant_description: payment_intent.description,
+            sdk_layout: payment_link_config.sdk_layout.clone(),
+            display_sdk_only: payment_link_config.display_sdk_only,
+        });
 
-    let js_script = get_js_script(&api_models::payments::PaymentLinkData::PaymentLinkDetails(
-        &payment_details,
-    ))?;
+    Ok((payment_link, payment_link_details, payment_link_config))
+}
+
+pub async fn initiate_secure_payment_link_flow(
+    state: SessionState,
+    merchant_account: domain::MerchantAccount,
+    key_store: domain::MerchantKeyStore,
+    merchant_id: common_utils::id_type::MerchantId,
+    payment_id: String,
+    request_headers: &header::HeaderMap,
+) -> RouterResponse<services::PaymentLinkFormData> {
+    let (payment_link, payment_link_details, payment_link_config) =
+        form_payment_link_data(&state, merchant_account, key_store, merchant_id, payment_id)
+            .await?;
+
+    validator::validate_secure_payment_link_render_request(
+        request_headers,
+        &payment_link,
+        &payment_link_config,
+    )?;
 
-    let html_meta_tags = get_meta_tags_html(payment_details);
+    let css_script = get_color_scheme_css(&payment_link_config);
+
+    match payment_link_details {
+        PaymentLinkData::PaymentLinkStatusDetails(ref status_details) => {
+            let js_script = get_js_script(&payment_link_details)?;
+            let payment_link_error_data = services::PaymentLinkStatusData {
+                js_script,
+                css_script,
+            };
+            logger::info!(
+                "payment link data, for building payment link status page {:?}",
+                status_details
+            );
+            Ok(services::ApplicationResponse::PaymentLinkForm(Box::new(
+                services::api::PaymentLinkAction::PaymentLinkStatus(payment_link_error_data),
+            )))
+        }
+        PaymentLinkData::PaymentLinkDetails(link_details) => {
+            let secure_payment_link_details = api_models::payments::SecurePaymentLinkDetails {
+                enabled_saved_payment_method: payment_link_config.enabled_saved_payment_method,
+                payment_link_details: link_details.to_owned(),
+            };
+            let js_script = format!(
+                "window.__PAYMENT_DETAILS = {}",
+                serde_json::to_string(&secure_payment_link_details)
+                    .change_context(errors::ApiErrorResponse::InternalServerError)
+                    .attach_printable("Failed to serialize PaymentLinkData")?
+            );
+            let html_meta_tags = get_meta_tags_html(&link_details);
+            let payment_link_data = services::PaymentLinkFormData {
+                js_script,
+                sdk_url: state.conf.payment_link.sdk_url.clone(),
+                css_script,
+                html_meta_tags,
+            };
+            let allowed_domains = payment_link_config
+                .allowed_domains
+                .clone()
+                .ok_or(report!(errors::ApiErrorResponse::InternalServerError))
+                .attach_printable_lazy(|| {
+                    format!(
+                        "Invalid list of allowed_domains found - {:?}",
+                        payment_link_config.allowed_domains.clone()
+                    )
+                })?;
+
+            if allowed_domains.is_empty() {
+                return Err(report!(errors::ApiErrorResponse::InternalServerError))
+                    .attach_printable_lazy(|| {
+                        format!(
+                            "Invalid list of allowed_domains found - {:?}",
+                            payment_link_config.allowed_domains.clone()
+                        )
+                    });
+            }
 
-    let payment_link_data = services::PaymentLinkFormData {
-        js_script,
-        sdk_url: state.conf.payment_link.sdk_url.clone(),
-        css_script,
-        html_meta_tags,
-    };
+            let link_data = GenericLinks {
+                allowed_domains,
+                data: GenericLinksData::SecurePaymentLink(payment_link_data),
+            };
+            logger::info!(
+                "payment link data, for building secure payment link {:?}",
+                link_data
+            );
+
+            Ok(services::ApplicationResponse::GenericLinkForm(Box::new(
+                link_data,
+            )))
+        }
+    }
+}
 
-    logger::info!(
-        "payment link data, for building payment link {:?}",
-        payment_link_data
-    );
-    Ok(services::ApplicationResponse::PaymentLinkForm(Box::new(
-        services::api::PaymentLinkAction::PaymentLinkFormData(payment_link_data),
-    )))
+pub async fn initiate_payment_link_flow(
+    state: SessionState,
+    merchant_account: domain::MerchantAccount,
+    key_store: domain::MerchantKeyStore,
+    merchant_id: common_utils::id_type::MerchantId,
+    payment_id: String,
+) -> RouterResponse<services::PaymentLinkFormData> {
+    let (_, payment_details, payment_link_config) =
+        form_payment_link_data(&state, merchant_account, key_store, merchant_id, payment_id)
+            .await?;
+
+    let css_script = get_color_scheme_css(&payment_link_config);
+    let js_script = get_js_script(&payment_details)?;
+
+    match payment_details {
+        PaymentLinkData::PaymentLinkStatusDetails(status_details) => {
+            let payment_link_error_data = services::PaymentLinkStatusData {
+                js_script,
+                css_script,
+            };
+            logger::info!(
+                "payment link data, for building payment link status page {:?}",
+                status_details
+            );
+            Ok(services::ApplicationResponse::PaymentLinkForm(Box::new(
+                services::api::PaymentLinkAction::PaymentLinkStatus(payment_link_error_data),
+            )))
+        }
+        PaymentLinkData::PaymentLinkDetails(payment_details) => {
+            let html_meta_tags = get_meta_tags_html(&payment_details);
+            let payment_link_data = services::PaymentLinkFormData {
+                js_script,
+                sdk_url: state.conf.payment_link.sdk_url.clone(),
+                css_script,
+                html_meta_tags,
+            };
+            logger::info!(
+                "payment link data, for building open payment link {:?}",
+                payment_link_data
+            );
+            Ok(services::ApplicationResponse::PaymentLinkForm(Box::new(
+                services::api::PaymentLinkAction::PaymentLinkFormData(payment_link_data),
+            )))
+        }
+    }
 }
 
 /*
 The get_js_script function is used to inject dynamic value to payment_link sdk, which is unique to every payment.
 */
 
-fn get_js_script(
-    payment_details: &api_models::payments::PaymentLinkData<'_>,
-) -> RouterResult<String> {
+fn get_js_script(payment_details: &PaymentLinkData) -> RouterResult<String> {
     let payment_details_str = serde_json::to_string(payment_details)
         .change_context(errors::ApiErrorResponse::InternalServerError)
         .attach_printable("Failed to serialize PaymentLinkData")?;
     Ok(format!("window.__PAYMENT_DETAILS = {payment_details_str};"))
 }
 
-fn get_color_scheme_css(payment_link_config: api_models::admin::PaymentLinkConfig) -> String {
-    let background_primary_color = payment_link_config.theme;
+fn get_color_scheme_css(payment_link_config: &PaymentLinkConfig) -> String {
+    let background_primary_color = payment_link_config.theme.clone();
     format!(
         ":root {{
       --primary-color: {background_primary_color};
@@ -284,12 +402,15 @@ fn get_color_scheme_css(payment_link_config: api_models::admin::PaymentLinkConfi
     )
 }
 
-fn get_meta_tags_html(payment_details: api_models::payments::PaymentLinkDetails) -> String {
+fn get_meta_tags_html(payment_details: &api_models::payments::PaymentLinkDetails) -> String {
     format!(
         r#"<meta property="og:title" content="Payment request from {0}"/>
         <meta property="og:description" content="{1}"/>"#,
-        payment_details.merchant_name,
-        payment_details.merchant_description.unwrap_or_default()
+        payment_details.merchant_name.clone(),
+        payment_details
+            .merchant_description
+            .clone()
+            .unwrap_or_default()
     )
 }
 
@@ -395,11 +516,12 @@ fn validate_order_details(
 
 pub fn extract_payment_link_config(
     pl_config: serde_json::Value,
-) -> Result<api_models::admin::PaymentLinkConfig, error_stack::Report<errors::ApiErrorResponse>> {
-    serde_json::from_value::<api_models::admin::PaymentLinkConfig>(pl_config.clone())
-        .change_context(errors::ApiErrorResponse::InvalidDataValue {
+) -> Result<PaymentLinkConfig, error_stack::Report<errors::ApiErrorResponse>> {
+    serde_json::from_value::<PaymentLinkConfig>(pl_config).change_context(
+        errors::ApiErrorResponse::InvalidDataValue {
             field_name: "payment_link_config",
-        })
+        },
+    )
 }
 
 pub fn get_payment_link_config_based_on_priority(
@@ -408,39 +530,39 @@ pub fn get_payment_link_config_based_on_priority(
     merchant_name: String,
     default_domain_name: String,
     payment_link_config_id: Option<String>,
-) -> Result<(admin_types::PaymentLinkConfig, String), error_stack::Report<errors::ApiErrorResponse>>
-{
-    let (domain_name, business_theme_configs) = if let Some(business_config) = business_link_config
-    {
-        let extracted_value: api_models::admin::BusinessPaymentLinkConfig = business_config
-            .parse_value("BusinessPaymentLinkConfig")
-            .change_context(errors::ApiErrorResponse::InvalidDataValue {
-                field_name: "payment_link_config",
-            })
-            .attach_printable("Invalid payment_link_config given in business config")?;
-        logger::info!(
-            "domain name set to custom domain https://{:?}",
-            extracted_value.domain_name
-        );
-
-        (
-            extracted_value
-                .domain_name
-                .clone()
-                .map(|d_name| format!("https://{}", d_name))
-                .unwrap_or_else(|| default_domain_name.clone()),
-            payment_link_config_id
-                .and_then(|id| {
-                    extracted_value
-                        .business_specific_configs
-                        .as_ref()
-                        .and_then(|specific_configs| specific_configs.get(&id).cloned())
+) -> Result<(PaymentLinkConfig, String), error_stack::Report<errors::ApiErrorResponse>> {
+    let (domain_name, business_theme_configs, allowed_domains) =
+        if let Some(business_config) = business_link_config {
+            let extracted_value: api_models::admin::BusinessPaymentLinkConfig = business_config
+                .parse_value("BusinessPaymentLinkConfig")
+                .change_context(errors::ApiErrorResponse::InvalidDataValue {
+                    field_name: "payment_link_config",
                 })
-                .or(extracted_value.default_config),
-        )
-    } else {
-        (default_domain_name, None)
-    };
+                .attach_printable("Invalid payment_link_config given in business config")?;
+            logger::info!(
+                "domain name set to custom domain https://{:?}",
+                extracted_value.domain_name
+            );
+
+            (
+                extracted_value
+                    .domain_name
+                    .clone()
+                    .map(|d_name| format!("https://{}", d_name))
+                    .unwrap_or_else(|| default_domain_name.clone()),
+                payment_link_config_id
+                    .and_then(|id| {
+                        extracted_value
+                            .business_specific_configs
+                            .as_ref()
+                            .and_then(|specific_configs| specific_configs.get(&id).cloned())
+                    })
+                    .or(extracted_value.default_config),
+                extracted_value.allowed_domains,
+            )
+        } else {
+            (default_domain_name, None, None)
+        };
 
     let (theme, logo, seller_name, sdk_layout, display_sdk_only, enabled_saved_payment_method) = get_payment_link_config_value!(
         payment_create_link_config,
@@ -456,13 +578,14 @@ pub fn get_payment_link_config_based_on_priority(
         )
     );
 
-    let payment_link_config = admin_types::PaymentLinkConfig {
+    let payment_link_config = PaymentLinkConfig {
         theme,
         logo,
         seller_name,
         sdk_layout,
         display_sdk_only,
         enabled_saved_payment_method,
+        allowed_domains,
     };
 
     Ok((payment_link_config, domain_name))
@@ -537,13 +660,14 @@ pub async fn get_payment_link_status(
     let payment_link_config = if let Some(pl_config_value) = payment_link.payment_link_config {
         extract_payment_link_config(pl_config_value)?
     } else {
-        admin_types::PaymentLinkConfig {
+        PaymentLinkConfig {
             theme: DEFAULT_BACKGROUND_COLOR.to_string(),
             logo: DEFAULT_MERCHANT_LOGO.to_string(),
             seller_name: merchant_name_from_merchant_account,
             sdk_layout: DEFAULT_SDK_LAYOUT.to_owned(),
             display_sdk_only: DEFAULT_DISPLAY_SDK_ONLY,
             enabled_saved_payment_method: DEFAULT_ENABLE_SAVED_PAYMENT_METHOD,
+            allowed_domains: DEFAULT_ALLOWED_DOMAINS,
         }
     };
 
@@ -564,7 +688,7 @@ pub async fn get_payment_link_status(
 
     // converting first letter of merchant name to upperCase
     let merchant_name = capitalize_first_char(&payment_link_config.seller_name);
-    let css_script = get_color_scheme_css(payment_link_config.clone());
+    let css_script = get_color_scheme_css(&payment_link_config);
 
     let profile_id = payment_link
         .profile_id
@@ -603,9 +727,7 @@ pub async fn get_payment_link_status(
         theme: payment_link_config.theme.clone(),
         return_url,
     };
-    let js_script = get_js_script(
-        &api_models::payments::PaymentLinkData::PaymentLinkStatusDetails(payment_details),
-    )?;
+    let js_script = get_js_script(&PaymentLinkData::PaymentLinkStatusDetails(payment_details))?;
     let payment_link_status_data = services::PaymentLinkStatusData {
         js_script,
         css_script,
diff --git a/crates/router/src/core/payment_link/payment_link_initiate/payment_link.html b/crates/router/src/core/payment_link/payment_link_initiate/payment_link.html
index 1e6723201c9..5e165ac171a 100644
--- a/crates/router/src/core/payment_link/payment_link_initiate/payment_link.html
+++ b/crates/router/src/core/payment_link/payment_link_initiate/payment_link.html
@@ -93,7 +93,7 @@
       }
     </style>
   </head>
-  <body class="hide-scrollbar">
+  <body id="payment-link" class="hide-scrollbar">
     <div id="payment-details-shimmer">
       <div class = "wrap">
         <box class="shine"></box>
@@ -324,6 +324,7 @@
     </div>
     <script>
       {{rendered_js}}
+      {{payment_link_initiator}}
       {{logging_template}}
     </script>
     {{ hyperloader_sdk_link }}
diff --git a/crates/router/src/core/payment_link/payment_link_initiate/payment_link.js b/crates/router/src/core/payment_link/payment_link_initiate/payment_link.js
index 1a28b6ecf82..48b3de97a81 100644
--- a/crates/router/src/core/payment_link/payment_link_initiate/payment_link.js
+++ b/crates/router/src/core/payment_link/payment_link_initiate/payment_link.js
@@ -379,75 +379,6 @@ function showSDK(display_sdk_only) {
   hide("#sdk-spinner");
 }
 
-/**
- * Trigger - post downloading SDK
- * Uses
- *  - Instantiate SDK
- *  - Create a payment widget
- *  - Decide whether or not to show SDK (based on status)
- **/
-function initializeSDK() {
-  // @ts-ignore
-  var paymentDetails = window.__PAYMENT_DETAILS;
-  var client_secret = paymentDetails.client_secret;
-  var appearance = {
-    variables: {
-      colorPrimary: paymentDetails.theme || "rgb(0, 109, 249)",
-      fontFamily: "Work Sans, sans-serif",
-      fontSizeBase: "16px",
-      colorText: "rgb(51, 65, 85)",
-      colorTextSecondary: "#334155B3",
-      colorPrimaryText: "rgb(51, 65, 85)",
-      colorTextPlaceholder: "#33415550",
-      borderColor: "#33415550",
-      colorBackground: "rgb(255, 255, 255)",
-    },
-  };
-  // @ts-ignore
-  hyper = window.Hyper(pub_key, {
-    isPreloadEnabled: false,
-  });
-  widgets = hyper.widgets({
-    appearance: appearance,
-    clientSecret: client_secret,
-  });
-  var type =
-    paymentDetails.sdk_layout === "spaced_accordion" ||
-    paymentDetails.sdk_layout === "accordion"
-      ? "accordion"
-      : paymentDetails.sdk_layout;
-
-  var enableSavedPaymentMethod = paymentDetails.enabled_saved_payment_method;
-  var unifiedCheckoutOptions = {
-    displaySavedPaymentMethodsCheckbox: enableSavedPaymentMethod,
-    displaySavedPaymentMethods: enableSavedPaymentMethod,
-    layout: {
-      type: type, //accordion , tabs, spaced accordion
-      spacedAccordionItems: paymentDetails.sdk_layout === "spaced_accordion",
-    },
-    branding: "never",
-    wallets: {
-      walletReturnUrl: paymentDetails.return_url,
-      style: {
-        theme: "dark",
-        type: "default",
-        height: 55,
-      },
-    },
-  };
-  unifiedCheckout = widgets.create("payment", unifiedCheckoutOptions);
-  mountUnifiedCheckout("#unified-checkout");
-  showSDK(paymentDetails.display_sdk_only);
-
-  let shimmer = document.getElementById("payment-details-shimmer");
-  shimmer.classList.add("reduce-opacity")
-
-  setTimeout(() => {
-    document.body.removeChild(shimmer);
-  }, 500)
-}
-
-
 /**
  * Use - mount payment widget on the passed element
  * @param {String} id
@@ -525,17 +456,6 @@ function showMessage(msg) {
   addText("#payment-message", msg);
 }
 
-/**
- * Use - redirect to /payment_link/status
- */
-function redirectToStatus() {
-  var arr = window.location.pathname.split("/");
-  arr.splice(0, 2);
-  arr.unshift("status");
-  arr.unshift("payment_link");
-  window.location.href = window.location.origin + "/" + arr.join("/");
-}
-
 function addText(id, msg) {
   var element = document.querySelector(id);
   element.innerText = msg;
diff --git a/crates/router/src/core/payment_link/payment_link_initiate/payment_link_initiator.js b/crates/router/src/core/payment_link/payment_link_initiate/payment_link_initiator.js
new file mode 100644
index 00000000000..4d14af1577c
--- /dev/null
+++ b/crates/router/src/core/payment_link/payment_link_initiate/payment_link_initiator.js
@@ -0,0 +1,82 @@
+// @ts-check
+
+/**
+ * Trigger - post downloading SDK
+ * Uses
+ *  - Instantiate SDK
+ *  - Create a payment widget
+ *  - Decide whether or not to show SDK (based on status)
+ **/
+function initializeSDK() {
+  // @ts-ignore
+  var paymentDetails = window.__PAYMENT_DETAILS;
+  var client_secret = paymentDetails.client_secret;
+  var appearance = {
+    variables: {
+      colorPrimary: paymentDetails.theme || "rgb(0, 109, 249)",
+      fontFamily: "Work Sans, sans-serif",
+      fontSizeBase: "16px",
+      colorText: "rgb(51, 65, 85)",
+      colorTextSecondary: "#334155B3",
+      colorPrimaryText: "rgb(51, 65, 85)",
+      colorTextPlaceholder: "#33415550",
+      borderColor: "#33415550",
+      colorBackground: "rgb(255, 255, 255)",
+    },
+  };
+  // @ts-ignore
+  hyper = window.Hyper(pub_key, {
+    isPreloadEnabled: false,
+  });
+  // @ts-ignore
+  widgets = hyper.widgets({
+    appearance: appearance,
+    clientSecret: client_secret,
+  });
+  var type =
+    paymentDetails.sdk_layout === "spaced_accordion" ||
+    paymentDetails.sdk_layout === "accordion"
+      ? "accordion"
+      : paymentDetails.sdk_layout;
+
+  var unifiedCheckoutOptions = {
+    displaySavedPaymentMethodsCheckbox: false,
+    layout: {
+      type: type, //accordion , tabs, spaced accordion
+      spacedAccordionItems: paymentDetails.sdk_layout === "spaced_accordion",
+    },
+    branding: "never",
+    wallets: {
+      walletReturnUrl: paymentDetails.return_url,
+      style: {
+        theme: "dark",
+        type: "default",
+        height: 55,
+      },
+    },
+  };
+  // @ts-ignore
+  unifiedCheckout = widgets.create("payment", unifiedCheckoutOptions);
+  // @ts-ignore
+  mountUnifiedCheckout("#unified-checkout");
+  // @ts-ignore
+  showSDK(paymentDetails.display_sdk_only);
+
+  let shimmer = document.getElementById("payment-details-shimmer");
+  shimmer.classList.add("reduce-opacity");
+
+  setTimeout(() => {
+    document.body.removeChild(shimmer);
+  }, 500);
+
+  /**
+   * Use - redirect to /payment_link/status
+   */
+  function redirectToStatus() {
+    var arr = window.location.pathname.split("/");
+    arr.splice(0, 2);
+    arr.unshift("status");
+    arr.unshift("payment_link");
+    window.location.href = window.location.origin + "/" + arr.join("/");
+  }
+}
diff --git a/crates/router/src/core/payment_link/payment_link_initiate/secure_payment_link_initiator.js b/crates/router/src/core/payment_link/payment_link_initiate/secure_payment_link_initiator.js
new file mode 100644
index 00000000000..7a75afc1d8e
--- /dev/null
+++ b/crates/router/src/core/payment_link/payment_link_initiate/secure_payment_link_initiator.js
@@ -0,0 +1,107 @@
+// @ts-check
+
+// Top level checks
+var isFramed = false;
+try {
+  isFramed = window.parent.location !== window.location;
+
+  // If parent's window object is restricted, DOMException is
+  // thrown which concludes that the webpage is iframed
+} catch (err) {
+  isFramed = true;
+}
+
+if (!isFramed) {
+  function initializeSDK() {
+    var errMsg = "You are not allowed to view this content.";
+    var contentElement = document.getElementById("payment-link");
+    if (contentElement instanceof HTMLDivElement) {
+      contentElement.innerHTML = errMsg;
+    } else {
+      document.body.innerHTML = errMsg;
+    }
+  }
+} else {
+  /**
+   * Trigger - post downloading SDK
+   * Uses
+   *  - Instantiate SDK
+   *  - Create a payment widget
+   *  - Decide whether or not to show SDK (based on status)
+   **/
+  function initializeSDK() {
+    // @ts-ignore
+    var paymentDetails = window.__PAYMENT_DETAILS;
+    var client_secret = paymentDetails.client_secret;
+    var appearance = {
+      variables: {
+        colorPrimary: paymentDetails.theme || "rgb(0, 109, 249)",
+        fontFamily: "Work Sans, sans-serif",
+        fontSizeBase: "16px",
+        colorText: "rgb(51, 65, 85)",
+        colorTextSecondary: "#334155B3",
+        colorPrimaryText: "rgb(51, 65, 85)",
+        colorTextPlaceholder: "#33415550",
+        borderColor: "#33415550",
+        colorBackground: "rgb(255, 255, 255)",
+      },
+    };
+    // @ts-ignore
+    hyper = window.Hyper(pub_key, {
+      isPreloadEnabled: false,
+    });
+    // @ts-ignore
+    widgets = hyper.widgets({
+      appearance: appearance,
+      clientSecret: client_secret,
+    });
+    var type =
+      paymentDetails.sdk_layout === "spaced_accordion" ||
+      paymentDetails.sdk_layout === "accordion"
+        ? "accordion"
+        : paymentDetails.sdk_layout;
+
+    var enableSavedPaymentMethod = paymentDetails.enabled_saved_payment_method;
+    var unifiedCheckoutOptions = {
+      displaySavedPaymentMethodsCheckbox: enableSavedPaymentMethod,
+      displaySavedPaymentMethods: enableSavedPaymentMethod,
+      layout: {
+        type: type, //accordion , tabs, spaced accordion
+        spacedAccordionItems: paymentDetails.sdk_layout === "spaced_accordion",
+      },
+      branding: "never",
+      wallets: {
+        walletReturnUrl: paymentDetails.return_url,
+        style: {
+          theme: "dark",
+          type: "default",
+          height: 55,
+        },
+      },
+    };
+    // @ts-ignore
+    unifiedCheckout = widgets.create("payment", unifiedCheckoutOptions);
+    // @ts-ignore
+    mountUnifiedCheckout("#unified-checkout");
+    // @ts-ignore
+    showSDK(paymentDetails.display_sdk_only);
+
+    let shimmer = document.getElementById("payment-details-shimmer");
+    shimmer.classList.add("reduce-opacity");
+
+    setTimeout(() => {
+      document.body.removeChild(shimmer);
+    }, 500);
+  }
+
+  /**
+   * Use - redirect to /payment_link/status
+   */
+  function redirectToStatus() {
+    var arr = window.location.pathname.split("/");
+    arr.splice(0, 3);
+    arr.unshift("status");
+    arr.unshift("payment_link");
+    window.location.href = window.location.origin + "/" + arr.join("/");
+  }
+}
diff --git a/crates/router/src/core/payment_link/validator.rs b/crates/router/src/core/payment_link/validator.rs
new file mode 100644
index 00000000000..0968c8301aa
--- /dev/null
+++ b/crates/router/src/core/payment_link/validator.rs
@@ -0,0 +1,118 @@
+use actix_http::header;
+use api_models::admin::PaymentLinkConfig;
+use common_utils::validation::validate_domain_against_allowed_domains;
+use error_stack::{report, ResultExt};
+use url::Url;
+
+use crate::{
+    core::errors::{self, RouterResult},
+    types::storage::PaymentLink,
+};
+
+pub fn validate_secure_payment_link_render_request(
+    request_headers: &header::HeaderMap,
+    payment_link: &PaymentLink,
+    payment_link_config: &PaymentLinkConfig,
+) -> RouterResult<()> {
+    let link_id = payment_link.payment_link_id.clone();
+    let allowed_domains = payment_link_config
+        .allowed_domains
+        .clone()
+        .ok_or(report!(errors::ApiErrorResponse::InvalidRequestUrl))
+        .attach_printable_lazy(|| {
+            format!(
+                "Secure payment link was not generated for {}\nmissing allowed_domains",
+                link_id
+            )
+        })?;
+
+    // Validate secure_link was generated
+    if payment_link.secure_link.clone().is_none() {
+        return Err(report!(errors::ApiErrorResponse::InvalidRequestUrl)).attach_printable_lazy(
+            || {
+                format!(
+                    "Secure payment link was not generated for {}\nmissing secure_link",
+                    link_id
+                )
+            },
+        );
+    }
+
+    // Fetch destination is "iframe"
+    match request_headers.get("sec-fetch-dest").and_then(|v| v.to_str().ok()) {
+        Some("iframe") => Ok(()),
+        Some(requestor) => Err(report!(errors::ApiErrorResponse::AccessForbidden {
+            resource: "payment_link".to_string(),
+        }))
+        .attach_printable_lazy(|| {
+            format!(
+                "Access to payment_link [{}] is forbidden when requested through {}",
+                link_id, requestor
+            )
+        }),
+        None => Err(report!(errors::ApiErrorResponse::AccessForbidden {
+            resource: "payment_link".to_string(),
+        }))
+        .attach_printable_lazy(|| {
+            format!(
+                "Access to payment_link [{}] is forbidden when sec-fetch-dest is not present in request headers",
+                link_id
+            )
+        }),
+    }?;
+
+    // Validate origin / referer
+    let domain_in_req = {
+        let origin_or_referer = request_headers
+            .get("origin")
+            .or_else(|| request_headers.get("referer"))
+            .and_then(|v| v.to_str().ok())
+            .ok_or_else(|| {
+                report!(errors::ApiErrorResponse::AccessForbidden {
+                    resource: "payment_link".to_string(),
+                })
+            })
+            .attach_printable_lazy(|| {
+                format!(
+                    "Access to payment_link [{}] is forbidden when origin or referer is not present in request headers",
+                    link_id
+                )
+            })?;
+
+        let url = Url::parse(origin_or_referer)
+            .map_err(|_| {
+                report!(errors::ApiErrorResponse::AccessForbidden {
+                    resource: "payment_link".to_string(),
+                })
+            })
+            .attach_printable_lazy(|| {
+                format!("Invalid URL found in request headers {}", origin_or_referer)
+            })?;
+
+        url.host_str()
+            .and_then(|host| url.port().map(|port| format!("{}:{}", host, port)))
+            .or_else(|| url.host_str().map(String::from))
+            .ok_or_else(|| {
+                report!(errors::ApiErrorResponse::AccessForbidden {
+                    resource: "payment_link".to_string(),
+                })
+            })
+            .attach_printable_lazy(|| {
+                format!("host or port not found in request headers {:?}", url)
+            })?
+    };
+
+    if validate_domain_against_allowed_domains(&domain_in_req, allowed_domains) {
+        Ok(())
+    } else {
+        Err(report!(errors::ApiErrorResponse::AccessForbidden {
+            resource: "payment_link".to_string(),
+        }))
+        .attach_printable_lazy(|| {
+            format!(
+                "Access to payment_link [{}] is forbidden from requestor - {}",
+                link_id, domain_in_req
+            )
+        })
+    }
+}
diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs
index b7f86b6838a..07233be3996 100644
--- a/crates/router/src/core/payments/operations/payment_create.rs
+++ b/crates/router/src/core/payments/operations/payment_create.rs
@@ -1218,13 +1218,22 @@ async fn create_payment_link(
 ) -> RouterResult<Option<api_models::payments::PaymentLinkResponse>> {
     let created_at @ last_modified_at = Some(common_utils::date_time::now());
     let payment_link_id = utils::generate_id(consts::ID_LENGTH, "plink");
-    let payment_link = format!(
+    let open_payment_link = format!(
         "{}/payment_link/{}/{}",
         domain_name,
         merchant_id.get_string_repr(),
         payment_id.clone()
     );
 
+    let secure_link = payment_link_config.allowed_domains.as_ref().map(|_| {
+        format!(
+            "{}/payment_link/s/{}/{}",
+            domain_name,
+            merchant_id.clone(),
+            payment_id.clone()
+        )
+    });
+
     let payment_link_config_encoded_value = payment_link_config.encode_to_value().change_context(
         errors::ApiErrorResponse::InvalidDataValue {
             field_name: "payment_link_config",
@@ -1235,7 +1244,7 @@ async fn create_payment_link(
         payment_link_id: payment_link_id.clone(),
         payment_id: payment_id.clone(),
         merchant_id: merchant_id.clone(),
-        link_to_pay: payment_link.clone(),
+        link_to_pay: open_payment_link.clone(),
         amount: MinorUnit::from(amount),
         currency: request.currency,
         created_at,
@@ -1245,6 +1254,7 @@ async fn create_payment_link(
         description,
         payment_link_config: Some(payment_link_config_encoded_value),
         profile_id: Some(profile_id),
+        secure_link,
     };
     let payment_link_db = db
         .insert_payment_link(payment_link_req)
@@ -1254,7 +1264,8 @@ async fn create_payment_link(
         })?;
 
     Ok(Some(api_models::payments::PaymentLinkResponse {
-        link: payment_link_db.link_to_pay,
+        link: payment_link_db.link_to_pay.clone(),
+        secure_link: payment_link_db.secure_link,
         payment_link_id: payment_link_db.payment_link_id,
     }))
 }
diff --git a/crates/router/src/core/payouts/validator.rs b/crates/router/src/core/payouts/validator.rs
index 7d5b8c05ee2..f9a2939eb08 100644
--- a/crates/router/src/core/payouts/validator.rs
+++ b/crates/router/src/core/payouts/validator.rs
@@ -1,13 +1,11 @@
-use std::collections::HashSet;
-
 use actix_web::http::header;
 #[cfg(feature = "olap")]
 use common_utils::errors::CustomResult;
+use common_utils::validation::validate_domain_against_allowed_domains;
 use diesel_models::generic_link::PayoutLink;
 use error_stack::{report, ResultExt};
-use globset::Glob;
 pub use hyperswitch_domain_models::errors::StorageError;
-use router_env::{instrument, logger, tracing};
+use router_env::{instrument, tracing};
 use url::Url;
 
 use super::helpers;
@@ -255,7 +253,7 @@ pub fn validate_payout_link_render_request(
             })?
     };
 
-    if is_domain_allowed(&domain_in_req, link_data.allowed_domains) {
+    if validate_domain_against_allowed_domains(&domain_in_req, link_data.allowed_domains) {
         Ok(())
     } else {
         Err(report!(errors::ApiErrorResponse::AccessForbidden {
@@ -269,12 +267,3 @@ pub fn validate_payout_link_render_request(
         })
     }
 }
-
-fn is_domain_allowed(domain: &str, allowed_domains: HashSet<String>) -> bool {
-    allowed_domains.iter().any(|allowed_domain| {
-        Glob::new(allowed_domain)
-            .map(|glob| glob.compile_matcher().is_match(domain))
-            .map_err(|err| logger::error!("Invalid glob pattern! - {:?}", err))
-            .unwrap_or(false)
-    })
-}
diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs
index 0e1be147fbd..76b4270a058 100644
--- a/crates/router/src/routes/app.rs
+++ b/crates/router/src/routes/app.rs
@@ -1341,6 +1341,10 @@ impl PaymentLink {
                 web::resource("{merchant_id}/{payment_id}")
                     .route(web::get().to(initiate_payment_link)),
             )
+            .service(
+                web::resource("s/{merchant_id}/{payment_id}")
+                    .route(web::get().to(initiate_secure_payment_link)),
+            )
             .service(
                 web::resource("status/{merchant_id}/{payment_id}")
                     .route(web::get().to(payment_link_status)),
diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs
index 9618971e52d..9f073853d88 100644
--- a/crates/router/src/routes/lock_utils.rs
+++ b/crates/router/src/routes/lock_utils.rs
@@ -188,6 +188,7 @@ impl From<Flow> for ApiIdentifier {
 
             Flow::PaymentLinkRetrieve
             | Flow::PaymentLinkInitiate
+            | Flow::PaymentSecureLinkInitiate
             | Flow::PaymentLinkList
             | Flow::PaymentLinkStatus => Self::PaymentLink,
 
diff --git a/crates/router/src/routes/payment_link.rs b/crates/router/src/routes/payment_link.rs
index 33d3c21ef9f..b1be7091151 100644
--- a/crates/router/src/routes/payment_link.rs
+++ b/crates/router/src/routes/payment_link.rs
@@ -82,6 +82,39 @@ pub async fn initiate_payment_link(
     .await
 }
 
+pub async fn initiate_secure_payment_link(
+    state: web::Data<AppState>,
+    req: actix_web::HttpRequest,
+    path: web::Path<(common_utils::id_type::MerchantId, String)>,
+) -> impl Responder {
+    let flow = Flow::PaymentSecureLinkInitiate;
+    let (merchant_id, payment_id) = path.into_inner();
+    let payload = api_models::payments::PaymentLinkInitiateRequest {
+        payment_id,
+        merchant_id: merchant_id.clone(),
+    };
+    let headers = req.headers();
+    Box::pin(api::server_wrap(
+        flow,
+        state,
+        &req,
+        payload.clone(),
+        |state, auth, _, _| {
+            initiate_secure_payment_link_flow(
+                state,
+                auth.merchant_account,
+                auth.key_store,
+                payload.merchant_id.clone(),
+                payload.payment_id.clone(),
+                headers,
+            )
+        },
+        &crate::services::authentication::MerchantIdAuth(merchant_id),
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
+
 /// Payment Link - List
 ///
 /// To list the payment links
diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs
index 129be81e41e..a973d855733 100644
--- a/crates/router/src/services/api.rs
+++ b/crates/router/src/services/api.rs
@@ -47,7 +47,7 @@ use masking::{Maskable, PeekInterface};
 use router_env::{instrument, metrics::add_attributes, tracing, tracing_actix_web::RequestId, Tag};
 use serde::Serialize;
 use serde_json::json;
-use tera::{Context, Tera};
+use tera::{Context, Error as TeraError, Tera};
 
 use self::request::{HeaderExt, RequestBuilderExt};
 use super::{
@@ -1058,14 +1058,18 @@ where
             let link_type = boxed_generic_link_data.data.to_string();
             match build_generic_link_html(boxed_generic_link_data.data) {
                 Ok(rendered_html) => {
-                    let domains_str = boxed_generic_link_data
-                        .allowed_domains
-                        .into_iter()
-                        .collect::<Vec<String>>()
-                        .join(" ");
-                    let csp_header = format!("frame-ancestors 'self' {};", domains_str);
-                    let headers = HashSet::from([("content-security-policy", csp_header)]);
-                    http_response_html_data(rendered_html, Some(headers))
+                    let headers = if !boxed_generic_link_data.allowed_domains.is_empty() {
+                        let domains_str = boxed_generic_link_data
+                            .allowed_domains
+                            .into_iter()
+                            .collect::<Vec<String>>()
+                            .join(" ");
+                        let csp_header = format!("frame-ancestors 'self' {};", domains_str);
+                        Some(HashSet::from([("content-security-policy", csp_header)]))
+                    } else {
+                        None
+                    };
+                    http_response_html_data(rendered_html, headers)
                 }
                 Err(_) => {
                     http_response_err(format!("Error while rendering {} HTML page", link_type))
@@ -1863,9 +1867,9 @@ pub fn build_redirection_form(
     }
 }
 
-pub fn build_payment_link_html(
+fn build_payment_link_template(
     payment_link_data: PaymentLinkFormData,
-) -> CustomResult<String, errors::ApiErrorResponse> {
+) -> CustomResult<(Tera, Context), errors::ApiErrorResponse> {
     let mut tera = Tera::default();
 
     // Add modification to css template with dynamic data
@@ -1916,11 +1920,6 @@ pub fn build_payment_link_html(
         &get_preload_link_html_template(&payment_link_data.sdk_url),
     );
 
-    context.insert(
-        "preload_link_tags",
-        &get_preload_link_html_template(&payment_link_data.sdk_url),
-    );
-
     context.insert(
         "hyperloader_sdk_link",
         &get_hyper_loader_sdk(&payment_link_data.sdk_url),
@@ -1930,13 +1929,43 @@ pub fn build_payment_link_html(
 
     context.insert("logging_template", &logging_template);
 
-    match tera.render("payment_link", &context) {
-        Ok(rendered_html) => Ok(rendered_html),
-        Err(tera_error) => {
+    Ok((tera, context))
+}
+
+pub fn build_payment_link_html(
+    payment_link_data: PaymentLinkFormData,
+) -> CustomResult<String, errors::ApiErrorResponse> {
+    let (tera, mut context) = build_payment_link_template(payment_link_data)
+        .attach_printable("Failed to build payment link's HTML template")?;
+    let payment_link_initiator =
+        include_str!("../core/payment_link/payment_link_initiate/payment_link_initiator.js")
+            .to_string();
+    context.insert("payment_link_initiator", &payment_link_initiator);
+
+    tera.render("payment_link", &context)
+        .map_err(|tera_error: TeraError| {
             crate::logger::warn!("{tera_error}");
-            Err(errors::ApiErrorResponse::InternalServerError)?
-        }
-    }
+            report!(errors::ApiErrorResponse::InternalServerError)
+        })
+        .attach_printable("Error while rendering open payment link's HTML template")
+}
+
+pub fn build_secure_payment_link_html(
+    payment_link_data: PaymentLinkFormData,
+) -> CustomResult<String, errors::ApiErrorResponse> {
+    let (tera, mut context) = build_payment_link_template(payment_link_data)
+        .attach_printable("Failed to build payment link's HTML template")?;
+    let payment_link_initiator =
+        include_str!("../core/payment_link/payment_link_initiate/secure_payment_link_initiator.js")
+            .to_string();
+    context.insert("payment_link_initiator", &payment_link_initiator);
+
+    tera.render("payment_link", &context)
+        .map_err(|tera_error: TeraError| {
+            crate::logger::warn!("{tera_error}");
+            report!(errors::ApiErrorResponse::InternalServerError)
+        })
+        .attach_printable("Error while rendering secure payment link's HTML template")
 }
 
 fn get_hyper_loader_sdk(sdk_url: &str) -> String {
diff --git a/crates/router/src/services/api/generic_link_response.rs b/crates/router/src/services/api/generic_link_response.rs
index 497926481ab..f180e3d3f33 100644
--- a/crates/router/src/services/api/generic_link_response.rs
+++ b/crates/router/src/services/api/generic_link_response.rs
@@ -5,6 +5,7 @@ use hyperswitch_domain_models::api::{
 };
 use tera::{Context, Tera};
 
+use super::build_secure_payment_link_html;
 use crate::core::errors;
 
 pub fn build_generic_link_html(
@@ -24,6 +25,9 @@ pub fn build_generic_link_html(
         GenericLinksData::PayoutLinkStatus(pm_collect_data) => {
             build_payout_link_status_html(&pm_collect_data)
         }
+        GenericLinksData::SecurePaymentLink(payment_link_data) => {
+            build_secure_payment_link_html(payment_link_data)
+        }
     }
 }
 
@@ -45,46 +49,58 @@ pub fn build_generic_expired_link_html(
         .attach_printable("Failed to render expired link HTML template")
 }
 
-pub fn build_payout_link_html(
+fn build_html_template(
     link_data: &GenericLinkFormData,
-) -> CustomResult<String, errors::ApiErrorResponse> {
-    let mut tera = Tera::default();
+    document: &'static str,
+    script: &'static str,
+    styles: &'static str,
+) -> CustomResult<(Tera, Context), errors::ApiErrorResponse> {
+    let mut tera: Tera = Tera::default();
     let mut context = Context::new();
 
     // Insert dynamic context in CSS
     let css_dynamic_context = "{{ color_scheme }}";
-    let css_template =
-        include_str!("../../core/generic_link/payout_link/initiate/styles.css").to_string();
+    let css_template = styles.to_string();
     let final_css = format!("{}\n{}", css_dynamic_context, css_template);
-    let _ = tera.add_raw_template("payout_link_styles", &final_css);
+    let _ = tera.add_raw_template("document_styles", &final_css);
     context.insert("color_scheme", &link_data.css_data);
 
+    // Insert dynamic context in JS
+    let js_dynamic_context = "{{ script_data }}";
+    let js_template = script.to_string();
+    let final_js = format!("{}\n{}", js_dynamic_context, js_template);
+    let _ = tera.add_raw_template("document_scripts", &final_js);
+    context.insert("script_data", &link_data.js_data);
+
     let css_style_tag = tera
-        .render("payout_link_styles", &context)
+        .render("document_styles", &context)
         .map(|css| format!("<style>{}</style>", css))
         .change_context(errors::ApiErrorResponse::InternalServerError)
-        .attach_printable("Failed to render payout link's CSS template")?;
-
-    // Insert dynamic context in JS
-    let js_dynamic_context = "{{ payout_link_context }}";
-    let js_template =
-        include_str!("../../core/generic_link/payout_link/initiate/script.js").to_string();
-    let final_js = format!("{}\n{}", js_dynamic_context, js_template);
-    let _ = tera.add_raw_template("payout_link_script", &final_js);
-    context.insert("payout_link_context", &link_data.js_data);
+        .attach_printable("Failed to render CSS template")?;
 
     let js_script_tag = tera
-        .render("payout_link_script", &context)
+        .render("document_scripts", &context)
         .map(|js| format!("<script>{}</script>", js))
         .change_context(errors::ApiErrorResponse::InternalServerError)
-        .attach_printable("Failed to render payout link's JS template")?;
+        .attach_printable("Failed to render JS template")?;
 
-    // Build HTML
-    let html_template =
-        include_str!("../../core/generic_link/payout_link/initiate/index.html").to_string();
-    let _ = tera.add_raw_template("payout_link", &html_template);
+    // Insert HTML context
+    let html_template = document.to_string();
+    let _ = tera.add_raw_template("html_template", &html_template);
     context.insert("css_style_tag", &css_style_tag);
     context.insert("js_script_tag", &js_script_tag);
+
+    Ok((tera, context))
+}
+
+pub fn build_payout_link_html(
+    link_data: &GenericLinkFormData,
+) -> CustomResult<String, errors::ApiErrorResponse> {
+    let document = include_str!("../../core/generic_link/payout_link/initiate/index.html");
+    let script = include_str!("../../core/generic_link/payout_link/initiate/script.js");
+    let styles = include_str!("../../core/generic_link/payout_link/initiate/styles.css");
+    let (tera, mut context) = build_html_template(link_data, document, script, styles)
+        .attach_printable("Failed to build context for payout link's HTML template")?;
     context.insert(
         "hyper_sdk_loader_script_tag",
         &format!(
@@ -93,7 +109,7 @@ pub fn build_payout_link_html(
         ),
     );
 
-    tera.render("payout_link", &context)
+    tera.render("html_template", &context)
         .change_context(errors::ApiErrorResponse::InternalServerError)
         .attach_printable("Failed to render payout link's HTML template")
 }
@@ -101,46 +117,14 @@ pub fn build_payout_link_html(
 pub fn build_pm_collect_link_html(
     link_data: &GenericLinkFormData,
 ) -> CustomResult<String, errors::ApiErrorResponse> {
-    let mut tera = Tera::default();
-    let mut context = Context::new();
-
-    // Insert dynamic context in CSS
-    let css_dynamic_context = "{{ color_scheme }}";
-    let css_template =
-        include_str!("../../core/generic_link/payment_method_collect/initiate/styles.css")
-            .to_string();
-    let final_css = format!("{}\n{}", css_dynamic_context, css_template);
-    let _ = tera.add_raw_template("pm_collect_link_styles", &final_css);
-    context.insert("color_scheme", &link_data.css_data);
-
-    let css_style_tag = tera
-        .render("pm_collect_link_styles", &context)
-        .map(|css| format!("<style>{}</style>", css))
-        .change_context(errors::ApiErrorResponse::InternalServerError)
-        .attach_printable("Failed to render payment method collect link's CSS template")?;
-
-    // Insert dynamic context in JS
-    let js_dynamic_context = "{{ collect_link_context }}";
-    let js_template =
-        include_str!("../../core/generic_link/payment_method_collect/initiate/script.js")
-            .to_string();
-    let final_js = format!("{}\n{}", js_dynamic_context, js_template);
-    let _ = tera.add_raw_template("pm_collect_link_script", &final_js);
-    context.insert("collect_link_context", &link_data.js_data);
-
-    let js_script_tag = tera
-        .render("pm_collect_link_script", &context)
-        .map(|js| format!("<script>{}</script>", js))
-        .change_context(errors::ApiErrorResponse::InternalServerError)
-        .attach_printable("Failed to render payment method collect link's JS template")?;
-
-    // Build HTML
-    let html_template =
-        include_str!("../../core/generic_link/payment_method_collect/initiate/index.html")
-            .to_string();
-    let _ = tera.add_raw_template("payment_method_collect_link", &html_template);
-    context.insert("css_style_tag", &css_style_tag);
-    context.insert("js_script_tag", &js_script_tag);
+    let document =
+        include_str!("../../core/generic_link/payment_method_collect/initiate/index.html");
+    let script = include_str!("../../core/generic_link/payment_method_collect/initiate/script.js");
+    let styles = include_str!("../../core/generic_link/payment_method_collect/initiate/styles.css");
+    let (tera, mut context) = build_html_template(link_data, document, script, styles)
+        .attach_printable(
+            "Failed to build context for payment method collect link's HTML template",
+        )?;
     context.insert(
         "hyper_sdk_loader_script_tag",
         &format!(
@@ -149,7 +133,7 @@ pub fn build_pm_collect_link_html(
         ),
     );
 
-    tera.render("payment_method_collect_link", &context)
+    tera.render("html_template", &context)
         .change_context(errors::ApiErrorResponse::InternalServerError)
         .attach_printable("Failed to render payment method collect link's HTML template")
 }
diff --git a/crates/router/src/types/api/payment_link.rs b/crates/router/src/types/api/payment_link.rs
index 85cb539d411..3d50db35fc4 100644
--- a/crates/router/src/types/api/payment_link.rs
+++ b/crates/router/src/types/api/payment_link.rs
@@ -30,6 +30,7 @@ impl PaymentLinkResponseExt for RetrievePaymentLinkResponse {
             expiry: payment_link.fulfilment_time,
             currency: payment_link.currency,
             status,
+            secure_link: payment_link.secure_link,
         })
     }
 }
diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs
index 8a29ac7e79a..a510a26151b 100644
--- a/crates/router/src/types/transformers.rs
+++ b/crates/router/src/types/transformers.rs
@@ -1289,6 +1289,7 @@ impl ForeignFrom<(storage::PaymentLink, payments::PaymentLinkStatus)>
             description: payment_link_config.description,
             currency: payment_link_config.currency,
             status,
+            secure_link: payment_link_config.secure_link,
         }
     }
 }
diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs
index cefc2fbd1d0..f1d2e5d8dcc 100644
--- a/crates/router_env/src/logger/types.rs
+++ b/crates/router_env/src/logger/types.rs
@@ -290,6 +290,8 @@ pub enum Flow {
     PaymentLinkRetrieve,
     /// payment Link Initiate flow
     PaymentLinkInitiate,
+    /// payment Link Initiate flow
+    PaymentSecureLinkInitiate,
     /// Payment Link List flow
     PaymentLinkList,
     /// Payment Link Status
diff --git a/migrations/2024-07-17-131830_alter_payment_link/down.sql b/migrations/2024-07-17-131830_alter_payment_link/down.sql
new file mode 100644
index 00000000000..db8d02eb036
--- /dev/null
+++ b/migrations/2024-07-17-131830_alter_payment_link/down.sql
@@ -0,0 +1 @@
+ALTER table payment_link DROP COLUMN IF EXISTS secure_link;
\ No newline at end of file
diff --git a/migrations/2024-07-17-131830_alter_payment_link/up.sql b/migrations/2024-07-17-131830_alter_payment_link/up.sql
new file mode 100644
index 00000000000..979c92a0ac7
--- /dev/null
+++ b/migrations/2024-07-17-131830_alter_payment_link/up.sql
@@ -0,0 +1,2 @@
+-- Add a new column for allowed domains and secure link endpoint
+ALTER table payment_link ADD COLUMN IF NOT EXISTS secure_link VARCHAR(255);
\ No newline at end of file

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

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

## Description
Described in https://github.com/juspay/hyperswitch/issues/5372


### Additional Changes

- [x] This PR modifies the API contract
- [x] 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).
-->


## How did you test it?
Tested locally using postman collection. Expectations -
- existing payment links is available as open links (cannot list SPMs or render checkbox to store PM)
- secure links are generated only when allowed_domains are configured
- secure links can only be accessed from within an iframe on the allowed domains

#### Open links
<details>
<summary>1. Create a payment link - (unfold to view cURL)</summary>
curl --location 'http://localhost:8080/payments' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: dev_eBTVu8OMN1HmE2xr0I0dsfypWOVwQBlietvkxf0sgYMdyFM5nRO40M8M4MLH42Jk' \
--data '{
    "customer_id": "cus_izAOZiynP5URGEvEfmgH",
    "amount": 100,
    "currency": "USD",
    "payment_link": true,
    "connector": [
        "stripe"
    ],
    "session_expiry": 1000000,
    "return_url": "http://127.0.0.1:5500/src/pl_iframe.html",
    "payment_link_config": {
        "theme": "#14356f",
        "logo": "https://logosandtypes.com/wp-content/uploads/2020/08/zurich.svg",
        "seller_name": "Zurich Inc."
    }
}'
</details>

2. Open the payment link (`link` in API response) and validate the functionality

#### Secure links
<details>
<summary>1. Update `allowed_domains` in business profile - (unfold to view cURL)</summary>
curl --location 'http://localhost:8080/account/merchant_1721984694/business_profile/pro_oXHnmgfZSnfe92PvodqP' \
--header 'Content-Type: application/json' \
--header 'api-key: test_admin' \
--data '{
    "payment_link_config": {
        "allowed_domains": [
            "*"
        ],
        "enabled_saved_payment_method": true
    }
}'
</details>
<details>
<summary>2. Create a payment link - (unfold to view cURL)</summary>
curl --location 'http://localhost:8080/payments' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: dev_eBTVu8OMN1HmE2xr0I0dsfypWOVwQBlietvkxf0sgYMdyFM5nRO40M8M4MLH42Jk' \
--data '{
    "customer_id": "cus_izAOZiynP5URGEvEfmgH",
    "amount": 100,
    "currency": "USD",
    "payment_link": true,
    "connector": [
        "stripe"
    ],
    "session_expiry": 1000000,
    "return_url": "http://127.0.0.1:5500/src/pl_iframe.html",
    "payment_link_config": {
        "theme": "#14356f",
        "logo": "https://logosandtypes.com/wp-content/uploads/2020/08/zurich.svg",
        "seller_name": "Zurich Inc."
    }
}'
</details>
3. Open the secure payment link (`secure_link` in API response) in an iframe for validating the functionality

#### Secure links demo
[Screencast from 26-07-24 02:34:52 PM IST.webm](https://github.com/user-attachments/assets/fca25275-fcb3-49cf-9fc5-967884b635d2)



## 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
