Bug: feat(router): Add v2 endpoint to list payment attempts by intent_id



In payment flows that involve retries—such as smart , cascading retires —a single payment intent may result in multiple attempts. Previously, there was no clean way to retrieve all these attempts together.

### API Flow:
Adds a new endpoint: GET /v2/payments/{intent_id}/attempts
Validates the request and ensures merchant authorization
Retrieves all associated payment attempts linked to that intent_id
diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs
index ef55763b0af..ec4c8667f45 100644
--- a/crates/api_models/src/events/payment.rs
+++ b/crates/api_models/src/events/payment.rs
@@ -2,8 +2,8 @@ use common_utils::events::{ApiEventMetric, ApiEventsType};
 
 #[cfg(feature = "v2")]
 use super::{
-    PaymentStartRedirectionRequest, PaymentsCreateIntentRequest, PaymentsGetIntentRequest,
-    PaymentsIntentResponse, PaymentsRequest,
+    PaymentAttemptListRequest, PaymentAttemptListResponse, PaymentStartRedirectionRequest,
+    PaymentsCreateIntentRequest, PaymentsGetIntentRequest, PaymentsIntentResponse, PaymentsRequest,
 };
 #[cfg(feature = "v2")]
 use crate::payment_methods::PaymentMethodListResponseForSession;
@@ -185,6 +185,22 @@ impl ApiEventMetric for PaymentsGetIntentRequest {
     }
 }
 
