> ## 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.

# Web

> Reference implementation for Web (TypeScript/JavaScript)

This page contains a complete reference implementation for integrating the MoonPay Developer Platform on Web. The implementation is in TypeScript and uses iframes to render MoonPay frames.

## Overview

The Web SDK consists of the following files:

| File        | Description                                                                                                                  |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `types.ts`  | Type definitions imported from generated OpenAPI types, plus SDK-specific types like `Result`, `Connection`, and error types |
| `api.ts`    | API client functions for fetching payment methods and quotes                                                                 |
| `client.ts` | Main `createClient` function that manages credentials and provides a unified interface                                       |
| `crypto.ts` | X25519 key generation and AES-GCM decryption for secure credential handling                                                  |
| `frames.ts` | Iframe management for check, connect, and Apple Pay frames with postMessage communication                                    |
| `index.ts`  | Public API exports                                                                                                           |

## Project Setup

<Accordion title="package.json" icon="box">
  ```json package.json theme={null}
  {
    "name": "@moonpay/platform",
    "version": "0.0.1",
    "type": "module",
    "main": "./src/index.ts",
    "types": "./src/index.ts",
    "scripts": {
      "generate": "openapi-ts",
      "typecheck": "tsc --noEmit"
    },
    "dependencies": {
      "@noble/ciphers": "^2.1.1",
      "@noble/curves": "^2.0.1",
      "@noble/hashes": "^2.0.1"
    },
    "devDependencies": {
      "@hey-api/openapi-ts": "^0.92.3",
      "typescript": "~5.9.3"
    }
  }
  ```
</Accordion>

<Accordion title="openapi-ts.config.ts - Type generation config" icon="gear">
  Configuration for generating TypeScript types from the [OpenAPI spec](/platform/guides/openapi-codegen). Download the `openapi.json` file to your project root, then run `npm run generate`.

  ```ts openapi-ts.config.ts theme={null}
  import { defineConfig } from "@hey-api/openapi-ts";

  export default defineConfig({
    input: "./openapi.json",
    output: {
      clean: true,
      lint: false,
      path: "./src/gen",
    },
    plugins: [
      {
        name: "@hey-api/typescript",
        enums: false, // Use union types instead of enums for better tree-shaking
      },
    ],
  });
  ```
</Accordion>

## Source Files

<Accordion title="index.ts - Public API exports" icon="file-export">
  ```ts index.ts theme={null}
  // Client
  export { createClient } from "./client.js";
  export type { Client, CreateClientOptions } from "./client.js";

  // Types
  export type {
    Result,
    Connection,
    Credentials,
    CreateClientError,
    GetConnectionError,
    ConnectError,
    ConnectEventError,
    GetPaymentMethodsError,
    GetQuoteError,
    SetupApplePayError,
    ConnectEvent,
    ApplePayEvent,
    ApplePayEventError,
    ConnectFrame,
    ApplePayFrame,
    ConnectOptions,
    GetQuoteParams,
    PaymentMethod,
    Quote,
    Transaction,
    TransactionStatus,
    Fees,
    QuoteLimits,
    Asset,
    Wallet,
  } from "./types.js";
  ```
</Accordion>

