Bug: [FIX]: Make v2 endpoints follow standard API conventions



There are a lot of minor inconsistencies in v2 endpoints. Fix them in accordance with https://google.aip.dev/general
diff --git a/crates/hyperswitch_connectors/src/connectors/hyperswitch_vault.rs b/crates/hyperswitch_connectors/src/connectors/hyperswitch_vault.rs
index d7e0e93c592..e6007d1e249 100644
--- a/crates/hyperswitch_connectors/src/connectors/hyperswitch_vault.rs
+++ b/crates/hyperswitch_connectors/src/connectors/hyperswitch_vault.rs
@@ -85,7 +85,7 @@ impl ConnectorIntegration<ExternalVaultCreateFlow, VaultRequestData, VaultRespon
         connectors: &Connectors,
     ) -> CustomResult<String, errors::ConnectorError> {
         Ok(format!(
-            "{}/v2/payment-methods-session",
+            "{}/v2/payment-method-sessions",
             self.base_url(connectors)
         ))
     }
diff --git a/crates/openapi/src/routes/organization.rs b/crates/openapi/src/routes/organization.rs
index d677131d5db..832b203729d 100644
--- a/crates/openapi/src/routes/organization.rs
+++ b/crates/openapi/src/routes/organization.rs
@@ -77,7 +77,7 @@ pub async fn organization_update() {}
 /// Create a new organization
 #[utoipa::path(
     post,