+#[cfg(feature = "v2")]
+impl ApiEventMetric for PaymentAttemptListRequest {
+    fn get_api_event_type(&self) -> Option<ApiEventsType> {
+        Some(ApiEventsType::Payment {
+            payment_id: self.payment_intent_id.clone(),
+        })
+    }
+}
+
+#[cfg(feature = "v2")]
+impl ApiEventMetric for PaymentAttemptListResponse {
+    fn get_api_event_type(&self) -> Option<ApiEventsType> {
+        None
+    }
+}
+
 #[cfg(feature = "v2")]
 impl ApiEventMetric for PaymentsIntentResponse {
     fn get_api_event_type(&self) -> Option<ApiEventsType> {
diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs
index ca382fa1479..cb5480ed1f4 100644
--- a/crates/api_models/src/payments.rs
+++ b/crates/api_models/src/payments.rs
@@ -290,7 +290,18 @@ pub struct MerchantConnectorDetails {
     }"#)]
     pub merchant_connector_creds: pii::SecretSerdeValue,
 }
+#[cfg(feature = "v2")]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, ToSchema)]
+pub struct PaymentAttemptListRequest {
+    #[schema(value_type = String)]
+    pub payment_intent_id: id_type::GlobalPaymentId,
+}
 
+#[cfg(feature = "v2")]
+#[derive(Debug, serde::Serialize, Clone, ToSchema)]
+pub struct PaymentAttemptListResponse {
+    pub payment_attempt_list: Vec<PaymentAttemptResponse>,
+}
 #[cfg(feature = "v2")]
 impl PaymentsCreateIntentRequest {
     pub fn get_feature_metadata_as_value(
diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs
index eb5c0a467c5..99ce4ca17f3 100644
--- a/crates/hyperswitch_domain_models/src/payments.rs
+++ b/crates/hyperswitch_domain_models/src/payments.rs
@@ -845,6 +845,16 @@ where
     pub connector_customer_id: Option<String>,
 }
 
+#[cfg(feature = "v2")]
+#[derive(Clone)]
+pub struct PaymentAttemptListData<F>
+where
+    F: Clone,
+{
+    pub flow: PhantomData<F>,
+    pub payment_attempt_list: Vec<PaymentAttempt>,
+}
+
 // TODO: Check if this can be merged with existing payment data
 #[cfg(feature = "v2")]
 #[derive(Clone, Debug)]
diff --git a/crates/hyperswitch_domain_models/src/router_flow_types/payments.rs b/crates/hyperswitch_domain_models/src/router_flow_types/payments.rs
index f139249f62a..660c9fae4e2 100644
--- a/crates/hyperswitch_domain_models/src/router_flow_types/payments.rs
+++ b/crates/hyperswitch_domain_models/src/router_flow_types/payments.rs
@@ -76,3 +76,6 @@ pub struct UpdateMetadata;
 
 #[derive(Debug, Clone)]
 pub struct CreateOrder;
+
+#[derive(Debug, Clone)]
+pub struct PaymentGetListAttempts;
diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs
index 822397767cf..43bf1159e7d 100644
--- a/crates/openapi/src/openapi_v2.rs
+++ b/crates/openapi/src/openapi_v2.rs
@@ -432,6 +432,8 @@ Never share your secret api keys. Keep them guarded and secure.
         api_models::payments::PaymentsCreateIntentRequest,
         api_models::payments::PaymentsUpdateIntentRequest,
         api_models::payments::PaymentsIntentResponse,
+        api_models::payments::PaymentAttemptListRequest,
+        api_models::payments::PaymentAttemptListResponse,
         api_models::payments::PazeWalletData,
         api_models::payments::AmountDetails,
         api_models::payments::AmountDetailsUpdate,
diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs
index 854af3f8333..59de54f41fa 100644
--- a/crates/router/src/core/payments.rs
+++ b/crates/router/src/core/payments.rs
@@ -45,7 +45,8 @@ use futures::future::join_all;
 use helpers::{decrypt_paze_token, ApplePayData};
 #[cfg(feature = "v2")]
 use hyperswitch_domain_models::payments::{
-    PaymentCaptureData, PaymentConfirmData, PaymentIntentData, PaymentStatusData,
+    PaymentAttemptListData, PaymentCaptureData, PaymentConfirmData, PaymentIntentData,
+    PaymentStatusData,
 };
 #[cfg(feature = "v2")]
 use hyperswitch_domain_models::router_response_types::RedirectForm;
@@ -1410,6 +1411,83 @@ where
     Ok((payment_data, req, customer))
 }
 
+#[cfg(feature = "v2")]
+#[allow(clippy::too_many_arguments, clippy::type_complexity)]
+#[instrument(skip_all, fields(payment_id, merchant_id))]
+pub async fn payments_attempt_operation_core<F, Req, Op, D>(
+    state: &SessionState,
+    req_state: ReqState,
+    merchant_context: domain::MerchantContext,
+    profile: domain::Profile,
+    operation: Op,
+    req: Req,
+    payment_id: id_type::GlobalPaymentId,
+    header_payload: HeaderPayload,
+) -> RouterResult<(D, Req, Option<domain::Customer>)>
+where
+    F: Send + Clone + Sync,
+    Req: Clone,
+    Op: Operation<F, Req, Data = D> + Send + Sync,
+    D: OperationSessionGetters<F> + Send + Sync + Clone,
+{
+    let operation: BoxedOperation<'_, F, Req, D> = Box::new(operation);
+
+    tracing::Span::current().record(
+        "merchant_id",
+        merchant_context
+            .get_merchant_account()
+            .get_id()
+            .get_string_repr(),
+    );
+
+    let _validate_result = operation
+        .to_validate_request()?
+        .validate_request(&req, &merchant_context)?;
+
+    tracing::Span::current().record("global_payment_id", payment_id.get_string_repr());
+
+    let operations::GetTrackerResponse { mut payment_data } = operation
+        .to_get_tracker()?
+        .get_trackers(
+            state,
+            &payment_id,
+            &req,
+            &merchant_context,
+            &profile,
+            &header_payload,
+        )
+        .await?;
+
+    let (_operation, customer) = operation
+        .to_domain()?
+        .get_customer_details(
+            state,
+            &mut payment_data,
+            merchant_context.get_merchant_key_store(),
+            merchant_context.get_merchant_account().storage_scheme,
+        )
+        .await
+        .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)
+        .attach_printable("Failed while fetching/creating customer")?;
+
+    let (_operation, payment_data) = operation
+        .to_update_tracker()?
+        .update_trackers(
+            state,
+            req_state,
+            payment_data,
+            customer.clone(),
+            merchant_context.get_merchant_account().storage_scheme,
+            None,
+            merchant_context.get_merchant_key_store(),
+            None,
+            header_payload,
+        )
+        .await?;
+
+    Ok((payment_data, req, customer))
+}
+
 #[instrument(skip_all)]
 #[cfg(feature = "v1")]
 pub async fn call_decision_manager<F, D>(
@@ -2089,6 +2167,50 @@ where
     )
 }
 