<Accordion title="types.ts - Type definitions" icon="brackets-curly">
  Types are imported from the generated OpenAPI types where possible, with SDK-specific types defined separately.

  ```ts types.ts theme={null}
  // Import generated types from OpenAPI spec
  import type {
    AuthorizeReturnType,
    GetQuoteResponse,
    MoonPayPaymentsPaymentMethod,
    TransactionStatus as GeneratedTransactionStatus,
    Fees as GeneratedFees,
    QuoteLimits as GeneratedQuoteLimits,
    Asset as GeneratedAsset,
    Wallet as GeneratedWallet,
  } from "./gen";

  // Re-export generated types
  export type {
    GetQuoteResponse,
    Fees,
    QuoteLimits,
    Asset,
    Wallet,
    TransactionStatus,
  } from "./gen";

  // Alias GetQuoteResponse as Quote for convenience
  export type Quote = GetQuoteResponse;

  // Result type for SDK operations
  export type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };

  // Credentials derived from AuthorizeReturnType
  export type Credentials = Pick<
    AuthorizeReturnType,
    "accessToken" | "clientToken"
  >;

  // Connection status
  export type Connection =
    | { status: "connectionRequired" }
    | { status: "active"; credentials: Credentials }
    | { status: "unavailable" }
    | { status: "pending" }
    | { status: "failed"; reason: string };

  // Error types (SDK-specific)
  export type CreateClientError = {
    kind: "validationError" | "genericError";
    message: string;
  };

  export type GetConnectionError = {
    kind:
      | "ipAddressMismatch"
      | "invalidSessionToken"
      | "frameTimeout"
      | "genericError";
    message: string;
  };

  export type ConnectError = {
    kind:
      | "ipAddressMismatch"
      | "invalidSessionToken"
      | "frameTimeout"
      | "genericError";
    message: string;
  };

  export type ConnectEventError = {
    kind: "genericError";
    message: string;
  };

  export type GetPaymentMethodsError = {
    kind: "validationError" | "genericError";
    message: string;
  };

  export type GetQuoteError = {
    kind: "validationError" | "genericError";
    message: string;
  };

  export type SetupApplePayError = {
    kind:
      | "invalidQuote"
      | "quoteExpired"
      | "applePayUnavailable"
      | "genericError";
    message: string;
  };

  // Event types (SDK-specific)
  export type ConnectEvent =
    | { kind: "ready" }
    | {
        kind: "complete";
        payload: { frame: { dispose: () => void } };
        connection: Connection;
      }
    | { kind: "error"; payload: ConnectEventError };

  // Frame types (SDK-specific)
  export type ConnectFrame = {
    dispose: () => void;
  };

  // Connect options (SDK-specific)
  export type ConnectOptions = {
    container: HTMLElement;
    theme?: { appearance: "dark" | "light" };
    onEvent: (event: ConnectEvent) => void;
  };

  // Payment method - re-export from generated types
  export type PaymentMethod = MoonPayPaymentsPaymentMethod;

  // Quote params (SDK-specific for convenience)
  export type GetQuoteParams = {
    source: string;
    destination: string;
    sourceAmount: string;
    walletAddress: string;
    paymentMethod: string;
  };

  // Transaction - uses generated TransactionStatus with discriminated union
  export type TransactionStatus = GeneratedTransactionStatus;

  export type Transaction =
    | { id: string; status: Exclude<TransactionStatus, "failed"> }
    | { id: string; status: "failed"; failureReason: string };

  // Apple Pay types (SDK-specific)
  export type ApplePayEventError = {
    kind:
      | "invalidQuote"
      | "quoteExpired"
      | "applePayUnavailable"
      | "genericError";
    message: string;
  };

  export type ApplePayEvent =
    | { kind: "ready" }
    | { kind: "complete"; payload: { transaction: Transaction } }
    | {
        kind: "quoteExpired";
        payload: { setQuote: (quoteSignature: string) => void };
      }
    | { kind: "error"; payload: ApplePayEventError }
    | { kind: "unsupported" };

  export type ApplePayFrame = {
    setQuote: (quoteSignature: string) => void;
    dispose: () => void;
  };

  export type SetupApplePayOptions = {
    quote: string;
    container: HTMLElement;
    onEvent: (event: ApplePayEvent) => void;
  };
  ```
</Accordion>

