Skip to main content
Load the Daimo payment UI inside any native WebView (iOS WKWebView, Android WebView, or a plain react-native-webview). No SDK installation required — create a session and construct a URL.
On React Native, prefer DaimoFrameRN from @daimo/sdk — it handles sizing, dismissal, and deeplink hand-off for you. Use this guide when you can’t add the SDK, or you’re on native iOS / Android.

How it works

  1. Create a session via the API
  2. Construct the webview URL from the session ID and client secret
  3. Load that URL in your native WebView
  4. Listen for postMessage events to track payment progress

Create a session

curl -X POST https://api.daimo.com/v1/sessions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "destination": {
      "type": "evm",
      "address": "0xYourAddress",
      "chainId": 8453,
      "tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "amountUnits": "10.00"
    },
    "display": {
      "title": "Deposit to Acme",
      "verb": "Deposit"
    }
  }'
Then construct the webview URL from the response:
https://daimo.com/webview?session={sessionId}&cs={clientSecret}

Load the WebView

iOS (Swift)

import WebKit

let webView = WKWebView(frame: view.bounds)
view.addSubview(webView)

// Register message handler for payment events
let handler = PaymentMessageHandler()
webView.configuration.userContentController.add(handler, name: "daimoPay")

// Construct the URL from your session response
let urlString = "https://daimo.com/webview?session=\(sessionId)&cs=\(clientSecret)"
let url = URL(string: urlString)!
webView.load(URLRequest(url: url))

class PaymentMessageHandler: NSObject, WKScriptMessageHandler {
    func userContentController(
        _ controller: WKUserContentController,
        didReceive message: WKScriptMessage
    ) {
        guard let body = message.body as? [String: Any],
              let type = body["type"] as? String else { return }

        switch type {
        case "ready": print("Payment UI loaded")
        case "paymentStarted": print("Payment initiated")
        case "paymentCompleted": print("Payment succeeded")
        default: break
        }
    }
}

Android (Kotlin)

val webView = WebView(this)
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true

webView.addJavascriptInterface(object {
    @JavascriptInterface
    fun postMessage(message: String) {
        val json = JSONObject(message)
        when (json.getString("type")) {
            "ready" -> Log.d("Daimo", "Payment UI loaded")
            "paymentStarted" -> Log.d("Daimo", "Payment initiated")
            "paymentCompleted" -> Log.d("Daimo", "Payment succeeded")
        }
    }
}, "ReactNativeWebView")

// Construct the URL from your session response
val url = "https://daimo.com/webview?session=$sessionId&cs=$clientSecret"
webView.loadUrl(url)

React Native (without the SDK)

import { WebView } from "react-native-webview";

function PaymentWebView({
  sessionId,
  clientSecret,
}: {
  sessionId: string;
  clientSecret: string;
}) {
  const uri = `https://daimo.com/webview?session=${sessionId}&cs=${clientSecret}`;

  const handleMessage = (event: { nativeEvent: { data: string } }) => {
    const msg = JSON.parse(event.nativeEvent.data);
    switch (msg.type) {
      case "ready":
        console.log("Payment UI loaded");
        break;
      case "contentHeightChanged":
        console.log("Payment UI height", msg.payload.height);
        break;
      case "paymentStarted":
        console.log("Payment initiated");
        break;
      case "paymentCompleted":
        console.log("Payment succeeded");
        break;
    }
  };

  return (
    <WebView
      source={{ uri }}
      onMessage={handleMessage}
      javaScriptEnabled
      domStorageEnabled
    />
  );
}

Query parameters

ParameterRequiredValuesDescription
sessionYesstringSession ID
csYesstringClient secret
localeNoes, fr, etc.UI language (default: browser locale)
themeNolight, darkColor theme (default: auto)
layoutNoembed, centerembed renders inline; center vertically centers full-height

Handle events

The WebView sends messages via postMessage. The message format:
{
  "source": "daimo-pay",
  "version": 1,
  "type": "paymentCompleted",
  "payload": {}
}
EventDescription
readyPayment UI finished loading
modalOpenedModal became visible
modalClosedModal was dismissed
contentHeightChangedEmbedded content height changed in layout=embed
paymentStartedUser’s deposit transaction is detected
paymentCompletedFunds delivered to destination
Payment events are notifications only — the payload object is empty. In layout=embed, contentHeightChanged includes { "height": number } in CSS pixels. To get full session details (tx hash, chain, amounts, etc.) after any payment event, poll GET /v1/sessions/{sessionId}.

Fit embedded height

When you load the webview inside your own modal or bottom sheet, add layout=embed and size the host container to the reported content height. This avoids showing a full-screen webview with empty space below the payment UI.
https://daimo.com/webview?session={sessionId}&cs={clientSecret}&layout=embed
Only layout=embed sends contentHeightChanged. In React Native, store that height and apply it to the WebView:
const [height, setHeight] = useState(480);

const handleMessage = (event: { nativeEvent: { data: string } }) => {
  const msg = JSON.parse(event.nativeEvent.data);
  if (msg.type === "contentHeightChanged") {
    setHeight(msg.payload.height);
  }
};

return (
  <WebView
    source={{ uri }}
    onMessage={handleMessage}
    style={{ height }}
    javaScriptEnabled
    domStorageEnabled
  />
);

Center full-height WebViews

When the WebView itself fills the screen, add layout=center to center the payment UI in a full-height themed WebView shell:
https://daimo.com/webview?session={sessionId}&cs={clientSecret}&layout=center
This mode does not send contentHeightChanged; the host WebView should own the full available height.

Theming

Set your org theme in the Daimo dashboard to match the hosted payment UI to your brand. The WebView loads it automatically from the session. Choose Automatic, Light, or Dark in dashboard customization to control the default active mode. Pass theme=light or theme=dark to override it for a specific WebView. See Customization for the dashboard flow and token list.

sendToHost protocol

The webview page communicates with the host container via a sendToHost function that tries three transport layers in order:
  1. React Native / Expowindow.ReactNativeWebView.postMessage(JSON.stringify(msg))
  2. iOS / macOS WKWebViewwindow.webkit.messageHandlers.daimoPay.postMessage(msg)
  3. Browser iframe / desktop WebViewwindow.parent.postMessage(msg, "*")
All messages share a common envelope:
{
  "source": "daimo-pay",
  "version": 1,
  "type": "<event name>",
  "payload": {}
}