Skip to main content

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.

Mount the headless Buy frame into your UI and execute a transaction from a quote signature. The frame renders no visible UI — it drives the buy pipeline in the background and emits events you handle from your own purchase screen.
The Buy frame is headless. When it emits a challenge event, render a separate challenge frame with client.setupChallenge() at the URL from the event payload. See the Handle challenges guide for the full flow.
Setup buy
import { createClient, type BuyEvent } from "@moonpay/platform-sdk-web";

const client = createClient({ sessionToken: "c3N0XzAwMQ==" });

const buyResult = await client.setupBuy({
  quote: quoteResult.value.data.signature,
  container: document.querySelector("#buyContainer"),
  externalTransactionId: "order_12345",
  onEvent: (event: BuyEvent) => {
    switch (event.kind) {
      case "ready":
        // Pipeline starting — show a loading indicator
        break;
      case "complete":
        // Transaction created. Track final status via polling.
        console.log(event.payload.transaction);
        break;
      case "challenge":
        // Verification required — render the challenge frame at the URL.
        // See: /platform/guides/handling-challenges
        openChallengeFrame(event.payload.url);
        break;
      case "error":
        console.error(event.payload.code, event.payload.message);
        break;
    }
  },
});

if (!buyResult.ok) {
  // Handle error
  console.error(buyResult.error.kind, buyResult.error.message);
  return;
}

const buy = buyResult.value;

Parameters

PropertyTypeRequiredDescription
quotestringThe quote signature returned from getQuote.
containerHTMLElementA DOM element to mount the headless Buy frame into. The frame has zero dimensions and renders no visible UI.
externalTransactionIdstringYour own identifier for the transaction. Stored on the MoonPay transaction for correlation.
onEvent(event: BuyEvent) => voidCallback invoked for buy flow events. See BuyEvent.
This method does not require a separate auth token. The client uses stored credentials from an active connection.

BuyEvent

onEvent receives events as the buy pipeline progresses. Use event.kind to decide how to handle each event.
kindPayloadWhen you receive it
"ready"The buy pipeline has started. Show a loading state in your own UI.
"complete"{ transaction: FrameTransaction }The transaction was created. Use the transaction id to poll for final status.
"challenge"BuyChallengePayloadVerification is required before the transaction can proceed. Render the challenge frame at the provided url using setupChallenge().
"error"BuyEventErrorThe flow encountered an error. Surface the message to developers and tear down the frame.

FrameTransaction

This is the transaction object returned when the buy pipeline completes. FrameTransaction is a discriminated union — the failure variant carries failureReason, the non-failure variant always carries id. Pass id to client.getTransaction() to poll for the final status.
FieldTypeRequiredDescription
statusstringThe transaction status. On the failure variant, "failed".
idstringRequired on the non-failure variant; optional when status is "failed" (a transaction may not exist yet on early failure).
failureReasonstringPresent only on the failure variant (status === "failed"). A developer-friendly reason.

BuyChallengePayload

FieldTypeRequiredDescription
kindstringThe challenge type (currently always "frame", but typed as string for forward compatibility).
urlstringA fully-formed URL to pass directly to setupChallenge(). Do not modify it.

BuyEventError

FieldTypeRequiredDescription
codestringThe error category. Includes configurationError, invalidQuote, and backend-specific error codes.
messagestringDeveloper-friendly details. Not intended to be rendered in UI.

Result

client.setupBuy() returns a Result<BuyFrame, SetupBuyError>.

Result envelope

Result<BuyFrame, SetupBuyError>
FieldTypeRequiredDescription
okbooleanWhether the operation succeeded.
valueBuyFramePresent when ok is true.
errorSetupBuyErrorPresent when ok is false.

BuyFrame

FieldTypeRequiredDescription
setQuote(signature: string) => voidUpdates the quote signature used by the frame. Use this when the current quote expires before the customer completes the purchase — fetch a new quote and pass its signature.
dispose() => voidUnmounts the frame. After you call this, no further events are dispatched to your onEvent callback. Always call dispose() once the flow finishes or errors out.

