Skip to main content
Use this guide to add an express checkout button to your app. The buy button is a MoonPay-hosted button that, on tap, lets the customer pick a payment method (Apple Pay, Google Pay, or card), confirm the purchase in a hosted sheet, and complete the transaction — all from one piece of UI. Set up the button after you have a connected customer. MoonPay handles payment-method selection, the confirmation sheet, and any verification challenges inside hosted frames, so card data and sensitive verification never touch your domain. See the Going Live section for requirements you must meet before taking this integration to production.

When to use the buy button

MoonPay gives you three ways to run a buy. Pick the one that matches how much of the experience you want to own.
ApproachWhat you buildBest for
Buy button (setupBuyButton())A container. MoonPay renders the button, the payment-method picker, and the confirmation sheet.A fast, low-effort express checkout where you want a single ready-made payment button.
Headless buy (setupBuy())Your own button, amount entry, and confirmation screen. The frame is headless and emits events.Full control over the purchase UI while MoonPay runs the pipeline behind the scenes.
Widget (pay-with-widget)Nothing — MoonPay renders the entire flow, from amount entry to confirmation.The quickest path to a complete buy experience when you do not need a custom UI.
The key difference between the buy button and headless buy is the UI surface. The buy button renders a visible button and confirmation sheet for you. Headless buy renders no UI at all — you supply the button and confirmation screen and call setupBuy() when the customer confirms. Both run the same buy pipeline and emit the same events.

Prerequisites

  • A connected customer (via client.getConnection() or client.connect()). See Connect a customer.
  • A UI surface where you can render the buy button frame.
  • A destination wallet address for the purchased crypto.

Flow overview

1

Get a quote

Request a quote for the amount the customer wants to spend. Pass the destination wallet and the asset the customer will receive. You do not pass a payment method here — the customer picks one inside the buy button.Only quotes with executable: true can be used to execute a transaction. See the quotes API reference for the fields required to receive executable: true.
const quoteResult = await client.getQuote({
  source: { asset: { code: "USD" }, amount: "100.00" }, // The fiat currency and amount to pay
  destination: { asset: { code: "ETH" } }, // The crypto the customer will receive
  wallet: { address: "0x1234567890abcdef1234567890abcdef12345678" }, // The destination wallet address
});

if (!quoteResult.ok) {
  // Handle error
}

console.log(quoteResult.value);
Display the quote in your UI: source amount, destination amount, fees, and exchange rate. Monitor expiresAt and refresh the quote before it expires. If the button is already rendered, call buyButton.setQuote(newSignature) instead of re-creating it — see Refresh an expiring quote.
2

Render the buy button

Pass the quote signature and a container element to client.setupBuyButton(). The frame renders a payment button — card, Apple Pay, or Google Pay, depending on what’s available to the customer — into your container. On tap, the frame opens the confirmation sheet and runs the buy pipeline.For the frame URL, parameters, and events, see the buy button frame reference.
Render the buy button
import type { BuyButtonEvent } from "@moonpay/platform-sdk-web";

const buyButtonResult = await client.setupBuyButton({
  quote: quoteResult.value.signature, // The quote signature from getQuote

  container: document.querySelector("#buyButtonContainer"), // DOM element to render the button into

  onEvent: (event: BuyButtonEvent) => {
    switch (event.kind) {
      case "ready":
        // The button is rendered — hide any loading placeholder.
        break;

      case "complete": {
        const txn = event.payload.transaction;

        if (txn.status === "failed") {
          // The transaction failed. Show txn.failureReason to the customer.
          console.error(txn.failureReason);
          break;
        }

        // The transaction is executing. Track the final status by polling.
        pollTransaction(txn.id);
        break;
      }

      case "challenge":
        // Verification required. Render the challenge frame at the provided URL.
        // See: /platform/guides/handling-challenges
        openChallengeFrame(event.payload.url, buyButtonResult.value);
        break;

      case "error":
        // Surface to logs and tear down the frame.
        console.error(event.payload.code, event.payload.message);
        break;
    }
  },
});

if (!buyButtonResult.ok) {
  // Handle error setting up the buy button
  console.error(buyButtonResult.error.kind, buyButtonResult.error.message);
  return;
}