-    path = "/v2/organization",
+    path = "/v2/organizations",
     request_body(
         content = OrganizationCreateRequest,
         examples(
@@ -104,7 +104,7 @@ pub async fn organization_create() {}
 /// Retrieve an existing organization
 #[utoipa::path(
     get,
-    path = "/v2/organization/{id}",
+    path = "/v2/organizations/{id}",
     params (("id" = String, Path, description = "The unique identifier for the Organization")),
     responses(
         (status = 200, description = "Organization Created", body =OrganizationResponse),
@@ -122,7 +122,7 @@ pub async fn organization_retrieve() {}
 /// Create a new organization for .
 #[utoipa::path(
     put,
-    path = "/v2/organization/{id}",
+    path = "/v2/organizations/{id}",
     request_body(
         content = OrganizationUpdateRequest,
         examples(
@@ -150,7 +150,7 @@ pub async fn organization_update() {}
 /// List merchant accounts for an Organization
 #[utoipa::path(
     get,
-    path = "/v2/organization/{id}/merchant-accounts",
+    path = "/v2/organizations/{id}/merchant-accounts",
     params (("id" = String, Path, description = "The unique identifier for the Organization")),
     responses(
         (status = 200, description = "Merchant Account list retrieved successfully", body = Vec<MerchantAccountResponse>),
diff --git a/crates/openapi/src/routes/payment_method.rs b/crates/openapi/src/routes/payment_method.rs
index 0cfcde2cb7e..3f0ed7d799e 100644
--- a/crates/openapi/src/routes/payment_method.rs
+++ b/crates/openapi/src/routes/payment_method.rs
@@ -353,7 +353,7 @@ pub async fn list_customer_payment_method_api() {}
 #[cfg(feature = "v2")]
 #[utoipa::path(
     post,
-    path = "/v2/payment-method-session",
+    path = "/v2/payment-method-sessions",
     request_body(
     content = PaymentMethodSessionRequest,
         examples  (( "Create a payment method session with customer_id" = (
@@ -378,7 +378,7 @@ pub fn payment_method_session_create() {}
 #[cfg(feature = "v2")]
 #[utoipa::path(
     get,
-    path = "/v2/payment-method-session/{id}",
+    path = "/v2/payment-method-sessions/{id}",
     params (
         ("id" = String, Path, description = "The unique identifier for the Payment Method Session"),
     ),
@@ -399,7 +399,7 @@ pub fn payment_method_session_retrieve() {}
 #[cfg(feature = "v2")]
 #[utoipa::path(
     get,
-    path = "/v2/payment-method-session/{id}/list-payment-methods",
+    path = "/v2/payment-method-sessions/{id}/list-payment-methods",
     params (
         ("id" = String, Path, description = "The unique identifier for the Payment Method Session"),
     ),
@@ -419,7 +419,7 @@ pub fn payment_method_session_list_payment_methods() {}
 #[cfg(feature = "v2")]
 #[utoipa::path(
     put,
-    path = "/v2/payment-method-session/{id}/update-saved-payment-method",
+    path = "/v2/payment-method-sessions/{id}/update-saved-payment-method",
     params (
         ("id" = String, Path, description = "The unique identifier for the Payment Method Session"),
     ),
@@ -453,7 +453,7 @@ pub fn payment_method_session_update_saved_payment_method() {}
 #[cfg(feature = "v2")]
 #[utoipa::path(
     delete,
-    path = "/v2/payment-method-session/{id}",
+    path = "/v2/payment-method-sessions/{id}",
     params (
         ("id" = String, Path, description = "The unique identifier for the Payment Method Session"),
     ),
@@ -520,7 +520,7 @@ pub async fn tokenize_card_using_pm_api() {}
 /// **Confirms a payment method session object with the payment method data**
 #[utoipa::path(
   post,
-  path = "/v2/payment-method-session/{id}/confirm",
+  path = "/v2/payment-method-sessions/{id}/confirm",
   params (("id" = String, Path, description = "The unique identifier for the Payment Method Session"),
       (
         "X-Profile-Id" = String, Header,
diff --git a/crates/openapi/src/routes/refunds.rs b/crates/openapi/src/routes/refunds.rs
index f33369d3888..be8c63e4696 100644
--- a/crates/openapi/src/routes/refunds.rs
+++ b/crates/openapi/src/routes/refunds.rs
@@ -221,7 +221,7 @@ pub async fn refunds_create() {}
 /// Updates the properties of a Refund object. This API can be used to attach a reason for the refund or metadata fields
 #[utoipa::path(
     put,
-    path = "/v2/refunds/{id}/update_metadata",
+    path = "/v2/refunds/{id}/update-metadata",
     params(
         ("id" = String, Path, description = "The identifier for refund")
     ),
diff --git a/crates/openapi/src/routes/revenue_recovery.rs b/crates/openapi/src/routes/revenue_recovery.rs
index 8e3b2aaf412..0f22b340bd5 100644
--- a/crates/openapi/src/routes/revenue_recovery.rs
+++ b/crates/openapi/src/routes/revenue_recovery.rs
@@ -4,7 +4,7 @@
 /// Retrieve the Revenue Recovery Payment Info
 #[utoipa::path(
     get,
-    path = "/v2/process_tracker/revenue_recovery_workflow/{revenue_recovery_id}",
+    path = "/v2/process-trackers/revenue-recovery-workflow/{revenue_recovery_id}",
     params(
         ("recovery_recovery_id" = String, Path, description = "The payment intent id"),
     ),
diff --git a/crates/openapi/src/routes/routing.rs b/crates/openapi/src/routes/routing.rs
index f657915e6a5..78f4c8b187c 100644
--- a/crates/openapi/src/routes/routing.rs
+++ b/crates/openapi/src/routes/routing.rs
@@ -26,7 +26,7 @@ pub async fn routing_create_config() {}
 /// Create a routing algorithm
 #[utoipa::path(
     post,
-    path = "/v2/routing-algorithm",
+    path = "/v2/routing-algorithms",
     request_body = RoutingConfigRequest,
     responses(
         (status = 200, description = "Routing Algorithm created", body = RoutingDictionaryRecord),
@@ -92,7 +92,7 @@ pub async fn routing_retrieve_config() {}
 /// Retrieve a routing algorithm with its algorithm id
 #[utoipa::path(
     get,
-    path = "/v2/routing-algorithm/{id}",
+    path = "/v2/routing-algorithms/{id}",
     params(
         ("id" = String, Path, description = "The unique identifier for a routing algorithm"),
     ),
diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs
index e002f7fd050..01676a11d49 100644
--- a/crates/router/src/lib.rs
+++ b/crates/router/src/lib.rs
@@ -210,7 +210,10 @@ pub fn mk_app(
 
         #[cfg(feature = "v2")]
         {
-            server_app = server_app.service(routes::ProcessTracker::server(state.clone()));
+            server_app = server_app
+                .service(routes::UserDeprecated::server(state.clone()))
+                .service(routes::ProcessTrackerDeprecated::server(state.clone()))
+                .service(routes::ProcessTracker::server(state.clone()));
         }
     }
 
diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs
index fcf7270fa8a..b8fde79e2a1 100644
--- a/crates/router/src/routes.rs
+++ b/crates/router/src/routes.rs
@@ -91,8 +91,9 @@ pub use self::app::{
     ApiKeys, AppState, ApplePayCertificatesMigration, Authentication, Cache, Cards, Chat, Configs,
     ConnectorOnboarding, Customers, Disputes, EphemeralKey, FeatureMatrix, Files, Forex, Gsm,
     Health, Hypersense, Mandates, MerchantAccount, MerchantConnectorAccount, PaymentLink,
-    PaymentMethods, Payments, Poll, ProcessTracker, Profile, ProfileAcquirer, ProfileNew, Refunds,
-    Relay, RelayWebhooks, SessionState, ThreeDsDecisionRule, User, Webhooks,
+    PaymentMethods, Payments, Poll, ProcessTracker, ProcessTrackerDeprecated, Profile,
+    ProfileAcquirer, ProfileNew, Refunds, Relay, RelayWebhooks, SessionState, ThreeDsDecisionRule,
+    User, UserDeprecated, Webhooks,
 };
 #[cfg(feature = "olap")]
 pub use self::app::{Blocklist, Organization, Routing, Verify, WebhookEvents};
diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs
index cd132f0da63..d6d10dfe195 100644
--- a/crates/router/src/routes/app.rs
+++ b/crates/router/src/routes/app.rs
@@ -657,10 +657,15 @@ impl Payments {
                     web::resource("/confirm-intent")
                         .route(web::post().to(payments::payment_confirm_intent)),
                 )
+                // TODO: Deprecated. Remove this in favour of /list-attempts
                 .service(
                     web::resource("/list_attempts")
                         .route(web::get().to(payments::list_payment_attempts)),
                 )
+                .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)),
@@ -882,7 +887,7 @@ pub struct Routing;
 #[cfg(all(feature = "olap", feature = "v2"))]
 impl Routing {
     pub fn server(state: AppState) -> Scope {
-        web::scope("/v2/routing-algorithm")
+        web::scope("/v2/routing-algorithms")
             .app_data(web::Data::new(state.clone()))
             .service(
                 web::resource("").route(web::post().to(|state, req, payload| {
@@ -1242,7 +1247,7 @@ impl Refunds {
                         .route(web::post().to(refunds::refunds_retrieve_with_gateway_creds)),
                 )
                 .service(
-                    web::resource("/{id}/update_metadata")
+                    web::resource("/{id}/update-metadata")
                         .route(web::put().to(refunds::refunds_metadata_update)),
                 );
         }
@@ -1412,7 +1417,7 @@ pub struct PaymentMethodSession;
 #[cfg(all(feature = "v2", feature = "oltp"))]
 impl PaymentMethodSession {
     pub fn server(state: AppState) -> Scope {
-        let mut route = web::scope("/v2/payment-methods-session").app_data(web::Data::new(state));
+        let mut route = web::scope("/v2/payment-method-sessions").app_data(web::Data::new(state));
         route = route.service(
             web::resource("")
                 .route(web::post().to(payment_methods::payment_methods_session_create)),
@@ -1545,7 +1550,7 @@ impl Organization {
 #[cfg(all(feature = "v2", feature = "olap"))]
 impl Organization {
     pub fn server(state: AppState) -> Scope {
-        web::scope("/v2/organization")
+        web::scope("/v2/organizations")
             .app_data(web::Data::new(state))
             .service(web::resource("").route(web::post().to(admin::organization_create)))
             .service(
@@ -2269,11 +2274,12 @@ impl Verify {
     }
 }
 
-pub struct User;
+pub struct UserDeprecated;
 
 #[cfg(all(feature = "olap", feature = "v2"))]
-impl User {
+impl UserDeprecated {
     pub fn server(state: AppState) -> Scope {
+        // TODO: Deprecated. Remove this in favour of /v2/users
         let mut route = web::scope("/v2/user").app_data(web::Data::new(state));
 
         route = route.service(
@@ -2314,6 +2320,51 @@ impl User {
     }
 }
 
+pub struct User;
+
+#[cfg(all(feature = "olap", feature = "v2"))]
+impl User {
+    pub fn server(state: AppState) -> Scope {
+        let mut route = web::scope("/v2/users").app_data(web::Data::new(state));
+
+        route = route.service(
+            web::resource("/create-merchant")
+                .route(web::post().to(user::user_merchant_account_create)),
+        );
+        route = route.service(
+            web::scope("/list")
+                .service(
+                    web::resource("/merchant")
+                        .route(web::get().to(user::list_merchants_for_user_in_org)),
+                )
+                .service(
+                    web::resource("/profile")
+                        .route(web::get().to(user::list_profiles_for_user_in_org_and_merchant)),
+                ),
+        );
+
+        route = route.service(
+            web::scope("/switch")
+                .service(
+                    web::resource("/merchant")
+                        .route(web::post().to(user::switch_merchant_for_user_in_org)),
+                )
+                .service(
+                    web::resource("/profile")
+                        .route(web::post().to(user::switch_profile_for_user_in_org_and_merchant)),
+                ),
+        );
+
+        route = route.service(
+            web::resource("/data")
+                .route(web::get().to(user::get_multiple_dashboard_metadata))
+                .route(web::post().to(user::set_dashboard_metadata)),
+        );
+
+        route
+    }
+}
+
 #[cfg(all(feature = "olap", feature = "v1"))]
 impl User {
     pub fn server(state: AppState) -> Scope {
@@ -2683,6 +2734,23 @@ impl FeatureMatrix {
     }
 }
 
+#[cfg(feature = "olap")]
+pub struct ProcessTrackerDeprecated;
+
+#[cfg(all(feature = "olap", feature = "v2"))]
+impl ProcessTrackerDeprecated {
+    pub fn server(state: AppState) -> Scope {
+        use super::process_tracker::revenue_recovery;
+        // TODO: Deprecated. Remove this in favour of /v2/process-trackers
+        web::scope("/v2/process_tracker/revenue_recovery_workflow")
+            .app_data(web::Data::new(state.clone()))
+            .service(
+                web::resource("/{revenue_recovery_id}")
+                    .route(web::get().to(revenue_recovery::revenue_recovery_pt_retrieve_api)),
+            )
+    }
+}
+
 #[cfg(feature = "olap")]
 pub struct ProcessTracker;
 
@@ -2690,7 +2758,7 @@ pub struct ProcessTracker;
 impl ProcessTracker {
     pub fn server(state: AppState) -> Scope {
         use super::process_tracker::revenue_recovery;
-        web::scope("/v2/process_tracker/revenue_recovery_workflow")
+        web::scope("/v2/process-trackers/revenue-recovery-workflow")
             .app_data(web::Data::new(state.clone()))
             .service(
                 web::resource("/{revenue_recovery_id}")
diff --git a/cypress-tests-v2/cypress/support/commands.js b/cypress-tests-v2/cypress/support/commands.js
index 54e52495d67..e77a0f65143 100644
--- a/cypress-tests-v2/cypress/support/commands.js
+++ b/cypress-tests-v2/cypress/support/commands.js
@@ -45,7 +45,7 @@ Cypress.Commands.add(
     // Define the necessary variables and constants
     const api_key = globalState.get("adminApiKey");
     const base_url = globalState.get("baseUrl");
-    const url = `${base_url}/v2/organization`;
+    const url = `${base_url}/v2/organizations`;
 
     // Update request body
     organizationCreateBody.organization_name += " " + nanoid();
@@ -84,7 +84,7 @@ Cypress.Commands.add("organizationRetrieveCall", (globalState) => {
   const api_key = globalState.get("adminApiKey");
   const base_url = globalState.get("baseUrl");
   const organization_id = globalState.get("organizationId");
-  const url = `${base_url}/v2/organization/${organization_id}`;
+  const url = `${base_url}/v2/organizations/${organization_id}`;
 
   cy.request({
     method: "GET",
@@ -125,7 +125,7 @@ Cypress.Commands.add(
     const api_key = globalState.get("adminApiKey");
     const base_url = globalState.get("baseUrl");
     const organization_id = globalState.get("organizationId");
-    const url = `${base_url}/v2/organization/${organization_id}`;
+    const url = `${base_url}/v2/organizations/${organization_id}`;
 
     // Update request body
     organizationUpdateBody.organization_name += " " + nanoid();
@@ -805,7 +805,7 @@ Cypress.Commands.add(
     const api_key = globalState.get("userInfoToken");
     const base_url = globalState.get("baseUrl");
     const profile_id = globalState.get("profileId");
-    const url = `${base_url}/v2/routing-algorithm`;
+    const url = `${base_url}/v2/routing-algorithms`;
 
     // Update request body
     routingSetupBody.algorithm.data = payload.data;
@@ -961,7 +961,7 @@ Cypress.Commands.add("routingRetrieveCall", (globalState) => {
   const base_url = globalState.get("baseUrl");
   const profile_id = globalState.get("profileId");
   const routing_algorithm_id = globalState.get("routingAlgorithmId");
-  const url = `${base_url}/v2/routing-algorithm/${routing_algorithm_id}`;
+  const url = `${base_url}/v2/routing-algorithms/${routing_algorithm_id}`;
 
   cy.request({
     method: "GET",
@@ -1170,7 +1170,7 @@ Cypress.Commands.add("merchantAccountsListCall", (globalState) => {
   const key_id_type = "publishable_key";
   const key_id = validateEnv(base_url, key_id_type);
   const organization_id = globalState.get("organizationId");
-  const url = `${base_url}/v2/organization/${organization_id}/merchant-accounts`;
+  const url = `${base_url}/v2/organizations/${organization_id}/merchant-accounts`;
 
   cy.request({
     method: "GET",

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

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

## Description
<!-- Describe your changes in detail -->
- Consistently use kebab case in endpoint URLs
- Use plural form of resources in URLs, i.e. instead of `/v2/organization`, use `/v2/organizations`

Renamed (current version removed) endpoints:
`/v2/organization` -> `/v2/organizations`
`/v2/routing-algorithm` -> `/v2/routing-algorithms`
`/v2/payment-methods-session` -> `/v2/payment-method-sessions`
(refunds) `/update_metadata` -> `update-metadata`

Duplicated (current version not removed) endpoints:

`/v2/user` -> `/v2/users`
`/create_merchant` -> `/create-merchant`
`/v2/process_tracker/revenue_recovery_workflow` -> `/v2/process-trackers/revenue-recovery-workflow`
`/list_attempts` -> `/list-attempts`

### 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).
-->
Closes #8629 

## How did you test it?
<!--
Did you write an integration/unit/API test to verify the code changes?
Or did you test this change manually (provide relevant screenshots)?
-->

1. Organization

Request:
```
curl --location 'http://localhost:8080/v2/organizations' \
--header 'Authorization: admin-api-key=test_admin' \
--header 'Content-Type: application/json' \
--data '{
    "organization_name": "v2_org_1752571446"
}'
```

Response:

```json
{
    "id": "org_4YoqpRWuOelFCr7Lbh2l",
    "organization_name": "v2_org_1752571419",
    "organization_details": null,
    "metadata": null,
    "modified_at": "2025-07-15 09:23:39.045598",
    "created_at": "2025-07-15 09:23:39.045496"
}
```

2. Payment Method Sessions:

Request:
```
curl --location 'http://localhost:8080/v2/payment-method-sessions' \
--header 'x-profile-id: pro_k5m9OvIbAwRr29ZJrZFN' \
--header 'Authorization: api-key=dev_e0A7pdYme3jeCbLscKnTIXBci4IcuiixFCwaN0H3usrmeVSkKcn5r701nisZsvZc' \
--header 'Content-Type: application/json' \
--header 'api-key: dev_e0A7pdYme3jeCbLscKnTIXBci4IcuiixFCwaN0H3usrmeVSkKcn5r701nisZsvZc' \
--data-raw '{
    "customer_id": "12345_cus_01980d66d85c78f28c2f25980c48a60b",
     "billing": {
        "address": {
            "first_name": "John",
            "last_name": "Dough"
        },
        "email": "example@example.com"
    }
}'
```

Response:

```json
{
    "id": "12345_pms_01980d66e6ab715093173e3078818fc5",
    "customer_id": "12345_cus_01980d66d85c78f28c2f25980c48a60b",
    "billing": {
        "address": {
            "city": null,
            "country": null,
            "line1": null,
            "line2": null,
            "line3": null,
            "zip": null,
            "state": null,
            "first_name": "John",
            "last_name": "Dough"
        },
        "phone": null,
        "email": "example@example.com"
    },
    "psp_tokenization": null,
    "network_tokenization": null,
    "tokenization_data": null,
    "expires_at": "2025-07-15T09:40:04.299Z",
    "client_secret": "cs_01980d66e6ab715093173e43f85db63a",
    "return_url": null,
    "next_action": null,
    "authentication_details": null,
    "associated_payment_methods": null,
    "associated_token_id": null
}
```

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