Buyer Experience — WebView Integration

Embed the WalletConnect Pay checkout UI inside a native WebView so users complete payments without leaving your app.

Instead of redirecting users to an external browser, load the WalletConnect Pay checkout portal inside a native WebView. Supply two query parameters on the gatewayUrl, handle JavaScript bridge messages for the outcome, and intercept wallet deeplinks so the OS can route the user to their wallet app and back.

The hosted UI handles wallet selection, payment option display, compliance data collection, and signing orchestration. Your app only needs to render a WebView, respond to the result, and verify it server-side.

How It Works

Prerequisites

  • A gatewayUrl from the Merchant API (POST /v1/merchant/payment)
  • Your app registered to handle its own deep link scheme (so wallets can return after signing)
  • JavaScript and DOM storage enabled in the WebView

URL Construction

Start with the gatewayUrl the API returns and append the two WebView parameters. Never construct the base URL manually.

function buildPayUrl(gatewayUrl: string, appDeepLink: string): string {
  const url = new URL(gatewayUrl);
  // Required: the wallet returns here after signing
  url.searchParams.set('returnUrl', appDeepLink);
  // Required: open wallets via universal links instead of custom schemes
  url.searchParams.set('preferUniversalLinks', '1');
  return url.toString();
}

// Example
const payUrl = buildPayUrl(
  'https://pay.walletconnect.com/buy?pid=pay_01ABC',
  'myapp://'   // your app's registered deep link
);
ParameterRequiredDescription
returnUrlYesYour app's native deep link (e.g. myapp://). The checkout passes this to wallets as the return destination so the OS routes the user back after signing.
preferUniversalLinksYesSet to 1. Tells the checkout to open wallets via universal links (https://) rather than custom URL schemes when both are available — more reliable on iOS.

Bridge Messages

The checkout calls window.ReactNativeWebView.postMessage(json) with a JSON string payload.

type fieldsuccess fieldMeaning
PAY_SUCCESStruePayment succeeded. Optionally contains message — a human-readable confirmation summary.
PAY_FAILUREfalsePayment failed. Optionally contains error — a human-readable reason.

The checkout may send either the type field or the success boolean (or both). Treat type === "PAY_SUCCESS" or success === true as a success signal; type === "PAY_FAILURE" or success === false as failure.

Always verify the final payment status server-side via GET /v1/payments/{paymentId}/status after receiving PAY_SUCCESS. Never rely solely on the bridge message to confirm a payment.

The checkout opens the user's wallet by navigating to a URL that carries a ?uri=wc:… query parameter. WebViews don't handle these natively, so your app must intercept these navigations and forward them to the OS.

The checkout triggers this as a same-frame navigation, so intercepting navigation requests is all you need. Only forward URLs that carry a valid wc: URI — block any other non-https: navigation to prevent the page from driving the OS into arbitrary native schemes (tel:, sms:, intent:, etc.).

function isWalletDeeplink(url: string): boolean {
  try {
    const wcUri = new URL(url).searchParams.get('uri');
    return !!wcUri && wcUri.startsWith('wc:');
  } catch {
    return false;
  }
}

Platform Examples

Install the WebView package:

npm install react-native-webview
import React, {useCallback} from 'react';
import {Linking, StyleSheet} from 'react-native';
import {WebView, WebViewMessageEvent} from 'react-native-webview';
import type {ShouldStartLoadRequest} from 'react-native-webview/lib/WebViewTypes';

type PayMessage = {
  type?: 'PAY_SUCCESS' | 'PAY_FAILURE';
  success?: boolean;
  error?: string;
  message?: string;
};

function isWalletDeeplink(url: string): boolean {
  try {
    const wcUri = new URL(url).searchParams.get('uri');
    return !!wcUri && wcUri.startsWith('wc:');
  } catch {
    return false;
  }
}

function buildPayUrl(gatewayUrl: string, appDeepLink: string): string {
  const url = new URL(gatewayUrl);
  url.searchParams.set('returnUrl', appDeepLink);
  url.searchParams.set('preferUniversalLinks', '1');
  return url.toString();
}

interface PayWebViewProps {
  gatewayUrl: string;
  appDeepLink: string;
  onSuccess: (message?: string) => void;
  onFailure: (error?: string) => void;
}

export function PayWebView({gatewayUrl, appDeepLink, onSuccess, onFailure}: PayWebViewProps) {
  const url = buildPayUrl(gatewayUrl, appDeepLink);

  const onShouldStartLoadWithRequest = useCallback(
    (request: ShouldStartLoadRequest): boolean => {
      if (isWalletDeeplink(request.url)) {
        Linking.openURL(request.url).catch(console.warn);
        return false;
      }
      // Allow https (the checkout) and about:; block any other scheme so the
      // page can't drive the OS into arbitrary native schemes (tel:, sms:, …).
      try {
        const scheme = new URL(request.url).protocol;
        return scheme === 'https:' || scheme === 'about:';
      } catch {
        return false;
      }
    },
    [],
  );

  const onMessage = useCallback(
    (event: WebViewMessageEvent) => {
      let msg: PayMessage;
      try {
        msg = JSON.parse(event.nativeEvent.data);
      } catch {
        return;
      }
      if (msg.type === 'PAY_SUCCESS' || msg.success === true) {
        onSuccess(msg.message);
      } else if (msg.type === 'PAY_FAILURE' || msg.success === false) {
        onFailure(msg.error);
      }
    },
    [onSuccess, onFailure],
  );

  return (
    <WebView
      source={{uri: url}}
      style={styles.webview}
      startInLoadingState
      javaScriptEnabled
      domStorageEnabled
      onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
      onMessage={onMessage}
    />
  );
}

const styles = StyleSheet.create({webview: {flex: 1}});

Register your returnUrl scheme so the OS routes users back after signing in their wallet.

<!-- AndroidManifest.xml -->
<intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="myapp" />
</intent-filter>

Verifying Payment Status

After PAY_SUCCESS, confirm server-side before marking the order as paid:

const res = await fetch(
  `https://api.pay.walletconnect.com/v1/payments/${paymentId}/status`,
  { headers: { 'Api-Key': process.env.WCP_API_KEY } }
);
const { status } = await res.json(); // "succeeded" | "processing" | "failed" | …

See the Payments Status API for the full response schema.

Best Practices

  • Enable JavaScript and DOM storage — both required. JavaScript renders the checkout; DOM storage backs its session and wallet state.
  • Always intercept wallet deeplinks — URLs with ?uri=wc:… must be forwarded to the OS. The checkout triggers these as same-frame navigations, so intercepting navigation requests covers it.
  • Only intercept wc: deeplinks — block other non-https: navigations to prevent arbitrary native app launches.
  • Handle both message formats — check type === "PAY_SUCCESS" and success === true for forward compatibility.
  • Always verify server-side — treat bridge messages as a trigger to check status, not as proof of payment.
  • Keep the WebView full-screen or modal — the checkout is optimized for full viewport rendering.

Reference Example

PayWebView — React Native example

Full implementation including success animation, wallet deeplink handling, and error recovery.

On this page