const buyButton = buyButtonResult.value;
Like setupBuy(), the buy button emits a ready event once the button is rendered and ready to tap. For Apple Pay and Google Pay, it fires after the customer’s device is confirmed to support the wallet.
3

Handle the events

onEvent receives BuyButtonEvent events as the pipeline progresses. Handle each kind:
  • ready — the button is rendered and ready for the customer to tap. Use it to hide any loading placeholder.
  • complete — the pipeline finished. The payload carries a transaction. Inspect its status first: when status is "failed", read failureReason and show it to the customer; otherwise pass transaction.id to getTransaction() to poll for the final status.
  • challenge — verification is required before the transaction can proceed. Render the challenge frame at the url from the payload. Do not construct the URL yourself.
  • error — the flow encountered an error. Log code and message, then tear down the frame. The message is for logs, not for display to customers.
4

Handle challenges

When the buy button emits a challenge event, the customer must complete one or more verification steps before the transaction can proceed. Set up the challenge frame with the URL from the event payload. For the full flow, see Handle challenges.The challenge frame is self-driving: after initialization, it sequences through all required verification steps, creates the transaction, and emits complete when the pipeline finishes.
Handle challenges
import type { ChallengeEvent } from "@moonpay/platform-sdk-web";

async function openChallengeFrame(challengeUrl: string, buyButton) {
  // The challenge URL does not include a channelId — append one before rendering.
  const url = new URL(challengeUrl);
  url.searchParams.set("channelId", crypto.randomUUID());

  const challengeResult = await client.setupChallenge({
    challengeUrl: url.toString(),
    container: document.querySelector("#challengeModal"),

    onEvent: (event: ChallengeEvent) => {
      switch (event.kind) {
        case "ready":
          // Challenge UI is rendered and visible
          break;

        case "complete":
          // All verification resolved, transaction complete.
          buyButton.dispose();
          pollTransaction(event.payload.transaction.id);
          break;

        case "cancelled":
          // Customer dismissed the challenge — allow retry.
          buyButton.dispose();
          showRetryOption();
          break;

        case "error":
          buyButton.dispose();
          console.error(event.payload.message);
          break;
      }
    },
  });
}
When you receive complete, cancelled, or error from the challenge frame, call buyButton.dispose() to also tear down the buy button.
The challenge frame handles all verification types automatically — KYC, Strong Customer Authentication (SCA), CVC re-entry, wallet ownership, micro-authorization, and 3D Secure (3DS). You never need to distinguish between them.
5

Track the transaction

When the buy button or challenge frame emits complete, the payload includes a transaction with an id and status. The transaction is created and payment is processing. Poll for the final status with getTransaction().
Track the transaction
async function pollTransaction(transactionId: string) {
  const terminal = new Set(["completed", "failed"]);

  while (true) {
    const res = await client.getTransaction(transactionId);

    if (!res.ok) throw new Error(res.error.message);
    if (terminal.has(res.value.data.status)) return res.value.data.status;

    await new Promise((r) => setTimeout(r, 3000));
  }
}
Webhook support is coming soon. Until then, use polling to track transaction status.

Refresh an expiring quote

Quotes expire. If the current quote expires before the customer taps the button, fetch a new one and call setQuote() with its signature instead of re-rendering the frame.
Refresh the quote
const newQuote = await client.getQuote({
  source: { asset: { code: "USD" }, amount: "100.00" },
  destination: { asset: { code: "ETH" } },
  wallet: { address: "0x1234567890abcdef1234567890abcdef12345678" },
});

if (newQuote.ok) {
  buyButton.setQuote(newQuote.value.signature);
}
When you no longer need the button, call buyButton.dispose() to unmount the frame. After you dispose it, no further events reach your onEvent callback.

Transaction statuses

Transactions have the following statuses:
  • Pending: The transaction has been initiated and the payment accepted. The assets are being transferred.
  • Complete: The transaction is finalized. The payment is complete and the assets have been delivered to their destination.
  • Failed: The transaction has failed. The payment was not executed and funds were not transferred.

Next steps

setupBuyButton() reference

Full method signature, parameters, events, and errors.

Buy button frame

Frame URL, parameters, and protocol messages.

Connect a customer

Connect a customer’s MoonPay account before you start a buy.

Handle challenges

Render the challenge frame when verification is required.