Bug: Subscription Create to return client secret and create payment intent automatically




diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs
index 3b343459e9e..7c48d626f2d 100644
--- a/crates/api_models/src/lib.rs
+++ b/crates/api_models/src/lib.rs
@@ -42,6 +42,8 @@ pub mod recon;
 pub mod refunds;
 pub mod relay;
 pub mod routing;
+#[cfg(feature = "v1")]
+pub mod subscription;
 pub mod surcharge_decision_configs;
 pub mod three_ds_decision_rule;
 #[cfg(feature = "tokenization_v2")]
diff --git a/crates/api_models/src/subscription.rs b/crates/api_models/src/subscription.rs
new file mode 100644
index 00000000000..7dff21205a2
--- /dev/null
+++ b/crates/api_models/src/subscription.rs
@@ -0,0 +1,109 @@
+use common_utils::events::ApiEventMetric;
+use masking::Secret;
+use utoipa::ToSchema;
+
+// use crate::{
+//     customers::{CustomerRequest, CustomerResponse},
+//     payments::CustomerDetailsResponse,
+// };
+
+/// Request payload for creating a subscription.
+///
+/// This struct captures details required to create a subscription,
+/// including plan, profile, merchant connector, and optional customer info.
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
+pub struct CreateSubscriptionRequest {
+    /// Merchant specific Unique identifier.
+    pub merchant_reference_id: Option<String>,
+
+    /// Identifier for the subscription plan.
+    pub plan_id: Option<String>,
+
+    /// Optional coupon code applied to the subscription.
+    pub coupon_code: Option<String>,
+
+    /// customer ID associated with this subscription.
+    pub customer_id: common_utils::id_type::CustomerId,
+}
+
+/// Response payload returned after successfully creating a subscription.
+///
+/// Includes details such as subscription ID, status, plan, merchant, and customer info.
+#[derive(Debug, Clone, serde::Serialize, ToSchema)]
+pub struct CreateSubscriptionResponse {
+    /// Unique identifier for the subscription.
+    pub id: common_utils::id_type::SubscriptionId,
+
+    /// Merchant specific Unique identifier.
+    pub merchant_reference_id: Option<String>,
+
+    /// Current status of the subscription.
+    pub status: SubscriptionStatus,
+
+    /// Identifier for the associated subscription plan.
+    pub plan_id: Option<String>,
+
+    /// Associated profile ID.
+    pub profile_id: common_utils::id_type::ProfileId,
+
+    /// Optional client secret used for secure client-side interactions.
+    pub client_secret: Option<Secret<String>>,
+
+    /// Merchant identifier owning this subscription.
+    pub merchant_id: common_utils::id_type::MerchantId,
+
+    /// Optional coupon code applied to this subscription.
+    pub coupon_code: Option<String>,
+
+    /// Optional customer ID associated with this subscription.
+    pub customer_id: common_utils::id_type::CustomerId,
+}
+
+/// Possible states of a subscription lifecycle.
+///
+/// - `Created`: Subscription was created but not yet activated.
+/// - `Active`: Subscription is currently active.
+/// - `InActive`: Subscription is inactive (e.g., cancelled or expired).
+#[derive(Debug, Clone, serde::Serialize, strum::EnumString, strum::Display, ToSchema)]
+pub enum SubscriptionStatus {
+    /// Subscription is active.
+    Active,
+    /// Subscription is created but not yet active.
+    Created,
+    /// Subscription is inactive.
+    InActive,
+    /// Subscription is in pending state.
+    Pending,
+}
+
+impl CreateSubscriptionResponse {
+    /// Creates a new [`CreateSubscriptionResponse`] with the given identifiers.
+    ///
+    /// By default, `client_secret`, `coupon_code`, and `customer` fields are `None`.
+    #[allow(clippy::too_many_arguments)]
+    pub fn new(
+        id: common_utils::id_type::SubscriptionId,
+        merchant_reference_id: Option<String>,
+        status: SubscriptionStatus,
+        plan_id: Option<String>,
+        profile_id: common_utils::id_type::ProfileId,
+        merchant_id: common_utils::id_type::MerchantId,
+        client_secret: Option<Secret<String>>,
+        customer_id: common_utils::id_type::CustomerId,
+    ) -> Self {
+        Self {
+            id,
+            merchant_reference_id,
+            status,
+            plan_id,
+            profile_id,
+            client_secret,
+            merchant_id,
+            coupon_code: None,
+            customer_id,
+        }
+    }
+}
+
+impl ApiEventMetric for CreateSubscriptionResponse {}
+impl ApiEventMetric for CreateSubscriptionRequest {}
diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs
index bbb1be10b80..03ceab3f9a2 100644
--- a/crates/common_enums/src/enums.rs
+++ b/crates/common_enums/src/enums.rs
@@ -8412,6 +8412,7 @@ pub enum Resource {
     RunRecon,
     ReconConfig,
     RevenueRecovery,
+    Subscription,
     InternalConnector,
     Theme,
 }
