Bug: [REFACTOR] add webhook_url for v2 tunnel



add support for create_webhook_url for v2 tunnel flow.
diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs
index 840cdf7a1de..9a7eb4ca8cd 100644
--- a/crates/api_models/src/payments.rs
+++ b/crates/api_models/src/payments.rs
@@ -5933,6 +5933,10 @@ pub struct PaymentsConfirmIntentRequest {
 
     /// If true, returns stringified connector raw response body
     pub return_raw_connector_response: Option<bool>,
+
+    /// The webhook endpoint URL to receive payment status notifications
+    #[schema(value_type = Option<String>, example = "https://merchant.example.com/webhooks/payment")]
+    pub webhook_url: Option<common_utils::types::Url>,
 }
 
 // Serialize is implemented because, this will be serialized in the api events.
@@ -6173,6 +6177,10 @@ pub struct PaymentsRequest {
     /// Allow partial authorization for this payment
     #[schema(value_type = Option<bool>, default = false)]
     pub enable_partial_authorization: Option<primitive_wrappers::EnablePartialAuthorizationBool>,
+
+    /// The webhook endpoint URL to receive payment status notifications
+    #[schema(value_type = Option<String>, example = "https://merchant.example.com/webhooks/payment")]
+    pub webhook_url: Option<common_utils::types::Url>,
 }
 
 #[cfg(feature = "v2")]
@@ -6228,6 +6236,7 @@ impl From<&PaymentsRequest> for PaymentsConfirmIntentRequest {
             merchant_connector_details: request.merchant_connector_details.clone(),
             return_raw_connector_response: request.return_raw_connector_response,
             split_payment_method_data: None,
+            webhook_url: request.webhook_url.clone(),
         }
     }
 }
diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs
index b1f7a4745e1..a01efdf0749 100644
--- a/crates/hyperswitch_domain_models/src/payments.rs
+++ b/crates/hyperswitch_domain_models/src/payments.rs
@@ -944,6 +944,8 @@ where
     pub payment_method: Option<payment_methods::PaymentMethod>,
     pub merchant_connector_details: Option<common_types::domain::MerchantConnectorAuthDetails>,
     pub external_vault_pmd: Option<payment_method_data::ExternalVaultPaymentMethodData>,
+    /// The webhook url of the merchant, to which the connector will send the webhook.
+    pub webhook_url: Option<String>,
 }
 
 #[cfg(feature = "v2")]
diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs
index a7d23edde09..2e1b2d7f999 100644
--- a/crates/router/src/core/payment_methods.rs
+++ b/crates/router/src/core/payment_methods.rs
@@ -3438,6 +3438,7 @@ fn construct_zero_auth_payments_request(
         merchant_connector_details: None,
         return_raw_connector_response: None,
         enable_partial_authorization: None,
+        webhook_url: None,
     })
 }
 
diff --git a/crates/router/src/core/payments/operations/external_vault_proxy_payment_intent.rs b/crates/router/src/core/payments/operations/external_vault_proxy_payment_intent.rs
index 7d40bb81c28..4e4302b1d5a 100644
--- a/crates/router/src/core/payments/operations/external_vault_proxy_payment_intent.rs
+++ b/crates/router/src/core/payments/operations/external_vault_proxy_payment_intent.rs
@@ -298,6 +298,7 @@ impl<F: Send + Clone + Sync> GetTracker<F, PaymentConfirmData<F>, ExternalVaultP
             payment_method: None,
             merchant_connector_details: None,
             external_vault_pmd: payment_method_data,
+            webhook_url: None,
         };
 
         let get_trackers_response = operations::GetTrackerResponse { payment_data };
