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

# Pay with the buy button

> Let customers buy crypto from a single express checkout button that handles payment-method selection and confirmation for you.

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](/platform/guides/connect-a-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](/platform/overview/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.

| Approach                                                           | What you build                                                                                  | Best 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`](/platform/guides/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()`](/platform/sdk-reference/web/setup-buy) 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](/platform/guides/connect-a-customer).
* A UI surface where you can render the [buy button frame](/platform/frames/buy-button).
* A destination wallet address for the purchased crypto.

## Flow overview

```mermaid theme={null}
sequenceDiagram
    autonumber
    actor C as Customer
    participant FE as Your frontend
    participant API as MoonPay API
    participant BB as Buy button frame
    participant CF as Challenge frame

    Note over C,CF: Prerequisite: customer is connected

    C->>FE: Enters amount
    FE->>API: getQuote(...)
    API-->>FE: { quote with signature }

    FE->>BB: setupBuyButton({ quote, container })
    Note over BB: Button renders into your container

    C->>BB: Taps the button, picks a method, confirms

    alt Happy path
        BB-->>FE: complete({ transaction: { id, status } })
    else Verification required
        BB-->>FE: challenge({ url })
        FE->>CF: Render challenge frame at URL
        CF-->>FE: complete({ transaction: { id, status } })
    end

    FE->>API: getTransaction(id)
    Note over FE: Poll for final status
```

<Steps>
  <Step title="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](/api-reference/platform/endpoints/quotes/get) for the
    fields required to receive `executable: true`.

    <CodeGroup>
      ```ts Get quote theme={null}
      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);
      ```

      ```ts Result theme={null}
      {
        source: {
          amount: "100.00",
          asset: { code: "USD" }
        },
        destination: {
          amount: "0.025",
          asset: { code: "ETH" }
        },
        fees: {
          network: { amount: "2.50", currencyCode: "USD" },
          moonpay: { amount: "3.99", currencyCode: "USD" }
        },
        wallet: { address: "0x1234..." },
        expiresAt: "2026-06-17T15:45:00Z",
        executable: true,
        signature: "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9..."
      }
      ```
    </CodeGroup>

    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](#refresh-an-expiring-quote).
  </Step>

  <Step title="Render the buy button">
    Pass the quote `signature` and a container element to
    [`client.setupBuyButton()`](/platform/sdk-reference/web/setup-buy-button). 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](/platform/frames/buy-button) reference.

    ```ts Render the buy button theme={null}
    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;
    ```

    <Note>
      Like [`setupBuy()`](/platform/sdk-reference/web/setup-buy), 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.
    </Note>
  </Step>

  <Step title="Handle the events">
    `onEvent` receives [`BuyButtonEvent`](/platform/sdk-reference/web/setup-buy-button#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()`](/platform/sdk-reference/web/get-transaction) to poll
      for the final status.
    * **`challenge`** — verification is required before the transaction can proceed.
      Render the [challenge frame](/platform/frames/challenge) 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.
  </Step>

  <Step title="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](/platform/guides/handling-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.

    ```ts Handle challenges theme={null}
    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;
          }
        },
      });
    }
    ```

    <Warning>
      When you receive `complete`, `cancelled`, or `error` from the challenge frame,
      call `buyButton.dispose()` to also tear down the buy button.
    </Warning>

    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.
  </Step>

  <Step title="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()`](/platform/sdk-reference/web/get-transaction).

    ```ts Track the transaction theme={null}
    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));
      }
    }
    ```

    <Note>
      Webhook support is coming soon. Until then, use polling to track transaction
      status.
    </Note>
  </Step>
</Steps>

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

```ts Refresh the quote theme={null}
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

<CardGroup cols={2}>
  <Card title="setupBuyButton() reference" href="/platform/sdk-reference/web/setup-buy-button">
    Full method signature, parameters, events, and errors.
  </Card>

  <Card title="Buy button frame" href="/platform/frames/buy-button">
    Frame URL, parameters, and protocol messages.
  </Card>

  <Card title="Connect a customer" href="/platform/guides/connect-a-customer">
    Connect a customer's MoonPay account before you start a buy.
  </Card>

  <Card title="Handle challenges" href="/platform/guides/handling-challenges">
    Render the challenge frame when verification is required.
  </Card>
</CardGroup>
