> ## Documentation Index
> Fetch the complete documentation index at: https://developers.ligdicash.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Redirect the customer to the payment page

> How to open the LigdiCash payment link: same tab, new tab, popup, or native WebView. Constraints and recommended patterns.

After the invoice is created, the `response_text` field of the response contains the URL of the LigdiCash payment page. This is the URL you must open so the customer can choose their operator and complete the payment. The opening mode depends on your context: web application, native mobile, or hybrid.

## Iframes are blocked

<Warning>
  LigdiCash blocks the loading of its payment page in an `<iframe>`. Never embed the URL in an iframe — the page will not display.

  Always open the link in a real navigation context: same tab, new tab, popup, or native WebView.
</Warning>

This block is enforced server-side through the `X-Frame-Options` and `Content-Security-Policy` headers. It is not a bug — it is a deliberate security measure against clickjacking.

## The 4 opening modes

| Mode           | Recommended context                          | Implementation                   |
| -------------- | -------------------------------------------- | -------------------------------- |
| Same tab       | Web — linear flow with no state to keep      | `window.location.href = url`     |
| New tab        | Web — user keeps the current page open       | `window.open(url, '_blank')`     |
| Popup          | Web — modal UX, automatic return to the page | Anti-blocker pattern (see below) |
| Native WebView | iOS / Android — native mobile application    | Detect return URLs to close      |

## Popup pattern (web)

Triggering a `window.open()` after a network request is systematically blocked by modern browsers: it is not considered a direct user action.

**The two-step solution:**

1. Open `about:blank` **synchronously** on click, before any `await`
2. Navigate to the payment URL **after** receiving the API response

```javascript JavaScript theme={null}
async function startPayment(order) {
  // Step 1 — open the popup immediately on click (synchronous)
  const popup = window.open("about:blank", "ligdicash-payment", "width=500,height=700");

  try {
    // Step 2 — create the invoice
    const response = await fetch(
      "https://app.ligdicash.com/pay/v01/redirect/checkout-invoice/create",
      {
        method: "POST",
        headers: {
          Apikey: process.env.LIGDICASH_API_KEY,
          Authorization: `Bearer ${process.env.LIGDICASH_API_TOKEN}`,
          Accept: "application/json",
          "Content-Type": "application/json",
        },
        body: JSON.stringify(order),
      }
    );

    const data = await response.json();

    if (data.response_code === "00") {
      // Step 3 — navigate within the already-opened popup
      popup.location.href = data.response_text;
    } else {
      popup.close();
      console.error("Invoice creation failed:", data.response_text);
    }
  } catch (err) {
    popup.close();
    throw err;
  }
}
```

<Tip>
  This pattern works on all modern browsers (Chrome, Firefox, Safari, Edge). The key is that `window.open()` must be called in the same synchronous execution flow as the click handler.
</Tip>

## WebView pattern (native mobile)

On iOS and Android, open the payment URL in a **native WebView** rather than in the system browser. This lets you detect the end of the flow and close the WebView automatically.

**Principle:** intercept every navigation in the WebView and compare the URL with your `return_url` and `cancel_url`.

<CodeGroup>
  ```jsx React Native theme={null}
  import { WebView } from "react-native-webview";

  export function PaymentWebView({ paymentUrl, onSuccess, onCancel }) {
    const handleNavigationChange = (navState) => {
      const { url } = navState;

      if (url.startsWith("https://myapp.com/payment/success")) {
        onSuccess(); // always confirm server-side afterwards
      } else if (url.startsWith("https://myapp.com/payment/cancel")) {
        onCancel();
      }
    };

    return (
      <WebView
        source={{ uri: paymentUrl }}
        onNavigationStateChange={handleNavigationChange}
      />
    );
  }
  ```

  ```dart Flutter theme={null}
  import 'package:webview_flutter/webview_flutter.dart';

  class PaymentWebView extends StatefulWidget {
    final String paymentUrl;
    final VoidCallback onSuccess;
    final VoidCallback onCancel;

    const PaymentWebView({
      required this.paymentUrl,
      required this.onSuccess,
      required this.onCancel,
      super.key,
    });

    @override
    State<PaymentWebView> createState() => _PaymentWebViewState();
  }

  class _PaymentWebViewState extends State<PaymentWebView> {
    late final WebViewController _controller;

    @override
    void initState() {
      super.initState();
      _controller = WebViewController()
        ..setNavigationDelegate(NavigationDelegate(
          onNavigationRequest: (request) {
            final url = request.url;

            if (url.startsWith("https://myapp.com/payment/success")) {
              widget.onSuccess(); // always confirm server-side afterwards
              return NavigationDecision.prevent;
            } else if (url.startsWith("https://myapp.com/payment/cancel")) {
              widget.onCancel();
              return NavigationDecision.prevent;
            }
            return NavigationDecision.navigate;
          },
        ))
        ..loadRequest(Uri.parse(widget.paymentUrl));
    }

    @override
    Widget build(BuildContext context) {
      return WebViewWidget(controller: _controller);
    }
  }
  ```

  ```swift iOS (Swift) theme={null}
  func webView(_ webView: WKWebView,
               decidePolicyFor action: WKNavigationAction,
               decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {

      let url = action.request.url?.absoluteString ?? ""

      if url.hasPrefix("https://myapp.com/payment/success") {
          decisionHandler(.cancel)
          dismissWebView()
          verifyPaymentStatus() // always confirm server-side
      } else if url.hasPrefix("https://myapp.com/payment/cancel") {
          decisionHandler(.cancel)
          dismissWebView()
      } else {
          decisionHandler(.allow)
      }
  }
  ```

  ```kotlin Android (Kotlin) theme={null}
  webView.webViewClient = object : WebViewClient() {
      override fun shouldOverrideUrlLoading(
          view: WebView,
          request: WebResourceRequest
      ): Boolean {
          val url = request.url.toString()

          return when {
              url.startsWith("https://myapp.com/payment/success") -> {
                  dismissWebView()
                  verifyPaymentStatus() // always confirm server-side
                  true
              }
              url.startsWith("https://myapp.com/payment/cancel") -> {
                  dismissWebView()
                  true
              }
              else -> false
          }
      }
  }
  ```
</CodeGroup>

<Note>
  For a complete implementation including React Native and Flutter, see [Mobile integration](/en/payment-api/hosted-payin/mobile-integration).
</Note>

## Detecting the user's return

When LigdiCash redirects the customer to your `return_url` or `cancel_url`, it indicates that the payment flow on the LigdiCash side is finished. This is not proof of a successful payment.

<Warning>
  Never consider a payment validated based on the redirect to `return_url` alone. A customer can modify the URL manually, or the redirect can occur following a timeout.

  Always verify the status through the [confirm](/en/payment-api/hosted-payin/verify-status) endpoint, or wait for the [callback](/en/payment-api/callback/introduction) notification.
</Warning>

**Recommended behavior on receiving `return_url`:**

1. Display an intermediate screen "Verifying payment…"
2. Call your backend, which calls `confirm` with the token stored at creation
3. Display the final result (success / failure / pending)

## Related pages

* [Create an invoice](/en/payment-api/hosted-payin/create-invoice) — getting the payment URL
* [Verify the status](/en/payment-api/hosted-payin/verify-status) — confirm the payment after redirect
* [Common pitfalls](/en/payment-api/hosted-payin/common-pitfalls) — iframe, empty customer, and other frequent errors
* [Mobile integration](/en/payment-api/hosted-payin/mobile-integration) — React Native, Flutter, iOS, Android