<Accordion title="client.ts - Main client" icon="code">
  ```ts client.ts theme={null}
  import {
    runCheckFrame,
    setupConnectFrame,
    setupApplePay as setupApplePayFrame,
    type KeyPair,
  } from "./frames.js";
  import {
    getPaymentMethods as getPaymentMethodsApi,
    getQuote as getQuoteApi,
  } from "./api.js";
  import type {
    Result,
    Connection,
    Credentials,
    CreateClientError,
    GetConnectionError,
    ConnectError,
    ConnectOptions,
    ConnectFrame,
    PaymentMethod,
    GetPaymentMethodsError,
    Quote,
    GetQuoteParams,
    GetQuoteError,
    ApplePayFrame,
    ApplePayEvent,
    SetupApplePayError,
  } from "./types.js";

  export type MessageDirection = "inbound" | "outbound";

  export type SetupApplePayClientOptions = {
    quote: string;
    container: HTMLElement;
    onEvent: (event: ApplePayEvent) => void;
  };

  export type Client = {
    getConnection: () => Promise<Result<Connection, GetConnectionError>>;
    connect: (
      options: ConnectOptions,
    ) => Promise<Result<ConnectFrame, ConnectError>>;
    getPaymentMethods: () => Promise<
      Result<PaymentMethod[], GetPaymentMethodsError>
    >;
    getQuote: (params: GetQuoteParams) => Promise<Result<Quote, GetQuoteError>>;
    setupApplePay: (
      options: SetupApplePayClientOptions,
    ) => Promise<Result<ApplePayFrame, SetupApplePayError>>;
  };

  export type CreateClientOptions = {
    sessionToken: string;
    onMessage?: (direction: MessageDirection, data: unknown) => void;
  };

  export function createClient(
    options: CreateClientOptions,
  ): Result<Client, CreateClientError> {
    const { sessionToken, onMessage } = options;

    if (!sessionToken || typeof sessionToken !== "string") {
      return {
        ok: false,
        error: { kind: "validationError", message: "Invalid sessionToken" },
      };
    }

    let storedKeyPair: KeyPair | null = null;
    let storedCredentials: Credentials | null = null;

    const client: Client = {
      async getConnection() {
        const result = await runCheckFrame({ sessionToken, onMessage });
        if (result.ok) {
          storedKeyPair = result.keyPair;
          if (result.value.status === "active")
            storedCredentials = result.value.credentials;
          return { ok: true, value: result.value };
        }
        return {
          ok: false,
          error: {
            kind: result.error.kind as GetConnectionError["kind"],
            message: result.error.message,
          },
        };
      },

      async connect(connectOptions) {
        const { container, theme, onEvent } = connectOptions;

        if (!storedKeyPair) {
          const checkResult = await runCheckFrame({ sessionToken, onMessage });
          if (!checkResult.ok) {
            return {
              ok: false,
              error: {
                kind: checkResult.error.kind as ConnectError["kind"],
                message: checkResult.error.message,
              },
            };
          }
          storedKeyPair = checkResult.keyPair;
        }

        const wrappedOnEvent = (event: Parameters<typeof onEvent>[0]) => {
          if (
            event.kind === "complete" &&
            event.connection?.status === "active"
          ) {
            storedCredentials = event.connection.credentials;
          }
          onEvent(event);
        };

        const result = await setupConnectFrame({
          sessionToken,
          keyPair: storedKeyPair,
          container,
          theme: theme?.appearance,
          onEvent: wrappedOnEvent,
          onMessage,
        });

        return result.ok
          ? { ok: true, value: result.value }
          : {
              ok: false,
              error: {
                kind: result.error.kind as ConnectError["kind"],
                message: result.error.message,
              },
            };
      },

      async getPaymentMethods() {
        if (!storedCredentials) {
          return {
            ok: false,
            error: {
              kind: "validationError",
              message:
                "No active connection. Call getConnection() or connect() first.",
            },
          };
        }
        return getPaymentMethodsApi({
          accessToken: storedCredentials.accessToken,
        });
      },

      async getQuote(params) {
        if (!storedCredentials) {
          return {
            ok: false,
            error: {
              kind: "validationError",
              message:
                "No active connection. Call getConnection() or connect() first.",
            },
          };
        }
        return getQuoteApi({
          accessToken: storedCredentials.accessToken,
          ...params,
        });
      },

      async setupApplePay(applePayOptions) {
        if (!storedCredentials) {
          return {
            ok: false,
            error: {
              kind: "genericError",
              message:
                "No active connection. Call getConnection() or connect() first.",
            },
          };
        }
        return setupApplePayFrame({
          clientToken: storedCredentials.clientToken,
          quote: applePayOptions.quote,
          container: applePayOptions.container,
          onEvent: applePayOptions.onEvent,
        });
      },
    };

    return { ok: true, value: client };
  }
  ```
