Skip to main content
Load the Daimo payment UI inside a native WebView (iOS WKWebView, Android WebView, or React Native). No SDK installation required — just create a session and construct a URL.

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

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)
layoutNoembedRenders inline; omit for modal (default)

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
paymentStartedUser’s deposit transaction is detected
paymentCompletedFunds delivered to destination
Payment events are notifications only — the payload object is empty. 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
The page sends contentHeightChanged whenever its rendered height changes. 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
  />
);

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. Use theme=light or theme=dark to force a mode. Otherwise, the UI follows the user’s system preference. See Customization for the dashboard flow, token list, and per-session CSS override.

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": {}
}