SetupBuyError

FieldTypeRequiredDescription
kind"configurationError" | "genericError"The error category.
messagestringDeveloper-friendly details.

Full example

The following example walks through the full card-payment flow: get a quote for a stored card, mount the headless Buy frame, hand off to a challenge frame when verification is required, and dispose of the frame when the transaction completes.
Buy with a stored card
import {
  createClient,
  type BuyEvent,
  type ChallengeEvent,
} from "@moonpay/platform-sdk-web";

const client = createClient({ sessionToken: "c3N0XzAwMQ==" });

// 1. Get a quote for the selected stored card.
const quoteResult = await client.getQuote({
  source: "USD",
  destination: "ETH",
  sourceAmount: "100.00",
  walletAddress: "0x1234567890abcdef1234567890abcdef12345678",
  paymentMethod: { type: "card", id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" },
});
if (!quoteResult.ok) throw new Error(quoteResult.error.message);

// 2. Mount the headless Buy frame.
const buyResult = await client.setupBuy({
  quote: quoteResult.value.data.signature,
  container: document.querySelector("#buyContainer"),
  externalTransactionId: "order_12345",
  onEvent: (event: BuyEvent) => {
    switch (event.kind) {
      case "ready":
        showLoadingIndicator();
        break;

      case "challenge":
        // 3. Hand off to the challenge frame for verification.
        handleChallenge(event.payload.url);
        break;

      case "complete":
        // The transaction was created (or failed). Inspect FrameTransaction
        // before polling — `id` is only required on the non-failure variant.
        buyResult.value.dispose();
        if (event.payload.transaction.status !== "failed") {
          pollTransaction(event.payload.transaction.id);
        }
        break;

      case "error":
        buyResult.value.dispose();
        console.error(event.payload.code, event.payload.message);
        showError(event.payload.message);
        break;
    }
  },
});

if (!buyResult.ok) {
  console.error(buyResult.error.kind, buyResult.error.message);
  return;
}

async function handleChallenge(challengeUrl: string) {
  await client.setupChallenge({
    url: challengeUrl,
    container: document.querySelector("#challengeModal"),
    onEvent: (event: ChallengeEvent) => {
      switch (event.kind) {
        case "complete":
          // Verification resolved. For buy challenges, the transaction is in payload.
          if (event.payload.flow === "buy") {
            pollTransaction(event.payload.transaction.id);
          }
          buyResult.value.dispose();
          break;

        case "cancelled":
          buyResult.value.dispose();
          showRetryOption();
          break;

        case "error":
          buyResult.value.dispose();
          console.error(event.payload.message);
          break;
      }
    },
  });
}
For the end-to-end card payment walkthrough — listing payment methods, adding a card, and tracking the transaction to a terminal status — see the Pay with card guide. For details on the challenge flow, see Handle challenges.
types.ts
type BuyFrame = {
  setQuote: (signature: string) => void;
  dispose: () => void;
};

type FrameTransaction =
  | { id: string; status: string }
  | { id?: string; status: "failed"; failureReason: string };

type BuyEvent =
  | {
      kind: "ready";
    }
  | {
      kind: "complete";
      payload: {
        transaction: FrameTransaction;
      };
    }
  | {
      kind: "challenge";
      payload: {
        /** Currently "frame", but typed as `string` for forward compatibility. */
        kind: string;
        /** Fully-formed URL to pass directly to setupChallenge(). */
        url: string;
      };
    }
  | {
      kind: "error";
      payload: BuyEventError;
    };

type BuyEventError = {
  /**
   * Includes "configurationError", "invalidQuote", and backend-specific
   * error codes returned during the buy pipeline.
   */
  code: string;
  message: string;
};

type SetupBuyError = {
  kind: "configurationError" | "genericError";
  message: string;
};