diff --git a/crates/router/src/core/payments/operations/payment_confirm_intent.rs b/crates/router/src/core/payments/operations/payment_confirm_intent.rs
index 9720cee1d9a..c66ad2299a5 100644
--- a/crates/router/src/core/payments/operations/payment_confirm_intent.rs
+++ b/crates/router/src/core/payments/operations/payment_confirm_intent.rs
@@ -276,6 +276,10 @@ impl<F: Send + Clone + Sync> GetTracker<F, PaymentConfirmData<F>, PaymentsConfir
             payment_method: None,
             merchant_connector_details,
             external_vault_pmd: None,
+            webhook_url: request
+                .webhook_url
+                .as_ref()
+                .map(|url| url.get_string_repr().to_string()),
         };
 
         let get_trackers_response = operations::GetTrackerResponse { payment_data };
diff --git a/crates/router/src/core/payments/operations/proxy_payments_intent.rs b/crates/router/src/core/payments/operations/proxy_payments_intent.rs
index c53aff42b51..ceae5d850f8 100644
--- a/crates/router/src/core/payments/operations/proxy_payments_intent.rs
+++ b/crates/router/src/core/payments/operations/proxy_payments_intent.rs
@@ -269,6 +269,7 @@ impl<F: Send + Clone + Sync> GetTracker<F, PaymentConfirmData<F>, ProxyPaymentsR
             payment_method: None,
             merchant_connector_details: None,
             external_vault_pmd: None,
+            webhook_url: None,
         };
 
         let get_trackers_response = operations::GetTrackerResponse { payment_data };
diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs
index 8467a17e12e..540b6d3df28 100644
--- a/crates/router/src/core/payments/transformers.rs
+++ b/crates/router/src/core/payments/transformers.rs
@@ -333,8 +333,9 @@ pub async fn construct_payment_router_data_for_authorize<'a>(
             &attempt.merchant_id,
             merchant_connector_account.get_id().get_string_repr(),
         )),
-        // TODO: Implement for connectors that require a webhook URL to be included in the request payload.
-        domain::MerchantConnectorAccountTypeDetails::MerchantConnectorDetails(_) => None,
+        domain::MerchantConnectorAccountTypeDetails::MerchantConnectorDetails(_) => {
+            payment_data.webhook_url
+        }
     };
 
     let router_return_url = payment_data
@@ -585,8 +586,9 @@ pub async fn construct_external_vault_proxy_payment_router_data<'a>(
             &attempt.merchant_id,
             merchant_connector_account.get_id().get_string_repr(),
         )),
-        // TODO: Implement for connectors that require a webhook URL to be included in the request payload.
-        domain::MerchantConnectorAccountTypeDetails::MerchantConnectorDetails(_) => None,
+        domain::MerchantConnectorAccountTypeDetails::MerchantConnectorDetails(_) => {
+            payment_data.webhook_url.clone()
+        }
     };
 
     let router_return_url = payment_data
@@ -1417,9 +1419,8 @@ pub async fn construct_payment_router_data_for_setup_mandate<'a>(
             &attempt.merchant_id,
             merchant_connector_account.get_id().get_string_repr(),
         )),
-        // TODO: Implement for connectors that require a webhook URL to be included in the request payload.
         domain::MerchantConnectorAccountTypeDetails::MerchantConnectorDetails(_) => {
-            todo!("Add webhook URL to request for this connector")
+            payment_data.webhook_url
         }
     };
 

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

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

## Description
<!-- Describe your changes in detail -->
This PR adds support for webhook_url in v2 PaymentsRequest. It allows merchants to pass their own webhook URL instead of creating one in Hyperswitch. This change is applicable only to v2 tunnel flow.

