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

# Prewarm the widget

> Load the MoonPay widget in the background before your customer opens it, so it appears instantly instead of showing a loading screen.

Prewarming loads the MoonPay widget in a hidden iframe or web view before your customer asks for it. The widget downloads its code, loads your account's theme and configuration, and renders its first screen in the background. When the customer taps your buy or sell button, you make the already-loaded widget visible.

On a cold load the widget can take several seconds to become usable, especially on mobile networks. Prewarming moves that time to a moment when the customer is not waiting.

<Note>
  Prewarming is supported on the `/v2/buy` and `/v2/sell` widget URLs only. See
  [Widget URLs](#widget-urls).
</Note>

## When to prewarm

Prewarm when there is a strong signal that the customer is about to open the widget. For example:

* The customer opens a screen that has your buy or sell button.
* The customer selects a token they do not hold enough of.

Do not prewarm on every page load or app launch. Each prewarm loads the full widget, which uses the customer's data and memory.

## How it works

```mermaid theme={null}
sequenceDiagram
    participant C as Customer
    participant P as Your app
    participant M as MoonPay widget
    P->>M: Create hidden iframe with the widget URL
    Note over M: Download code, load configuration,<br/>render the first screen
    M-->>P: postMessage MOONPAY_READY
    C->>P: Tap buy
    P->>M: Make the iframe visible
    Note over C,M: The widget appears with no loading screen
```

1. Build the widget URL with all the parameters you need, including the [signature](/widget/on-ramp/customization/url-signing) if you use one.
2. Create the iframe or web view with that URL. Keep it hidden but at its final size.
3. Listen for the `MOONPAY_READY` message.
4. When the customer taps your button and `MOONPAY_READY` has arrived, make the widget visible.

If the customer taps before `MOONPAY_READY` arrives, show your own loading state and reveal the widget when the message arrives. The widget keeps loading from wherever the background load reached.

Always add a fallback. If `MOONPAY_READY` has not arrived 5 seconds after the customer taps your button, reveal the widget anyway. The widget then shows its own loading screen or any error, instead of leaving the customer on your loading state.

## Widget URLs

| Flow | URL |
| - | - |
| Buy | `https://buy.moonpay.com/v2/buy` |
| Sell | `https://buy.moonpay.com/v2/sell` |

Add your [on-ramp](/widget/on-ramp/customization/parameters) or [off-ramp](/widget/off-ramp/customization/parameters) parameters as a query string, for example `https://buy.moonpay.com/v2/buy?apiKey=pk_live_123&currencyCode=eth`.

To test in sandbox, use your `pk_test_` API key with the same URLs. The widget switches to the sandbox environment based on the key.

## The ready message

When the widget has rendered its first screen, it sends this message to the page or app that hosts it:

```json theme={null}
{
  "type": "MOONPAY_READY",
  "timestamp": 1790000000000
}
```

| Host | How the message is delivered |
| - | - |
| iframe | `window.postMessage` to the parent window, as an object |
| React Native `react-native-webview` | `window.ReactNativeWebView.postMessage`, as a JSON string |

Your listener must ignore repeat `MOONPAY_READY` messages from the same load.

## Prewarm in a web page

Keep the iframe in the page at its final size, fully transparent, and unable to receive clicks or focus. Do not use `display: none`. A widget that has no size cannot lay out its first screen correctly.

```html theme={null}
<button id="buy-button">Buy crypto</button>
<div id="moonpay-container"></div>
```

```css theme={null}
#moonpay-container iframe {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

#moonpay-container iframe.moonpay-hidden {
  opacity: 0;
  pointer-events: none;
}
```

```javascript theme={null}
const WIDGET_ORIGIN = "https://buy.moonpay.com";
const READY_FALLBACK_MS = 5000;

function prewarmWidget(widgetUrl) {
  const iframe = document.createElement("iframe");
  iframe.src = widgetUrl;
  iframe.allow =
    "accelerometer; autoplay; camera; encrypted-media; gyroscope; payment";
  iframe.className = "moonpay-hidden";
  iframe.inert = true;
  iframe.setAttribute("aria-hidden", "true");

  let isReady = false;
  let isRequested = false;

  function reveal() {
    iframe.classList.remove("moonpay-hidden");
    iframe.inert = false;
    iframe.removeAttribute("aria-hidden");
  }

  function revealIfReady() {
    if (isReady && isRequested) reveal();
  }

  window.addEventListener("message", (event) => {
    if (event.origin !== WIDGET_ORIGIN) return;
    if (event.source !== iframe.contentWindow) return;
    if (event.data?.type === "MOONPAY_READY") {
      isReady = true;
      revealIfReady();
    }
  });

  document.getElementById("moonpay-container").appendChild(iframe);

  return {
    show() {
      isRequested = true;
      revealIfReady();
      setTimeout(reveal, READY_FALLBACK_MS);
    },
    isReady: () => isReady,
  };
}

const widget = prewarmWidget(
  "https://buy.moonpay.com/v2/buy?apiKey=pk_live_123&currencyCode=eth",
);

document.getElementById("buy-button").addEventListener("click", () => {
  widget.show();
});
```

Your domain must be allowed to embed the widget in an iframe. This is the same requirement as a standard iframe integration.

## Prewarm in a React Native app

Use the `WebView` component from [`react-native-webview`](https://github.com/react-native-webview/react-native-webview). It renders a `WKWebView` on iOS and an Android `WebView` on Android, and it can load the widget while it is hidden.

In-app browsers, such as `expo-web-browser`, `SFSafariViewController`, and Chrome Custom Tabs, cannot be used to prewarm. They are presented by the operating system, and they start to load only when they open.

Render the `WebView` at its final size behind your content. Bring it forward when the customer has tapped your button and the widget is ready.

```tsx theme={null}
import { useEffect, useState } from "react";
import { ActivityIndicator, Button, StyleSheet, View } from "react-native";
import { WebView } from "react-native-webview";

const WIDGET_URL =
  "https://buy.moonpay.com/v2/buy?apiKey=pk_live_123&currencyCode=eth";
const READY_FALLBACK_MS = 5000;

export function BuyScreen() {
  const [isRequested, setIsRequested] = useState(false);
  const [isReady, setIsReady] = useState(false);
  const [isFallbackDue, setIsFallbackDue] = useState(false);
  const isVisible = isRequested && (isReady || isFallbackDue);

  useEffect(() => {
    if (!isRequested) return;
    const timer = setTimeout(() => setIsFallbackDue(true), READY_FALLBACK_MS);
    return () => clearTimeout(timer);
  }, [isRequested]);

  return (
    <View style={styles.container}>
      <WebView
        source={{ uri: WIDGET_URL }}
        style={[styles.widget, !isVisible && styles.hidden]}
        pointerEvents={isVisible ? "auto" : "none"}
        onMessage={(event) => {
          try {
            const message = JSON.parse(event.nativeEvent.data);
            if (message.type === "MOONPAY_READY") {
              setIsReady(true);
            }
          } catch {
            // Ignore messages that are not JSON.
          }
        }}
      />
      {!isRequested && (
        <Button title="Buy crypto" onPress={() => setIsRequested(true)} />
      )}
      {isRequested && !isVisible && <ActivityIndicator style={styles.loader} />}
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1 },
  widget: { ...StyleSheet.absoluteFillObject },
  hidden: { opacity: 0 },
  loader: { ...StyleSheet.absoluteFillObject },
});
```

<Warning>
  If you use `SFSafariViewController` or Chrome Custom Tabs today for Apple Pay
  or Google Pay support, switching to a web view to prewarm can make those
  payment methods unavailable. See [Mobile payments](/widget/mobile-payments).
</Warning>

## Best practices

* **Use the final URL.** Build the URL with the amount, currency, wallet address, and signature before you prewarm. If a parameter changes, you must load a new URL, and the customer waits for a full load again.
* **Use one prewarmed widget at a time.** Remove any earlier hidden widget before you create a new one.
* **Do not reuse a closed widget.** After the customer closes the widget or completes a transaction, remove it. Prewarm a new one if the customer is likely to return.
* **Keep the hidden widget at its final size.** The widget lays out its first screen for the size it has when it loads.
* **Keep it out of reach while hidden.** Make sure the hidden widget cannot receive taps, clicks, keyboard focus, or screen reader focus.