+#[cfg(feature = "v2")]
+#[allow(clippy::too_many_arguments)]
+pub async fn payments_list_attempts_using_payment_intent_id<F, Res, Req, Op, D>(
+    state: SessionState,
+    req_state: ReqState,
+    merchant_context: domain::MerchantContext,
+    profile: domain::Profile,
+    operation: Op,
+    req: Req,
+    payment_id: id_type::GlobalPaymentId,
+    header_payload: HeaderPayload,
+) -> RouterResponse<Res>
+where
+    F: Send + Clone + Sync,
+    Op: Operation<F, Req, Data = D> + Send + Sync + Clone,
+    Req: Debug + Clone,
+    D: OperationSessionGetters<F> + Send + Sync + Clone,
+    Res: transformers::ToResponse<F, D, Op>,
+{
+    let (payment_data, _req, customer) = payments_attempt_operation_core::<_, _, _, _>(
+        &state,
+        req_state,
+        merchant_context.clone(),
+        profile,
+        operation.clone(),
+        req,
+        payment_id,
+        header_payload.clone(),
+    )
+    .await?;
+
+    Res::generate_response(
+        payment_data,
+        customer,
+        &state.base_url,
+        operation,
+        &state.conf.connector_request_reference_id_config,
+        None,
+        None,
+        header_payload.x_hs_latency,
+        &merchant_context,
+    )
+}
+
 #[cfg(feature = "v2")]
 #[allow(clippy::too_many_arguments)]
 pub async fn payments_get_intent_using_merchant_reference(
@@ -8716,6 +8838,8 @@ impl<F: Clone> PaymentMethodChecker<F> for PaymentData<F> {
 
 pub trait OperationSessionGetters<F> {
     fn get_payment_attempt(&self) -> &storage::PaymentAttempt;
+    #[cfg(feature = "v2")]
+    fn list_payments_attempts(&self) -> &Vec<storage::PaymentAttempt>;
     fn get_payment_intent(&self) -> &storage::PaymentIntent;
     #[cfg(feature = "v2")]
     fn get_client_secret(&self) -> &Option<Secret<String>>;
@@ -9147,6 +9271,10 @@ impl<F: Clone> OperationSessionGetters<F> for PaymentIntentData<F> {
     fn get_payment_attempt(&self) -> &storage::PaymentAttempt {
         todo!()
     }
+    #[cfg(feature = "v2")]
+    fn list_payments_attempts(&self) -> &Vec<storage::PaymentAttempt> {
+        todo!()
+    }
 
     fn get_client_secret(&self) -> &Option<Secret<String>> {
         &self.client_secret
@@ -9429,6 +9557,10 @@ impl<F: Clone> OperationSessionGetters<F> for PaymentConfirmData<F> {
     fn get_payment_attempt(&self) -> &storage::PaymentAttempt {
         &self.payment_attempt
     }
+    #[cfg(feature = "v2")]
+    fn list_payments_attempts(&self) -> &Vec<storage::PaymentAttempt> {
+        todo!()
+    }
     fn get_client_secret(&self) -> &Option<Secret<String>> {
         todo!()
     }
@@ -9715,6 +9847,10 @@ impl<F: Clone> OperationSessionGetters<F> for PaymentStatusData<F> {
     fn get_payment_attempt(&self) -> &storage::PaymentAttempt {
         &self.payment_attempt
     }
+    #[cfg(feature = "v2")]
+    fn list_payments_attempts(&self) -> &Vec<storage::PaymentAttempt> {
+        todo!()
+    }
     fn get_client_secret(&self) -> &Option<Secret<String>> {
         todo!()
     }
@@ -9996,6 +10132,10 @@ impl<F: Clone> OperationSessionGetters<F> for PaymentCaptureData<F> {
     fn get_payment_attempt(&self) -> &storage::PaymentAttempt {
         &self.payment_attempt
     }
+    #[cfg(feature = "v2")]
+    fn list_payments_attempts(&self) -> &Vec<storage::PaymentAttempt> {
+        todo!()
+    }
     fn get_client_secret(&self) -> &Option<Secret<String>> {
         todo!()
     }
@@ -10271,3 +10411,172 @@ impl<F: Clone> OperationSessionSetters<F> for PaymentCaptureData<F> {
         todo!()
     }
 }
+
+#[cfg(feature = "v2")]
+impl<F: Clone> OperationSessionGetters<F> for PaymentAttemptListData<F> {
+    #[track_caller]
+    fn get_payment_attempt(&self) -> &storage::PaymentAttempt {
+        todo!()
+    }
+    #[cfg(feature = "v2")]
+    fn list_payments_attempts(&self) -> &Vec<storage::PaymentAttempt> {
+        &self.payment_attempt_list
+    }
+    fn get_client_secret(&self) -> &Option<Secret<String>> {
+        todo!()
+    }
+    fn get_payment_intent(&self) -> &storage::PaymentIntent {
+        todo!()
+    }
+
+    fn get_payment_method_info(&self) -> Option<&domain::PaymentMethod> {
+        todo!()
+    }
+
+    fn get_mandate_id(&self) -> Option<&payments_api::MandateIds> {
+        todo!()
+    }
+
+    // what is this address find out and not required remove this
+    fn get_address(&self) -> &PaymentAddress {
+        todo!()
+    }
+
+    fn get_creds_identifier(&self) -> Option<&str> {
+        None
+    }
+
+    fn get_token(&self) -> Option<&str> {
+        todo!()
+    }
+
+    fn get_multiple_capture_data(&self) -> Option<&types::MultipleCaptureData> {
+        todo!()
+    }
+
+    fn get_payment_link_data(&self) -> Option<api_models::payments::PaymentLinkResponse> {
+        todo!()
+    }
+
+    fn get_ephemeral_key(&self) -> Option<ephemeral_key::EphemeralKey> {
+        todo!()
+    }
+
+    fn get_setup_mandate(&self) -> Option<&MandateData> {
+        todo!()
+    }
+
+    fn get_poll_config(&self) -> Option<router_types::PollConfig> {
+        todo!()
+    }
+
+    fn get_authentication(
+        &self,
+    ) -> Option<&hyperswitch_domain_models::router_request_types::authentication::AuthenticationStore>
+    {
+        todo!()
+    }
+
+    fn get_frm_message(&self) -> Option<FraudCheck> {
+        todo!()
+    }
+
+    fn get_refunds(&self) -> Vec<storage::Refund> {
+        todo!()
+    }
+
+    fn get_disputes(&self) -> Vec<storage::Dispute> {
+        todo!()
+    }
+
+    fn get_authorizations(&self) -> Vec<diesel_models::authorization::Authorization> {
+        todo!()
+    }
+
+    fn get_attempts(&self) -> Option<Vec<storage::PaymentAttempt>> {
+        todo!()
+    }
+
+    fn get_recurring_details(&self) -> Option<&RecurringDetails> {
+        todo!()
+    }
+
+    fn get_payment_intent_profile_id(&self) -> Option<&id_type::ProfileId> {
+        todo!()
+    }
+
+    fn get_currency(&self) -> storage_enums::Currency {
+        todo!()
+    }
+
+    fn get_amount(&self) -> api::Amount {
+        todo!()
+    }
+
+    fn get_payment_attempt_connector(&self) -> Option<&str> {
+        todo!()
+    }
+
+    fn get_merchant_connector_id_in_attempt(&self) -> Option<id_type::MerchantConnectorAccountId> {
+        todo!()
+    }
+
+    fn get_connector_customer_id(&self) -> Option<String> {
+        todo!()
+    }
+
+    fn get_billing_address(&self) -> Option<hyperswitch_domain_models::address::Address> {
+        todo!()
+    }
+
+    fn get_payment_method_data(&self) -> Option<&domain::PaymentMethodData> {
+        todo!()
+    }
+
+    fn get_sessions_token(&self) -> Vec<api::SessionToken> {
+        todo!()
+    }
+
+    fn get_token_data(&self) -> Option<&storage::PaymentTokenData> {
+        todo!()
+    }
+
+    fn get_mandate_connector(&self) -> Option<&MandateConnectorDetails> {
+        todo!()
+    }
+
+    fn get_force_sync(&self) -> Option<bool> {
+        todo!()
+    }
+
+    fn get_capture_method(&self) -> Option<enums::CaptureMethod> {
+        todo!()
+    }
+
+    #[cfg(feature = "v2")]
+    fn get_optional_payment_attempt(&self) -> Option<&storage::PaymentAttempt> {
+        todo!()
+    }
+
+    fn get_all_keys_required(&self) -> Option<bool> {
+        todo!();
+    }
+
+    fn get_whole_connector_response(&self) -> Option<String> {
+        todo!();
+    }
+    fn get_pre_routing_result(
+        &self,
+    ) -> Option<HashMap<enums::PaymentMethodType, domain::PreRoutingConnectorChoice>> {
+        None
+    }
+    fn get_merchant_connector_details(
+        &self,
+    ) -> Option<api_models::payments::MerchantConnectorDetails> {
+        todo!()
+    }
+
+    fn get_optional_external_vault_session_details(&self) -> Option<api::VaultSessionDetails> {
+        todo!()
+    }
+}
diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs
index 223ab65d0fb..fb42b2016cd 100644
--- a/crates/router/src/core/payments/operations.rs
+++ b/crates/router/src/core/payments/operations.rs
@@ -32,19 +32,20 @@ pub mod payments_incremental_authorization;
 #[cfg(feature = "v1")]
 pub mod tax_calculation;
 
+#[cfg(feature = "v2")]
+pub mod payment_attempt_list;
 #[cfg(feature = "v2")]
 pub mod payment_attempt_record;
 #[cfg(feature = "v2")]
 pub mod payment_confirm_intent;
-#[cfg(feature = "v2")]
-pub mod proxy_payments_intent;
-
 #[cfg(feature = "v2")]
 pub mod payment_create_intent;
 #[cfg(feature = "v2")]
 pub mod payment_get_intent;
 #[cfg(feature = "v2")]
 pub mod payment_update_intent;
+#[cfg(feature = "v2")]
+pub mod proxy_payments_intent;
 
 #[cfg(feature = "v2")]
 pub mod payment_get;
@@ -59,6 +60,8 @@ use async_trait::async_trait;
 use error_stack::{report, ResultExt};
 use router_env::{instrument, tracing};
 
+#[cfg(feature = "v2")]
+pub use self::payment_attempt_list::PaymentGetListAttempts;
 #[cfg(feature = "v2")]
 pub use self::payment_get::PaymentGet;
 #[cfg(feature = "v2")]
diff --git a/crates/router/src/core/payments/operations/payment_attempt_list.rs b/crates/router/src/core/payments/operations/payment_attempt_list.rs
new file mode 100644
index 00000000000..3d651b8c9e9
--- /dev/null
+++ b/crates/router/src/core/payments/operations/payment_attempt_list.rs
@@ -0,0 +1,224 @@
+use std::marker::PhantomData;
+
+#[cfg(feature = "v2")]
+use api_models::{enums::FrmSuggestion, payments::PaymentAttemptListRequest};
+use async_trait::async_trait;
+use common_utils::errors::CustomResult;
+use router_env::{instrument, tracing};
+
+use super::{BoxedOperation, Domain, GetTracker, Operation, UpdateTracker, ValidateRequest};
+use crate::{
+    core::{
+        errors::{self, RouterResult},
+        payments::{self, operations},
+    },
+    db::errors::StorageErrorExt,
+    routes::{app::ReqState, SessionState},
+    types::{
+        api, domain,
+        storage::{self, enums},
+    },
+};
+
+#[derive(Debug, Clone, Copy)]
+pub struct PaymentGetListAttempts;
+
+impl<F: Send + Clone + Sync> Operation<F, PaymentAttemptListRequest> for &PaymentGetListAttempts {
+    type Data = payments::PaymentAttemptListData<F>;
+    fn to_validate_request(
+        &self,
+    ) -> RouterResult<&(dyn ValidateRequest<F, PaymentAttemptListRequest, Self::Data> + Send + Sync)>
+    {
+        Ok(*self)
+    }
+    fn to_get_tracker(
+        &self,
+    ) -> RouterResult<&(dyn GetTracker<F, Self::Data, PaymentAttemptListRequest> + Send + Sync)>
+    {
+        Ok(*self)
+    }
+    fn to_domain(&self) -> RouterResult<&(dyn Domain<F, PaymentAttemptListRequest, Self::Data>)> {
+        Ok(*self)
+    }
+    fn to_update_tracker(
+        &self,
+    ) -> RouterResult<&(dyn UpdateTracker<F, Self::Data, PaymentAttemptListRequest> + Send + Sync)>
+    {
+        Ok(*self)
+    }
+}
+
+impl<F: Send + Clone + Sync> Operation<F, PaymentAttemptListRequest> for PaymentGetListAttempts {
+    type Data = payments::PaymentAttemptListData<F>;
+    fn to_validate_request(
+        &self,
+    ) -> RouterResult<&(dyn ValidateRequest<F, PaymentAttemptListRequest, Self::Data> + Send + Sync)>
+    {
+        Ok(self)
+    }
+    fn to_get_tracker(
+        &self,
+    ) -> RouterResult<&(dyn GetTracker<F, Self::Data, PaymentAttemptListRequest> + Send + Sync)>
+    {
+        Ok(self)
+    }
+    fn to_domain(&self) -> RouterResult<&(dyn Domain<F, PaymentAttemptListRequest, Self::Data>)> {
+        Ok(self)
+    }
+    fn to_update_tracker(
+        &self,
+    ) -> RouterResult<&(dyn UpdateTracker<F, Self::Data, PaymentAttemptListRequest> + Send + Sync)>
+    {
+        Ok(self)
+    }
+}
+
+type PaymentAttemptsListOperation<'b, F> =
+    BoxedOperation<'b, F, PaymentAttemptListRequest, payments::PaymentAttemptListData<F>>;
+
+#[async_trait]
+impl<F: Send + Clone + Sync>
+    GetTracker<F, payments::PaymentAttemptListData<F>, PaymentAttemptListRequest>
+    for PaymentGetListAttempts
+{
+    #[instrument(skip_all)]
+    async fn get_trackers<'a>(
+        &'a self,
+        state: &'a SessionState,
+        _payment_id: &common_utils::id_type::GlobalPaymentId,
+        request: &PaymentAttemptListRequest,
+        merchant_context: &domain::MerchantContext,
+        _profile: &domain::Profile,
+        _header_payload: &hyperswitch_domain_models::payments::HeaderPayload,
+    ) -> RouterResult<operations::GetTrackerResponse<payments::PaymentAttemptListData<F>>> {
+        let db = &*state.store;
+        let key_manager_state = &state.into();
+        let storage_scheme = merchant_context.get_merchant_account().storage_scheme;
+        let payment_attempt_list = db
+            .find_payment_attempts_by_payment_intent_id(
+                key_manager_state,
+                &request.payment_intent_id,
+                merchant_context.get_merchant_key_store(),
+                storage_scheme,
+            )
+            .await
+            .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
+
+        let payment_data = payments::PaymentAttemptListData {
+            flow: PhantomData,
+            payment_attempt_list,
+        };
+
+        let get_trackers_response = operations::GetTrackerResponse { payment_data };
+
+        Ok(get_trackers_response)
+    }
+}
+
+#[async_trait]
+impl<F: Clone + Sync>
+    UpdateTracker<F, payments::PaymentAttemptListData<F>, PaymentAttemptListRequest>
+    for PaymentGetListAttempts
+{
+    #[instrument(skip_all)]
+    async fn update_trackers<'b>(
+        &'b self,
+        _state: &'b SessionState,
+        _req_state: ReqState,
+        payment_data: payments::PaymentAttemptListData<F>,
+        _customer: Option<domain::Customer>,
+        _storage_scheme: enums::MerchantStorageScheme,
+        _updated_customer: Option<storage::CustomerUpdate>,
+        _key_store: &domain::MerchantKeyStore,
+        _frm_suggestion: Option<FrmSuggestion>,
+        _header_payload: hyperswitch_domain_models::payments::HeaderPayload,
+    ) -> RouterResult<(
+        PaymentAttemptsListOperation<'b, F>,
+        payments::PaymentAttemptListData<F>,
+    )>
+    where
+        F: 'b + Send,
+    {
+        Ok((Box::new(self), payment_data))
+    }
+}
+
+impl<F: Send + Clone + Sync>
+    ValidateRequest<F, PaymentAttemptListRequest, payments::PaymentAttemptListData<F>>
+    for PaymentGetListAttempts
+{
+    #[instrument(skip_all)]
+    fn validate_request<'a, 'b>(
+        &'b self,
+        _request: &PaymentAttemptListRequest,
+        merchant_context: &'a domain::MerchantContext,
+    ) -> RouterResult<operations::ValidateResult> {
+        Ok(operations::ValidateResult {
+            merchant_id: merchant_context.get_merchant_account().get_id().to_owned(),
+            storage_scheme: merchant_context.get_merchant_account().storage_scheme,
+            requeue: false,
+        })
+    }
+}
+
+#[async_trait]
+impl<F: Clone + Send + Sync>
+    Domain<F, PaymentAttemptListRequest, payments::PaymentAttemptListData<F>>
+    for PaymentGetListAttempts
+{
+    #[instrument(skip_all)]
+    async fn get_customer_details<'a>(
+        &'a self,
+        _state: &SessionState,
+        _payment_data: &mut payments::PaymentAttemptListData<F>,
+        _merchant_key_store: &domain::MerchantKeyStore,
+        _storage_scheme: enums::MerchantStorageScheme,
+    ) -> CustomResult<
+        (
+            BoxedOperation<'a, F, PaymentAttemptListRequest, payments::PaymentAttemptListData<F>>,
+            Option<domain::Customer>,
+        ),
+        errors::StorageError,
+    > {
+        Ok((Box::new(self), None))
+    }
+
+    #[instrument(skip_all)]
+    async fn make_pm_data<'a>(
+        &'a self,
+        _state: &'a SessionState,
+        _payment_data: &mut payments::PaymentAttemptListData<F>,
+        _storage_scheme: enums::MerchantStorageScheme,
+        _merchant_key_store: &domain::MerchantKeyStore,
+        _customer: &Option<domain::Customer>,
+        _business_profile: &domain::Profile,
+        _should_retry_with_pan: bool,
+    ) -> RouterResult<(
+        PaymentAttemptsListOperation<'a, F>,
+        Option<domain::PaymentMethodData>,
+        Option<String>,
+    )> {
+        Ok((Box::new(self), None, None))
+    }
+
+    #[instrument(skip_all)]
+    async fn perform_routing<'a>(
+        &'a self,
+        _merchant_context: &domain::MerchantContext,
+        _business_profile: &domain::Profile,
+        _state: &SessionState,
+        _payment_data: &mut payments::PaymentAttemptListData<F>,
+    ) -> CustomResult<api::ConnectorCallType, errors::ApiErrorResponse> {
+        Ok(api::ConnectorCallType::Skip)
+    }
+
+    #[instrument(skip_all)]
+    async fn guard_payment_against_blocklist<'a>(
+        &'a self,
+        _state: &SessionState,
+        _merchant_context: &domain::MerchantContext,
+        _payment_data: &mut payments::PaymentAttemptListData<F>,
+    ) -> CustomResult<bool, errors::ApiErrorResponse> {
+        Ok(false)
+    }
+}
diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs
index 4ded67d612e..5d40a603cbb 100644
--- a/crates/router/src/core/payments/transformers.rs
+++ b/crates/router/src/core/payments/transformers.rs
@@ -1826,6 +1826,38 @@ where
     }
 }
 