### 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)?
-->
Tested Phonepe through UCS
Payment Create
```
curl --location 'http://localhost:8080/v2/payments' \
--header 'Content-Type: application/json' \
--header 'x-profile-id: pro_zjXchnTKtjXfUfQ9FJEL' \
--header 'X-Merchant-Id: cloth_seller_Zoo9KadtslxR8ICC7dB6' \
--header 'x-tenant-id: public' \
--data-raw '{
    "amount_details": {
        "order_amount": 100,
        "currency": "INR"
    },
    "merchant_connector_details": {
        "connector_name": "phonepe",
        "merchant_connector_creds": {
            "auth_type": "SignatureKey",
            "api_key": "_",
            "key1": "_",
            "api_secret": "_"
        }
    },
    "return_url": "https://api-ns2.juspay.in/v2/pay/response/irctc",
    "merchant_reference_id": "phonepeirctc1759229122",
    "capture_method":"automatic",
    "payment_method_data": {
        "upi": {
            "upi_collect": {
                "vpa_id": "success@razorpay"
            }
        },
        "billing": {
            "address": {
                "line1": "1467",
                "line2": "Harrison Street",
                "line3": "Harrison Street",
                "city": "San Fransico",
                "state": "California",
                "zip": "94122",
                "country": "IN",
                "first_name": "Swangi",
                "last_name": "Kumari"
            },
            "phone": {
                "number": "8056594427",
                "country_code": "+91"
            },
            "email": "swangi.kumari@juspay.in"
        }
    },
    "browser_info": {
        "user_agent": "Dalvik/2.1.0",
        "referer": "abcd.com",
        "ip_address": "157.51.3.204"
    },
    "payment_method_subtype": "upi_collect",
    "payment_method_type": "upi",
    "return_raw_connector_response": true,
    "webhook_url": "https://v2/pay/response/irctc"
}'
```
Response
```
{
    "id": "12345_pay_01999962cf7d7e03b33e9ec92da6330c",
    "status": "requires_customer_action",
    "amount": {
        "order_amount": 100,
        "currency": "INR",
        "shipping_cost": null,
        "order_tax_amount": null,
        "external_tax_calculation": "skip",
        "surcharge_calculation": "skip",
        "surcharge_amount": null,
        "tax_on_surcharge": null,
        "net_amount": 100,
        "amount_to_capture": null,
        "amount_capturable": 0,
        "amount_captured": null
    },
    "customer_id": null,
    "connector": "phonepe",
    "created": "2025-09-30T06:50:13.764Z",
    "modified_at": "2025-09-30T06:50:15.068Z",
    "payment_method_data": {
        "billing": {
            "address": {
                "city": "San Fransico",
                "country": "IN",
                "line1": "1467",
                "line2": "Harrison Street",
                "line3": "Harrison Street",
                "zip": "94122",
                "state": "California",
                "first_name": "Swangi",
                "last_name": "Kumari",
                "origin_zip": null
            },
            "phone": {
                "number": "8056594427",
                "country_code": "+91"
            },
            "email": "swangi.kumari@juspay.in"
        }
    },
    "payment_method_type": "upi",
    "payment_method_subtype": "upi_collect",
    "connector_transaction_id": "OMO2509301220148555398840",
    "connector_reference_id": "phonepeirctc1759215013",
    "merchant_connector_id": null,
    "browser_info": null,
    "error": null,
    "shipping": null,
    "billing": null,
    "attempts": null,
    "connector_token_details": null,
    "payment_method_id": null,
    "next_action": null,
    "return_url": "https://api-ns2.juspay.in/v2/pay/response/irctc",
    "authentication_type": null,
    "authentication_type_applied": "no_three_ds",
    "is_iframe_redirection_enabled": null,
    "merchant_reference_id": "phonepeirctc1759215013",
    "raw_connector_response": "{\"success\":true,\"code\":\"PAYMENT_INITIATED\",\"message\":\"Payment initiated\",\"data\":{\"merchantId\":\"JUSPAONLINE\",\"merchantTransactionId\":\"phonepeirctc1759215013\",\"transactionId\":\"OMO2509301220148555398840\",\"instrumentResponse\":{\"type\":\"UPI_COLLECT\"}}}",
    "feature_metadata": null,
    "metadata": null
}
```
Verify webhook url in UCS logs
<img width="1648" height="247" alt="image" src="https://github.com/user-attachments/assets/d6beabd5-e13a-41bd-9188-53994f38ccb3" />


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