</Accordion>

<Accordion title="api.ts - API client functions" icon="cloud">
  ```ts api.ts theme={null}
  import type {
    Result,
    PaymentMethod,
    GetPaymentMethodsError,
    Quote,
    GetQuoteParams,
    GetQuoteError,
  } from "./types.js";

  const MOONPAY_API_URL = "https://api.moonpay.com";

  export type GetPaymentMethodsOptions = { accessToken: string };
  export type GetQuoteOptions = GetQuoteParams & { accessToken: string };

  export async function getPaymentMethods(
    options: GetPaymentMethodsOptions,
  ): Promise<Result<PaymentMethod[], GetPaymentMethodsError>> {
    try {
      const response = await fetch(
        `${MOONPAY_API_URL}/platform/v1/payment-methods`,
        {
          method: "GET",
          headers: {
            Authorization: `Bearer ${options.accessToken}`,
            "Content-Type": "application/json",
          },
        },
      );
      const data = await response.json();
      if (response.ok && Array.isArray(data)) return { ok: true, value: data };
      return {
        ok: false,
        error: {
          kind: "genericError",
          message: data?.message || "Failed to fetch payment methods",
        },
      };
    } catch (err) {
      return {
        ok: false,
        error: {
          kind: "genericError",
          message: err instanceof Error ? err.message : "Request failed",
        },
      };
    }
  }

  export async function getQuote(
    options: GetQuoteOptions,
  ): Promise<Result<Quote, GetQuoteError>> {
    const {
      accessToken,
      source,
      destination,
      sourceAmount,
      walletAddress,
      paymentMethod,
    } = options;

    try {
      const response = await fetch(`${MOONPAY_API_URL}/platform/v1/quotes/buy`, {
        method: "POST",
        headers: {
          Authorization: `Bearer ${accessToken}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          source: { asset: { code: source }, amount: sourceAmount },
          destination: { asset: { code: destination } },
          wallet: { address: walletAddress },
          paymentMethod: { type: paymentMethod },
        }),
      });

      const data = await response.json();
      if (response.ok && data.data) {
        const quote: Quote = data.data;
        if (!quote.signature) {
          return {
            ok: false,
            error: {
              kind: "validationError",
              message: "Quote is not executable - signature missing",
            },
          };
        }
        return { ok: true, value: quote };
      }
      return {
        ok: false,
        error: {
          kind: "genericError",
          message: data?.message || "Failed to get quote",
        },
      };
    } catch (err) {
      return {
        ok: false,
        error: {
          kind: "genericError",
          message: err instanceof Error ? err.message : "Request failed",
        },
      };
    }
  }
  ```
</Accordion>

<Accordion title="crypto.ts - Encryption utilities" icon="lock">
  ```ts crypto.ts theme={null}
  import { gcm } from "@noble/ciphers/aes.js";
  import { x25519 } from "@noble/curves/ed25519.js";
  import { hkdf } from "@noble/hashes/hkdf.js";
  import { sha256 } from "@noble/hashes/sha2.js";

  export type KeyPair = { privateKey: string; publicKey: string };

  const hexToBytes = (hex: string): Uint8Array => {
    const bytes = new Uint8Array(hex.length / 2);
    for (let i = 0; i < bytes.length; i++)
      bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
    return bytes;
  };

  const bytesToHex = (bytes: Uint8Array): string =>
    bytes.reduce((hex, b) => hex + b.toString(16).padStart(2, "0"), "");

  export const decrypt = (
    encryptedData: string,
    privateKeyHex: string,
  ): string => {
    const parsed = JSON.parse(atob(encryptedData)) as {
      ephemeralPublicKey: string;
      iv: string;
      ciphertext: string;
    };

    const sharedSecret = x25519.getSharedSecret(
      hexToBytes(privateKeyHex),
      hexToBytes(parsed.ephemeralPublicKey),
    );
    const encryptionKey = hkdf(sha256, sharedSecret, undefined, undefined, 32);
    const plainTextBytes = gcm(encryptionKey, hexToBytes(parsed.iv)).decrypt(
      hexToBytes(parsed.ciphertext),
    );

    return new TextDecoder().decode(plainTextBytes);
  };

  export const generateKeyPair = (): KeyPair => {
    const { secretKey, publicKey } = x25519.keygen();
    return {
      privateKey: bytesToHex(secretKey),
      publicKey: bytesToHex(publicKey),
    };
  };
  ```
</Accordion>

<Accordion title="frames.ts - Frame handling" icon="window-restore">
  Iframe management for check, connect, and Apple Pay frames.

  ```ts frames.ts theme={null}
  import { decrypt, generateKeyPair, type KeyPair } from "./crypto.js";
  import type {
    Connection,
    Credentials,
    Result,
    ApplePayFrame,
    ApplePayEvent,
    ApplePayEventError,
    Transaction,
    SetupApplePayError,
    SetupApplePayOptions,
    ConnectEvent,
  } from "./types.js";

  const FRAME_ORIGIN = "https://platform.moonpay.com";

  type FrameMessage = {
    version: 2;
    meta: { channelId: string };
    kind: string;
    payload?: unknown;
  };
  type CompletePayload = {
    status: Connection["status"];
    credentials?: string;
    reason?: string;
  };
  type ErrorPayload = { code: string; message: string };
  type MessageDirection = "inbound" | "outbound";

  function parseMessage(data: unknown, channelId: string): FrameMessage | null {
    try {
      const message: FrameMessage =
        typeof data === "string" ? JSON.parse(data) : data;
      return message.version === 2 && message.meta?.channelId === channelId
        ? message
        : null;
    } catch {
      return null;
    }
  }

  function decryptCredentials(
    encrypted: string,
    privateKey: string,
  ): Credentials {
    return JSON.parse(decrypt(encrypted, privateKey)) as Credentials;
  }

  function buildFrameUrl(path: string, params: Record<string, string>): string {
    const url = new URL(path, FRAME_ORIGIN);
    Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
    return url.toString();
  }

  const FRAME_TIMEOUT_MS = 10000;
  const generateChannelId = () => `ch_${crypto.randomUUID()}`;

  // Check Frame
  export type RunCheckFrameResult =
    | { ok: true; value: Connection; keyPair: KeyPair }
    | { ok: false; error: { kind: string; message: string } };

  export function runCheckFrame(options: {
    sessionToken: string;
    onMessage?: (dir: MessageDirection, data: unknown) => void;
  }): Promise<RunCheckFrameResult> {
    return new Promise((resolve) => {
      const channelId = generateChannelId();
      const keyPair = generateKeyPair();
      let timeoutId: ReturnType<typeof setTimeout>;

      const iframe = document.createElement("iframe");
      iframe.style.display = "none";
      iframe.src = buildFrameUrl("/platform/v1/check-connection", {
        sessionToken: options.sessionToken,
        publicKey: keyPair.publicKey,
        channelId,
      });

      const cleanup = () => {
        window.removeEventListener("message", handleMessage);
        iframe.remove();
      };

      const handleMessage = (event: MessageEvent) => {
        if (!event.origin.includes("moonpay.com")) return;
        const message = parseMessage(event.data, channelId);
        if (!message) return;
        options.onMessage?.("inbound", message);

        if (message.kind === "handshake") {
          const ack = { version: 2, meta: { channelId }, kind: "ack" };
          options.onMessage?.("outbound", ack);
          iframe.contentWindow?.postMessage(JSON.stringify(ack), event.origin);
        } else if (message.kind === "complete") {
          clearTimeout(timeoutId);
          cleanup();
          const payload = message.payload as CompletePayload;
          if (payload.status === "active" && payload.credentials) {
            try {
              resolve({
                ok: true,
                value: {
                  status: "active",
                  credentials: decryptCredentials(
                    payload.credentials,
                    keyPair.privateKey,
                  ),
                },
                keyPair,
              });
            } catch {
              resolve({
                ok: false,
                error: {
                  kind: "genericError",
                  message: "Failed to decrypt credentials",
                },
              });
            }
          } else if (payload.status === "failed") {
            resolve({
              ok: true,
              value: {
                status: "failed",
                reason: payload.reason || "Connection failed",
              },
              keyPair,
            });
          } else {
            resolve({
              ok: true,
              value: { status: payload.status } as Connection,
              keyPair,
            });
          }
        } else if (message.kind === "error") {
          clearTimeout(timeoutId);
          cleanup();
          const payload = message.payload as ErrorPayload;
          resolve({
            ok: false,
            error: { kind: payload.code, message: payload.message },
          });
        }
      };

      window.addEventListener("message", handleMessage);
      document.body.appendChild(iframe);
      timeoutId = setTimeout(() => {
        cleanup();
        resolve({
          ok: false,
          error: {
            kind: "frameTimeout",
            message: "Check frame did not respond in time",
          },
        });
      }, FRAME_TIMEOUT_MS);
    });
  }

  // Connect Frame
  export type SetupConnectFrameResult =
    | { ok: true; value: { dispose: () => void } }
    | { ok: false; error: { kind: string; message: string } };

  export function setupConnectFrame(options: {
    sessionToken: string;
    keyPair: KeyPair;
    container: HTMLElement;
    theme?: "dark" | "light";
    onEvent: (event: ConnectEvent) => void;
    onMessage?: (dir: MessageDirection, data: unknown) => void;
  }): Promise<SetupConnectFrameResult> {
    return new Promise((resolve) => {
      const channelId = generateChannelId();
      let timeoutId: ReturnType<typeof setTimeout>;

      const iframe = document.createElement("iframe");
      iframe.style.cssText = "width:100%;height:100%;border:none";
      iframe.allow = "camera; microphone";
      iframe.src = buildFrameUrl("/platform/v1/connect", {
        sessionToken: options.sessionToken,
        publicKey: options.keyPair.publicKey,
        channelId,
        ...(options.theme && { theme: options.theme }),
      });

      const dispose = () => {
        window.removeEventListener("message", handleMessage);
        iframe.remove();
      };

      const handleMessage = (event: MessageEvent) => {
        if (!event.origin.includes("moonpay.com")) return;
        const message = parseMessage(event.data, channelId);
        if (!message) return;
        options.onMessage?.("inbound", message);

        if (message.kind === "handshake") {
          const ack = { version: 2, meta: { channelId }, kind: "ack" };
          options.onMessage?.("outbound", ack);
          iframe.contentWindow?.postMessage(JSON.stringify(ack), event.origin);
        } else if (message.kind === "ready") {
          clearTimeout(timeoutId);
          options.onEvent({ kind: "ready" });
        } else if (message.kind === "complete") {
          clearTimeout(timeoutId);
          const payload = message.payload as CompletePayload;
          if (payload.status === "connectionRequired") return;
          if (payload.status === "active" && payload.credentials) {
            try {
              options.onEvent({
                kind: "complete",
                payload: { frame: { dispose } },
                connection: {
                  status: "active",
                  credentials: decryptCredentials(
                    payload.credentials,
                    options.keyPair.privateKey,
                  ),
                },
              });
            } catch {
              options.onEvent({
                kind: "error",
                payload: {
                  kind: "genericError",
                  message: "Failed to decrypt credentials",
                },
              });
            }
          } else if (payload.status === "failed") {
            options.onEvent({
              kind: "complete",
              payload: { frame: { dispose } },
              connection: {
                status: "failed",
                reason: payload.reason || "Connection failed",
              },
            });
          } else {
            options.onEvent({
              kind: "complete",
              payload: { frame: { dispose } },
              connection: { status: payload.status } as Connection,
            });
          }
        } else if (message.kind === "error") {
          clearTimeout(timeoutId);
          options.onEvent({
            kind: "error",
            payload: {
              kind: "genericError",
              message: (message.payload as ErrorPayload).message,
            },
          });
        }
      };

      window.addEventListener("message", handleMessage);
      options.container.appendChild(iframe);
      timeoutId = setTimeout(() => {
        dispose();
        resolve({
          ok: false,
          error: {
            kind: "frameTimeout",
            message: "Connect frame did not respond in time",
          },
        });
      }, FRAME_TIMEOUT_MS);
      resolve({ ok: true, value: { dispose } });
    });
  }

  // Apple Pay Frame
  export function setupApplePay(
    options: SetupApplePayOptions & { clientToken: string },
  ): Promise<Result<ApplePayFrame, SetupApplePayError>> {
    return new Promise((resolve) => {
      const channelId = crypto.randomUUID();
      let frameOrigin: string | null = null;

      const iframe = document.createElement("iframe");
      iframe.style.cssText = "width:100%;height:100%;border:none";
      iframe.allow = "payment";
      const url = new URL("/platform/v1/apple-pay", FRAME_ORIGIN);
      url.searchParams.set("clientToken", options.clientToken);
      url.searchParams.set("signature", options.quote);
      url.searchParams.set("channelId", channelId);
      iframe.src = url.toString();

      const sendSetQuote = (signature: string) => {
        if (!iframe.contentWindow || !frameOrigin) return;
        iframe.contentWindow.postMessage(
          JSON.stringify({
            version: 2,
            meta: { channelId },
            kind: "setQuote",
            payload: { quote: { signature } },
          }),
          frameOrigin,
        );
      };

      const dispose = () => {
        window.removeEventListener("message", handleMessage);
        iframe.remove();
      };

      const handleMessage = (event: MessageEvent) => {
        if (!event.origin.includes("moonpay.com")) return;
        const message = parseMessage(event.data, channelId);
        if (!message) return;

        if (message.kind === "handshake") {
          frameOrigin = event.origin;
          iframe.contentWindow?.postMessage(
            JSON.stringify({ version: 2, meta: { channelId }, kind: "ack" }),
            event.origin,
          );
        } else if (message.kind === "ready") {
          options.onEvent({ kind: "ready" });
        } else if (message.kind === "complete") {
          options.onEvent({
            kind: "complete",
            payload: {
              transaction: (message.payload as { transaction: Transaction })
                .transaction,
            },
          });
        } else if (message.kind === "error") {
          const payload = message.payload as ApplePayEventError;
          const errorKind =
            payload.kind || (payload as unknown as { code?: string }).code;
          if (errorKind === "quoteExpired")
            options.onEvent({
              kind: "quoteExpired",
              payload: { setQuote: sendSetQuote },
            });
          else if (errorKind === "applePayUnavailable")
            options.onEvent({ kind: "unsupported" });
          else options.onEvent({ kind: "error", payload });
        }
      };

      window.addEventListener("message", handleMessage);
      options.container.appendChild(iframe);
      resolve({ ok: true, value: { setQuote: sendSetQuote, dispose } });
    });
  }

  export { generateKeyPair, type KeyPair };
  ```
</Accordion>