+#[cfg(feature = "v2")]
+impl<F, Op, D> ToResponse<F, D, Op> for api::PaymentAttemptListResponse
+where
+    F: Clone,
+    Op: Debug,
+    D: OperationSessionGetters<F>,
+{
+    #[allow(clippy::too_many_arguments)]
+    fn generate_response(
+        payment_data: D,
+        _customer: Option<domain::Customer>,
+        _base_url: &str,
+        _operation: Op,
+        _connector_request_reference_id_config: &ConnectorRequestReferenceIdConfig,
+        _connector_http_status_code: Option<u16>,
+        _external_latency: Option<u128>,
+        _is_latency_header_enabled: Option<bool>,
+        _merchant_context: &domain::MerchantContext,
+    ) -> RouterResponse<Self> {
+        Ok(services::ApplicationResponse::JsonWithHeaders((
+            Self {
+                payment_attempt_list: payment_data
+                    .list_payments_attempts()
+                    .iter()
+                    .map(api_models::payments::PaymentAttemptResponse::foreign_from)
+                    .collect(),
+            },
+            vec![],
+        )))
+    }
+}
+
 #[cfg(feature = "v2")]
 impl<F> GenerateResponse<api_models::payments::PaymentsResponse>
     for hyperswitch_domain_models::payments::PaymentConfirmData<F>
diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs
index 020fea804fd..bbb17589162 100644
--- a/crates/router/src/routes/app.rs
+++ b/crates/router/src/routes/app.rs
@@ -639,6 +639,10 @@ impl Payments {
                     web::resource("/confirm-intent")
                         .route(web::post().to(payments::payment_confirm_intent)),
                 )
+                .service(
+                    web::resource("/list_attempts")
+                        .route(web::get().to(payments::list_payment_attempts)),
+                )
                 .service(
                     web::resource("/proxy-confirm-intent")
                         .route(web::post().to(payments::proxy_confirm_intent)),
diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs
index 73f98ebc2a5..039986f1f4a 100644
--- a/crates/router/src/routes/lock_utils.rs
+++ b/crates/router/src/routes/lock_utils.rs
@@ -161,7 +161,8 @@ impl From<Flow> for ApiIdentifier {
             | Flow::PaymentsCreateAndConfirmIntent
             | Flow::PaymentStartRedirection
             | Flow::ProxyConfirmIntent
-            | Flow::PaymentsRetrieveUsingMerchantReferenceId => Self::Payments,
+            | Flow::PaymentsRetrieveUsingMerchantReferenceId
+            | Flow::PaymentAttemptsList => Self::Payments,
 
             Flow::PayoutsCreate
             | Flow::PayoutsRetrieve
diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs
index ae2fcf2cf12..020a1fbf713 100644
--- a/crates/router/src/routes/payments.rs
+++ b/crates/router/src/routes/payments.rs
@@ -247,6 +247,71 @@ pub async fn payments_get_intent(
     .await
 }
 
+#[cfg(feature = "v2")]
+#[instrument(skip_all, fields(flow = ?Flow::PaymentAttemptsList, payment_id,))]
+pub async fn list_payment_attempts(
+    state: web::Data<app::AppState>,
+    req: actix_web::HttpRequest,
+    path: web::Path<common_utils::id_type::GlobalPaymentId>,
+) -> impl Responder {
+    let flow = Flow::PaymentAttemptsList;
+    let payment_intent_id = path.into_inner();
+
+    let payload = api_models::payments::PaymentAttemptListRequest {
+        payment_intent_id: payment_intent_id.clone(),
+    };
+
+    let header_payload = match HeaderPayload::foreign_try_from(req.headers()) {
+        Ok(headers) => headers,
+        Err(err) => {
+            return api::log_and_return_error_response(err);
+        }
+    };
+
+    Box::pin(api::server_wrap(
+        flow,
+        state,
+        &req,
+        payload.clone(),
+        |session_state, auth, req_payload, req_state| {
+            let merchant_context = domain::MerchantContext::NormalMerchant(Box::new(
+                domain::Context(auth.merchant_account, auth.key_store),
+            ));
+
+            payments::payments_list_attempts_using_payment_intent_id::<
+                payments::operations::PaymentGetListAttempts,
+                api_models::payments::PaymentAttemptListResponse,
+                api_models::payments::PaymentAttemptListRequest,
+                payments::operations::payment_attempt_list::PaymentGetListAttempts,
+                hyperswitch_domain_models::payments::PaymentAttemptListData<
+                    payments::operations::PaymentGetListAttempts,
+                >,
+            >(
+                session_state,
+                req_state,
+                merchant_context,
+                auth.profile,
+                payments::operations::PaymentGetListAttempts,
+                payload.clone(),
+                req_payload.payment_intent_id,
+                header_payload.clone(),
+            )
+        },
+        auth::auth_type(
+            &auth::V2ApiKeyAuth {
+                is_connected_allowed: false,
+                is_platform_allowed: false,
+            },
+            &auth::JWTAuth {
+                permission: Permission::ProfilePaymentRead,
+            },
+            req.headers(),
+        ),
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
+
 #[cfg(feature = "v2")]
 #[instrument(skip_all, fields(flow = ?Flow::PaymentsCreateAndConfirmIntent, payment_id))]
 pub async fn payments_create_and_confirm_intent(
diff --git a/crates/router/src/types/api/payments.rs b/crates/router/src/types/api/payments.rs
index 7728945fe62..725a821eba1 100644
--- a/crates/router/src/types/api/payments.rs
+++ b/crates/router/src/types/api/payments.rs
@@ -1,11 +1,11 @@
-#[cfg(feature = "v1")]
+#[cfg(feature = "v2")]
 pub use api_models::payments::{
-    PaymentListFilterConstraints, PaymentListResponse, PaymentListResponseV2,
+    PaymentAttemptListRequest, PaymentAttemptListResponse, PaymentsConfirmIntentRequest,
+    PaymentsCreateIntentRequest, PaymentsIntentResponse, PaymentsUpdateIntentRequest,
 };
-#[cfg(feature = "v2")]
+#[cfg(feature = "v1")]
 pub use api_models::payments::{
-    PaymentsConfirmIntentRequest, PaymentsCreateIntentRequest, PaymentsIntentResponse,
-    PaymentsUpdateIntentRequest,
+    PaymentListFilterConstraints, PaymentListResponse, PaymentListResponseV2,
 };
 pub use api_models::{
     feature_matrix::{
diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs
index e353d2b28f4..cf176e1562e 100644
--- a/crates/router_env/src/logger/types.rs
+++ b/crates/router_env/src/logger/types.rs
@@ -180,6 +180,8 @@ pub enum Flow {
     PaymentsConfirmIntent,
     /// Payments create and confirm intent flow
     PaymentsCreateAndConfirmIntent,
+    /// Payment attempt list flow
+    PaymentAttemptsList,
     #[cfg(feature = "payouts")]
     /// Payouts create flow
     PayoutsCreate,

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

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

## Description
<!-- Describe your changes in detail -->
This PR introduces an API to list all payment attempts associated with a given intent_id.
#### Why this is needed:
In payment flows that involve retries—such as smart , cascading retires —a single payment intent may result in multiple attempts. Previously, there was no clean way to retrieve all these attempts together.
#### Implemented API Flow:
- Adds a new endpoint: GET /v2/payments/{intent_id}/list_attempts
- Validates the request and ensures merchant authorization
- Retrieves all associated payment attempts linked to that intent_id

### Additional Changes

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

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

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


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

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


## How did you test it?
<!--
Did you write an integration/unit/API test to verify the code changes?
Or did you test this change manually (provide relevant screenshots)?
-->
I inserted two payment attempts for a payment intent as part of testing.
### Curl 
```
curl --location 'http://localhost:8080/v2/payments/12345_pay_01978809316e7850b05ab92288ed7746/list_attempts' \
--header 'Content-Type: application/json' \
--header 'x-profile-id: pro_pF6sS2TBfhg0Vdp6N04E' \
--header 'api-key: dev_bzJEoDI7SlTyV0M7Vq6VdtCtbKGwOPphw39wD1mYdu8rY3GoydtnymDbkkMxDopB' \
--header 'Authorization: api-key=dev_bzJEoDI7SlTyV0M7Vq6VdtCtbKGwOPphw39wD1mYdu8rY3GoydtnymDbkkMxDopB'
```
### Response
```
{
    "payment_attempts": [
        {
            "id": "12345_att_01975ae7bf32760282f479794fbf810c",
            "status": "failure",
            "amount": {
                "net_amount": 10000,
                "amount_to_capture": null,
                "surcharge_amount": null,
                "tax_on_surcharge": null,
                "amount_capturable": 10000,
                "shipping_cost": null,
                "order_tax_amount": null
            },
            "connector": "stripe",
            "error": null,
            "authentication_type": "no_three_ds",
            "created_at": "2025-06-17T13:50:49.173Z",
            "modified_at": "2025-06-17T13:50:49.173Z",
            "cancellation_reason": null,
            "payment_token": null,
            "connector_metadata": null,
            "payment_experience": null,
            "payment_method_type": "card",
            "connector_reference_id": null,
            "payment_method_subtype": "credit",
            "connector_payment_id": {
                "TxnId": "ch_3RYW3N06IkU6uKNZ01mrX2tD"
            },
            "payment_method_id": null,
            "client_source": null,
            "client_version": null,
            "feature_metadata": {
                "revenue_recovery": {
                    "attempt_triggered_by": "external"
                }
            }
        },
        {
            "id": "12345_att_01975ae7bf32760282f479794fbf810d",
            "status": "failure",
            "amount": {
                "net_amount": 10000,
                "amount_to_capture": null,
                "surcharge_amount": null,
                "tax_on_surcharge": null,
                "amount_capturable": 10000,
                "shipping_cost": null,
                "order_tax_amount": null
            },
            "connector": "stripe",
            "error": null,
            "authentication_type": "no_three_ds",
            "created_at": "2025-06-17T13:51:16.812Z",
            "modified_at": "2025-06-17T13:51:16.812Z",
            "cancellation_reason": null,
            "payment_token": null,
            "connector_metadata": null,
            "payment_experience": null,
            "payment_method_type": "card",
            "connector_reference_id": null,
            "payment_method_subtype": "credit",
            "connector_payment_id": {
                "TxnId": "ch_3RYW3N06IkU6uKNZ01mrX2tD"
            },
            "payment_method_id": null,
            "client_source": null,
            "client_version": null,
            "feature_metadata": {
                "revenue_recovery": {
                    "attempt_triggered_by": "external"
                }
            }
        }
    ]
}
```

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