diff --git a/crates/common_utils/src/events.rs b/crates/common_utils/src/events.rs
index 49be22fc931..dc8f6e333a4 100644
--- a/crates/common_utils/src/events.rs
+++ b/crates/common_utils/src/events.rs
@@ -85,6 +85,7 @@ pub enum ApiEventsType {
         payment_id: Option<id_type::GlobalPaymentId>,
     },
     Routing,
+    Subscription,
     ResourceListAPI,
     #[cfg(feature = "v1")]
     PaymentRedirectionResponse {
diff --git a/crates/common_utils/src/id_type.rs b/crates/common_utils/src/id_type.rs
index 6d73a90eab2..237ca661c10 100644
--- a/crates/common_utils/src/id_type.rs
+++ b/crates/common_utils/src/id_type.rs
@@ -17,6 +17,7 @@ mod profile_acquirer;
 mod refunds;
 mod relay;
 mod routing;
+mod subscription;
 mod tenant;
 
 use std::{borrow::Cow, fmt::Debug};
@@ -55,6 +56,7 @@ pub use self::{
     refunds::RefundReferenceId,
     relay::RelayId,
     routing::RoutingId,
+    subscription::SubscriptionId,
     tenant::TenantId,
 };
 use crate::{fp_utils::when, generate_id_with_default_len};
diff --git a/crates/common_utils/src/id_type/subscription.rs b/crates/common_utils/src/id_type/subscription.rs
new file mode 100644
index 00000000000..20f0c483fa1
--- /dev/null
+++ b/crates/common_utils/src/id_type/subscription.rs
@@ -0,0 +1,21 @@
+crate::id_type!(
+    SubscriptionId,
+    " A type for subscription_id that can be used for subscription ids"
+);
+
+crate::impl_id_type_methods!(SubscriptionId, "subscription_id");
+
+// This is to display the `SubscriptionId` as SubscriptionId(subs)
+crate::impl_debug_id_type!(SubscriptionId);
+crate::impl_try_from_cow_str_id_type!(SubscriptionId, "subscription_id");
+
+crate::impl_generate_id_id_type!(SubscriptionId, "subscription");
+crate::impl_serializable_secret_id_type!(SubscriptionId);
+crate::impl_queryable_id_type!(SubscriptionId);
+crate::impl_to_sql_from_sql_id_type!(SubscriptionId);
+
+impl crate::events::ApiEventMetric for SubscriptionId {
+    fn get_api_event_type(&self) -> Option<crate::events::ApiEventsType> {
+        Some(crate::events::ApiEventsType::Subscription)
+    }
+}
diff --git a/crates/diesel_models/src/query/subscription.rs b/crates/diesel_models/src/query/subscription.rs
index e25f1617873..7a6bdd19c15 100644
--- a/crates/diesel_models/src/query/subscription.rs
+++ b/crates/diesel_models/src/query/subscription.rs
@@ -19,13 +19,13 @@ impl Subscription {
     pub async fn find_by_merchant_id_subscription_id(
         conn: &PgPooledConn,
         merchant_id: &common_utils::id_type::MerchantId,
-        subscription_id: String,
+        id: String,
     ) -> StorageResult<Self> {
         generics::generic_find_one::<<Self as HasTable>::Table, _, _>(
             conn,
             dsl::merchant_id
                 .eq(merchant_id.to_owned())
-                .and(dsl::subscription_id.eq(subscription_id.to_owned())),
+                .and(dsl::id.eq(id.to_owned())),
         )
         .await
     }
@@ -33,7 +33,7 @@ impl Subscription {
     pub async fn update_subscription_entry(
         conn: &PgPooledConn,
         merchant_id: &common_utils::id_type::MerchantId,
-        subscription_id: String,
+        id: String,
         subscription_update: SubscriptionUpdate,
     ) -> StorageResult<Self> {
         generics::generic_update_with_results::<
@@ -43,8 +43,8 @@ impl Subscription {
             _,
         >(
             conn,
-            dsl::subscription_id
-                .eq(subscription_id.to_owned())
+            dsl::id
+                .eq(id.to_owned())
                 .and(dsl::merchant_id.eq(merchant_id.to_owned())),
             subscription_update,
         )
diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs
index 271d2a79ba0..c02a9748f81 100644
--- a/crates/diesel_models/src/schema.rs
+++ b/crates/diesel_models/src/schema.rs
@@ -1530,9 +1530,9 @@ diesel::table! {
     use diesel::sql_types::*;
     use crate::enums::diesel_exports::*;
 
-    subscription (subscription_id, merchant_id) {
+    subscription (id) {
         #[max_length = 128]
-        subscription_id -> Varchar,
+        id -> Varchar,
         #[max_length = 128]
         status -> Varchar,
         #[max_length = 128]
@@ -1554,6 +1554,8 @@ diesel::table! {
         modified_at -> Timestamp,
         #[max_length = 64]
         profile_id -> Varchar,
+        #[max_length = 128]
+        merchant_reference_id -> Nullable<Varchar>,
     }
 }
 
diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs
index 4195475f88d..4648eed49f7 100644
--- a/crates/diesel_models/src/schema_v2.rs
+++ b/crates/diesel_models/src/schema_v2.rs
@@ -1465,9 +1465,9 @@ diesel::table! {
     use diesel::sql_types::*;
     use crate::enums::diesel_exports::*;
 
-    subscription (subscription_id, merchant_id) {
+    subscription (id) {
         #[max_length = 128]
-        subscription_id -> Varchar,
+        id -> Varchar,
         #[max_length = 128]
         status -> Varchar,
         #[max_length = 128]
@@ -1489,6 +1489,8 @@ diesel::table! {
         modified_at -> Timestamp,
         #[max_length = 64]
         profile_id -> Varchar,
+        #[max_length = 128]
+        merchant_reference_id -> Nullable<Varchar>,
     }
 }
 
diff --git a/crates/diesel_models/src/subscription.rs b/crates/diesel_models/src/subscription.rs
index b2cb6e48470..f49fef4a7be 100644
--- a/crates/diesel_models/src/subscription.rs
+++ b/crates/diesel_models/src/subscription.rs
@@ -1,5 +1,6 @@
-use common_utils::pii::SecretSerdeValue;
+use common_utils::{generate_id_with_default_len, pii::SecretSerdeValue};
 use diesel::{AsChangeset, Identifiable, Insertable, Queryable, Selectable};
+use masking::Secret;
 use serde::{Deserialize, Serialize};
 
 use crate::schema::subscription;
@@ -7,7 +8,7 @@ use crate::schema::subscription;
 #[derive(Clone, Debug, Eq, Insertable, PartialEq, Serialize, Deserialize)]
 #[diesel(table_name = subscription)]
 pub struct SubscriptionNew {
-    subscription_id: String,
+    id: common_utils::id_type::SubscriptionId,
     status: String,
     billing_processor: Option<String>,
     payment_method_id: Option<String>,
@@ -20,14 +21,15 @@ pub struct SubscriptionNew {
     created_at: time::PrimitiveDateTime,
     modified_at: time::PrimitiveDateTime,
     profile_id: common_utils::id_type::ProfileId,
+    merchant_reference_id: Option<String>,
 }
 
 #[derive(
     Clone, Debug, Eq, PartialEq, Identifiable, Queryable, Selectable, Deserialize, Serialize,
 )]
-#[diesel(table_name = subscription, primary_key(subscription_id, merchant_id), check_for_backend(diesel::pg::Pg))]
+#[diesel(table_name = subscription, primary_key(id), check_for_backend(diesel::pg::Pg))]
 pub struct Subscription {
-    pub subscription_id: String,
+    pub id: common_utils::id_type::SubscriptionId,
     pub status: String,
     pub billing_processor: Option<String>,
     pub payment_method_id: Option<String>,
@@ -40,6 +42,7 @@ pub struct Subscription {
     pub created_at: time::PrimitiveDateTime,
     pub modified_at: time::PrimitiveDateTime,
     pub profile_id: common_utils::id_type::ProfileId,
+    pub merchant_reference_id: Option<String>,
 }
 
 #[derive(Clone, Debug, Eq, PartialEq, AsChangeset, router_derive::DebugAsDisplay, Deserialize)]
@@ -53,7 +56,7 @@ pub struct SubscriptionUpdate {
 impl SubscriptionNew {
     #[allow(clippy::too_many_arguments)]
     pub fn new(
-        subscription_id: String,
+        id: common_utils::id_type::SubscriptionId,
         status: String,
         billing_processor: Option<String>,
         payment_method_id: Option<String>,
@@ -64,10 +67,11 @@ impl SubscriptionNew {
         customer_id: common_utils::id_type::CustomerId,
         metadata: Option<SecretSerdeValue>,
         profile_id: common_utils::id_type::ProfileId,
+        merchant_reference_id: Option<String>,
     ) -> Self {
         let now = common_utils::date_time::now();
         Self {
-            subscription_id,
+            id,
             status,
             billing_processor,
             payment_method_id,
@@ -80,8 +84,16 @@ impl SubscriptionNew {
             created_at: now,
             modified_at: now,
             profile_id,
+            merchant_reference_id,
         }
     }
+
+    pub fn generate_and_set_client_secret(&mut self) -> Secret<String> {
+        let client_secret =
+            generate_id_with_default_len(&format!("{}_secret", self.id.get_string_repr()));
+        self.client_secret = Some(client_secret.clone());
+        Secret::new(client_secret)
+    }
 }
 
 impl SubscriptionUpdate {
diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs
index f7cb256f587..e6c13f5344f 100644
--- a/crates/router/src/core.rs
+++ b/crates/router/src/core.rs
@@ -51,6 +51,8 @@ pub mod refunds_v2;
 #[cfg(feature = "v1")]
 pub mod debit_routing;
 pub mod routing;
+#[cfg(feature = "v1")]
+pub mod subscription;
 pub mod surcharge_decision_config;
 pub mod three_ds_decision_rule;
 #[cfg(feature = "olap")]
diff --git a/crates/router/src/core/subscription.rs b/crates/router/src/core/subscription.rs
new file mode 100644
index 00000000000..32c6754e2fe
--- /dev/null
+++ b/crates/router/src/core/subscription.rs
@@ -0,0 +1,65 @@
+use std::str::FromStr;
+
+use api_models::subscription::{
+    self as subscription_types, CreateSubscriptionResponse, SubscriptionStatus,
+};
+use common_utils::id_type::GenerateId;
+use diesel_models::subscription::SubscriptionNew;
+use error_stack::ResultExt;
+use hyperswitch_domain_models::{api::ApplicationResponse, merchant_context::MerchantContext};
+use masking::Secret;
+
+use super::errors::{self, RouterResponse};
+use crate::routes::SessionState;
+
+pub async fn create_subscription(
+    state: SessionState,
+    merchant_context: MerchantContext,
+    profile_id: String,
+    request: subscription_types::CreateSubscriptionRequest,
+) -> RouterResponse<CreateSubscriptionResponse> {
+    let store = state.store.clone();
+    let db = store.as_ref();
+    let id = common_utils::id_type::SubscriptionId::generate();
+    let profile_id = common_utils::id_type::ProfileId::from_str(&profile_id).change_context(
+        errors::ApiErrorResponse::InvalidDataValue {
+            field_name: "X-Profile-Id",
+        },
+    )?;
+
+    let mut subscription = SubscriptionNew::new(
+        id,
+        SubscriptionStatus::Created.to_string(),
+        None,
+        None,
+        None,
+        None,
+        None,
+        merchant_context.get_merchant_account().get_id().clone(),
+        request.customer_id.clone(),
+        None,
+        profile_id,
+        request.merchant_reference_id,
+    );
+
+    subscription.generate_and_set_client_secret();
+    let subscription_response = db
+        .insert_subscription_entry(subscription)
+        .await
+        .change_context(errors::ApiErrorResponse::InternalServerError)
+        .attach_printable("subscriptions: unable to insert subscription entry to database")?;
+
+    let response = CreateSubscriptionResponse::new(
+        subscription_response.id.clone(),
+        subscription_response.merchant_reference_id,
+        SubscriptionStatus::from_str(&subscription_response.status)
+            .unwrap_or(SubscriptionStatus::Created),
+        None,
+        subscription_response.profile_id,
+        subscription_response.merchant_id,
+        subscription_response.client_secret.map(Secret::new),
+        request.customer_id,
+    );
+
+    Ok(ApplicationResponse::Json(response))
+}
diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs
index d92dc311e0c..fd9c7f8a0a5 100644
--- a/crates/router/src/lib.rs
+++ b/crates/router/src/lib.rs
@@ -211,6 +211,7 @@ pub fn mk_app(
                 .service(routes::Files::server(state.clone()))
                 .service(routes::Disputes::server(state.clone()))
                 .service(routes::Blocklist::server(state.clone()))
+                .service(routes::Subscription::server(state.clone()))
                 .service(routes::Gsm::server(state.clone()))
                 .service(routes::ApplePayCertificatesMigration::server(state.clone()))
                 .service(routes::PaymentLink::server(state.clone()))
diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs
index b8fde79e2a1..f2af4631917 100644
--- a/crates/router/src/routes.rs
+++ b/crates/router/src/routes.rs
@@ -50,6 +50,8 @@ pub mod recon;
 pub mod refunds;
 #[cfg(feature = "olap")]
 pub mod routing;
+#[cfg(feature = "v1")]
+pub mod subscription;
 pub mod three_ds_decision_rule;
 pub mod tokenization;
 #[cfg(feature = "olap")]
@@ -96,7 +98,7 @@ pub use self::app::{
     User, UserDeprecated, Webhooks,
 };
 #[cfg(feature = "olap")]
-pub use self::app::{Blocklist, Organization, Routing, Verify, WebhookEvents};
+pub use self::app::{Blocklist, Organization, Routing, Subscription, Verify, WebhookEvents};
 #[cfg(feature = "payouts")]
 pub use self::app::{PayoutLink, Payouts};
 #[cfg(all(feature = "stripe", feature = "v1"))]
diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs
index d52dc1570ce..1cdc2479364 100644
--- a/crates/router/src/routes/app.rs
+++ b/crates/router/src/routes/app.rs
@@ -65,7 +65,9 @@ use super::{
     profiles, relay, user, user_role,
 };
 #[cfg(feature = "v1")]
-use super::{apple_pay_certificates_migration, blocklist, payment_link, webhook_events};
+use super::{
+    apple_pay_certificates_migration, blocklist, payment_link, subscription, webhook_events,
+};
 #[cfg(any(feature = "olap", feature = "oltp"))]
 use super::{configs::*, customers, payments};
 #[cfg(all(any(feature = "olap", feature = "oltp"), feature = "v1"))]
@@ -1163,6 +1165,22 @@ impl Routing {
     }
 }
 
+#[cfg(feature = "oltp")]
+pub struct Subscription;
+
+#[cfg(all(feature = "oltp", feature = "v1"))]
+impl Subscription {
+    pub fn server(state: AppState) -> Scope {
+        web::scope("/subscription/create")
+            .app_data(web::Data::new(state.clone()))
+            .service(web::resource("").route(
+                web::post().to(|state, req, payload| {
+                    subscription::create_subscription(state, req, payload)
+                }),
+            ))
+    }
+}
+
 pub struct Customers;
 
 #[cfg(all(feature = "v2", any(feature = "olap", feature = "oltp")))]
diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs
index 28581198d83..8bbddaedc8e 100644
--- a/crates/router/src/routes/lock_utils.rs
+++ b/crates/router/src/routes/lock_utils.rs
@@ -26,6 +26,7 @@ pub enum ApiIdentifier {
     ApiKeys,
     PaymentLink,
     Routing,
+    Subscription,
     Blocklist,
     Forex,
     RustLockerMigration,
@@ -88,6 +89,8 @@ impl From<Flow> for ApiIdentifier {
             | Flow::DecisionEngineDecideGatewayCall
             | Flow::DecisionEngineGatewayFeedbackCall => Self::Routing,
 
+            Flow::CreateSubscription => Self::Subscription,
+
             Flow::RetrieveForexFlow => Self::Forex,
 
             Flow::AddToBlocklist => Self::Blocklist,
diff --git a/crates/router/src/routes/subscription.rs b/crates/router/src/routes/subscription.rs
new file mode 100644
index 00000000000..d4b716f63c0
--- /dev/null
+++ b/crates/router/src/routes/subscription.rs
@@ -0,0 +1,69 @@
+//! Analysis for usage of Subscription in Payment flows
+//!
+//! Functions that are used to perform the api level configuration and retrieval
+//! of various types under Subscriptions.
+
+use actix_web::{web, HttpRequest, HttpResponse, Responder};
+use api_models::subscription as subscription_types;
+use hyperswitch_domain_models::errors;
+use router_env::{
+    tracing::{self, instrument},
+    Flow,
+};
+
+use crate::{
+    core::{api_locking, subscription},
+    headers::X_PROFILE_ID,
+    routes::AppState,
+    services::{api as oss_api, authentication as auth, authorization::permissions::Permission},
+    types::domain,
+};
+
+#[cfg(all(feature = "oltp", feature = "v1"))]
+#[instrument(skip_all)]
+pub async fn create_subscription(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    json_payload: web::Json<subscription_types::CreateSubscriptionRequest>,
+) -> impl Responder {
+    let flow = Flow::CreateSubscription;
+    let profile_id = match req.headers().get(X_PROFILE_ID) {
+        Some(val) => val.to_str().unwrap_or_default().to_string(),
+        None => {
+            return HttpResponse::BadRequest().json(
+                errors::api_error_response::ApiErrorResponse::MissingRequiredField {
+                    field_name: "x-profile-id",
+                },
+            );
+        }
+    };
+    Box::pin(oss_api::server_wrap(
+        flow,
+        state,
+        &req,
+        json_payload.into_inner(),
+        move |state, auth: auth::AuthenticationData, payload, _| {
+            let merchant_context = domain::MerchantContext::NormalMerchant(Box::new(
+                domain::Context(auth.merchant_account, auth.key_store),
+            ));
+            subscription::create_subscription(
+                state,
+                merchant_context,
+                profile_id.clone(),
+                payload.clone(),
+            )
+        },
+        auth::auth_type(
+            &auth::HeaderAuth(auth::ApiKeyAuth {
+                is_connected_allowed: false,
+                is_platform_allowed: false,
+            }),
+            &auth::JWTAuth {
+                permission: Permission::ProfileSubscriptionWrite,
+            },
+            req.headers(),
+        ),
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
diff --git a/crates/router/src/services/authorization/permissions.rs b/crates/router/src/services/authorization/permissions.rs
index 3860d343b68..6fb4b09cd5d 100644
--- a/crates/router/src/services/authorization/permissions.rs
+++ b/crates/router/src/services/authorization/permissions.rs
@@ -43,6 +43,10 @@ generate_permissions! {
             scopes: [Read, Write],
             entities: [Profile, Merchant]
         },
+        Subscription: {
+            scopes: [Read, Write],
+            entities: [Profile, Merchant]
+        },
         ThreeDsDecisionManager: {
             scopes: [Read, Write],
             entities: [Merchant, Profile]
@@ -123,6 +127,7 @@ pub fn get_resource_name(resource: Resource, entity_type: EntityType) -> Option<
             Some("Payment Processors, Payout Processors, Fraud & Risk Managers")
         }
         (Resource::Routing, _) => Some("Routing"),
+        (Resource::Subscription, _) => Some("Subscription"),
         (Resource::RevenueRecovery, _) => Some("Revenue Recovery"),
         (Resource::ThreeDsDecisionManager, _) => Some("3DS Decision Manager"),
         (Resource::SurchargeDecisionManager, _) => Some("Surcharge Decision Manager"),
diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs
index f5359d84010..fad87204cee 100644
--- a/crates/router_env/src/logger/types.rs
+++ b/crates/router_env/src/logger/types.rs
@@ -263,6 +263,8 @@ pub enum Flow {
     RoutingUpdateDefaultConfig,
     /// Routing delete config
     RoutingDeleteConfig,
+    /// Subscription create flow,
+    CreateSubscription,
     /// Create dynamic routing
     CreateDynamicRoutingConfig,
     /// Toggle dynamic routing
diff --git a/migrations/2025-09-17-165505_update_subscription_table_with_merchant_ref_id/down.sql b/migrations/2025-09-17-165505_update_subscription_table_with_merchant_ref_id/down.sql
new file mode 100644
index 00000000000..cc002a5e7e6
--- /dev/null
+++ b/migrations/2025-09-17-165505_update_subscription_table_with_merchant_ref_id/down.sql
@@ -0,0 +1,9 @@
+ALTER TABLE subscription
+    DROP CONSTRAINT subscription_pkey,
+    DROP COLUMN merchant_reference_id;
+
+ALTER TABLE subscription
+    RENAME COLUMN id TO subscription_id;
+
+ALTER TABLE subscription
+    ADD PRIMARY KEY (subscription_id, merchant_id);
diff --git a/migrations/2025-09-17-165505_update_subscription_table_with_merchant_ref_id/up.sql b/migrations/2025-09-17-165505_update_subscription_table_with_merchant_ref_id/up.sql
new file mode 100644
index 00000000000..1d454442b81
--- /dev/null
+++ b/migrations/2025-09-17-165505_update_subscription_table_with_merchant_ref_id/up.sql
@@ -0,0 +1,9 @@
+ALTER TABLE subscription
+    DROP CONSTRAINT subscription_pkey,
+    ADD COLUMN merchant_reference_id VARCHAR(128);
+
+ALTER TABLE subscription
+    RENAME COLUMN subscription_id TO id;
+
+ALTER TABLE subscription
+    ADD PRIMARY KEY (id);

## Summary

Add **Subscriptions API (v1/OLAP)** entrypoint to create a _subscription intent_ and optionally create/attach a customer, returning a `client_secret` and basic subscription payload.

- New route: `POST /subscription/create` (behind `olap` + `v1`)
- Creates a `subscription` row (using existing table) and generates a `client_secret`
- Optionally **gets or creates** a `Customer` from inline `customer` payload or `customer_id`
- Persists optional `mca_id` on the subscription row (used later for connector selection)
- Returns a typed `CreateSubscriptionResponse` with `subscription`, `client_secret`, `merchant_id`, `mca_id`, and optional `customer`, `invoice` (placeholder)


## Endpoints

### Create Subscription Intent

`POST /subscription/create`

**Auth**
- API key (HeaderAuth) + JWT (`Permission::ProfileRoutingWrite`)

**Request Body**
```json
{
    "customer_id": "{{customer_id}}"
}
```

**Behavior**
- Generates `sub_<random>` for subscription row id
- If `customer_id` provided → fetch; else if `customer` has any PII fields → **create customer** (encrypting PII); else → `400 CustomerNotFound`
- Persists subscription with:
  - `id`
  - `subscription_id` = `null` (placeholder, to be set later)
  - `mca_id` (if provided)
  - `client_secret` = generated via `SubscriptionNew::generate_client_secret()`
  - `merchant_id`, `customer_id`
  - `billing_processor`, `payment_method_id`, `metadata` = `null` (placeholders)
- Returns `200` with response below

**Success Response**
```json
{
    "id": "subscription_f8Ng47rKaB296XHolKKG",
    "merchant_reference_id": null,
    "status": "Created",
    "plan_id": null,
    "profile_id": "pro_UjmcM35TVpYh9Zb3LaZV",
    "client_secret": "subscription_f8Ng47rKaB296XHolKKG_secret_0fz5A3aa4NTcdiQF5W7i",
    "merchant_id": "merchant_1758146582",
    "coupon_code": null,
    "customer_id": "cus_jtJ8WHTHnsguskpiorEt"
}
```

---

## Implementation Notes

- **Core**
  - `crates/router/src/core/subscription.rs` – service handler
  - `crates/router/src/core/subscription/utils.rs` – helpers, request/response types
- **Routing**
  - `crates/router/src/routes/subscription.rs` – web handler (`/subscription/create`)
  - `crates/router/src/routes/app.rs` – `Subscription::server()` scope under `/subscription`
  - `crates/router/src/lib.rs` – wire route
- **Domain / DTO**
  - `hyperswitch_domain_models::router_request_types::CustomerDetails` now `Serialize`/`Deserialize`
  - `hyperswitch_domain_models::customer::Customer` derives `Serialize` (v1)
- **Storage**
  - Uses existing `subscription` table and `SubscriptionNew::generate_client_secret()`
  - No new migrations introduced in this PR
- **Customer flow**
  - If not found, creates via encrypted batch operation; respects merchant key store and storage scheme

---

## Testing
```
curl --location 'http://localhost:8080/subscription/create' \
--header 'Content-Type: application/json' \
--header 'X-Profile-Id: pro_UjmcM35TVpYh9Zb3LaZV' \
--header 'api-key: dev_pPBMnOEL5SJnMfxxp2bh4du87ESZoMJNtUmn3oIS8kpCNfUJAZ2a8F832n1je5zQ' \
--data '{
    "customer_id": "cus_jtJ8WHTHnsguskpiorEt"
}'
```

Response:
```
{
    "id": "subscription_f8Ng47rKaB296XHolKKG",
    "merchant_reference_id": null,
    "status": "Created",
    "plan_id": null,
    "profile_id": "pro_UjmcM35TVpYh9Zb3LaZV",
    "client_secret": "subscription_f8Ng47rKaB296XHolKKG_secret_0fz5A3aa4NTcdiQF5W7i",
    "merchant_id": "merchant_1758146582",
    "coupon_code": null,
    "customer_id": "cus_jtJ8WHTHnsguskpiorEt"
}
```
---