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

# client.setupBuy()

> Mount the headless Buy frame and execute a transaction from a quote.

Mount the headless [Buy frame](/platform/frames/buy) in your app and execute a transaction from a quote signature. The frame renders no visible UI — the provider mounts it as a zero-dimension `react-native-webview` that drives the buy pipeline in the background and emits events you handle from your own purchase screen.

<Callout icon="circle-info" iconType="regular">
  The Buy frame is headless. When it emits a `challenge` event, render a
  separate challenge frame with
  [`client.setupChallenge()`](/platform/sdk-reference/react-native/setup-challenge)
  at the URL from the event payload. See the [Handle
  challenges](/platform/guides/handling-challenges) guide for the full flow.
</Callout>

```tsx Setup buy focus={5-40} theme={null}
import { useMoonPay, type BuyEvent } from "@moonpay/platform-sdk-react-native";

export function BuyScreen({ quoteSignature }: { quoteSignature: string }) {
  const { client } = useMoonPay();

  const start = async () => {
    const buyResult = await client.setupBuy({
      quote: quoteSignature,
      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

| Property                | Type                        | Required | Description                                                                                       |
| ----------------------- | --------------------------- | -------- | ------------------------------------------------------------------------------------------------- |
| `quote`                 | `string`                    | ✅        | The quote `signature` returned from [`getQuote`](/platform/sdk-reference/react-native/get-quote). |
| `externalTransactionId` | `string`                    |          | Your own identifier for the transaction. Stored on the MoonPay transaction for correlation.       |
| `onEvent`               | `(event: BuyEvent) => void` |          | Callback invoked for buy flow events. See [`BuyEvent`](#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.

| kind          | Payload                                                      | When you receive it                                                                                                                                                             |
| ------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `"ready"`     | —                                                            | The buy pipeline has started. Show a loading state in your own UI.                                                                                                              |
| `"complete"`  | `{ transaction: `[`FrameTransaction`](#frametransaction)` }` | The transaction was created. Use the transaction `id` to poll for final status.                                                                                                 |
| `"challenge"` | [`BuyChallengePayload`](#buychallengepayload)                | Verification is required before the transaction can proceed. Render the [challenge frame](/platform/guides/handling-challenges) at the provided `url` using `setupChallenge()`. |
| `"error"`     | [`BuyEventError`](#buyeventerror)                            | The 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()`](/platform/sdk-reference/react-native/get-transaction) to poll for the final status.

| Field           | Type     | Required | Description                                                                                                                   |
| --------------- | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `status`        | `string` | ✅        | The transaction status. On the failure variant, `"failed"`.                                                                   |
| `id`            | `string` |          | Required on the non-failure variant; optional when `status` is `"failed"` (a transaction may not exist yet on early failure). |
| `failureReason` | `string` |          | Present only on the failure variant (`status === "failed"`). A developer-friendly reason.                                     |

#### `BuyChallengePayload`

| Field  | Type     | Required | Description                                                                                                                          |
| ------ | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `kind` | `string` | ✅        | The challenge type (currently always `"frame"`, but typed as `string` for forward compatibility).                                    |
| `url`  | `string` | ✅        | A fully-formed URL to pass directly to [`setupChallenge()`](/platform/sdk-reference/react-native/setup-challenge). Do not modify it. |

#### `BuyEventError`

| Field     | Type     | Required | Description                                                                                          |
| --------- | -------- | -------- | ---------------------------------------------------------------------------------------------------- |
| `code`    | `string` | ✅        | The error category. Includes `configurationError`, `invalidQuote`, and backend-specific error codes. |
| `message` | `string` | ✅        | Developer-friendly details. Not intended to be rendered in UI.                                       |

## Result

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

### Result envelope

`Result<BuyFrame, SetupBuyError>`

| Field   | Type                              | Required | Description                      |
| ------- | --------------------------------- | -------- | -------------------------------- |
| `ok`    | `boolean`                         | ✅        | Whether the operation succeeded. |
| `value` | [`BuyFrame`](#buyframe)           |          | Present when `ok` is `true`.     |
| `error` | [`SetupBuyError`](#setupbuyerror) |          | Present when `ok` is `false`.    |

### `BuyFrame`

| Field      | Type                          | Required | Description                                                                                                                                                                     |
| ---------- | ----------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `setQuote` | `(signature: string) => void` | ✅        | Updates 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`  | `() => void`                  | ✅        | Unmounts 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`

| Field     | Type                                       | Required | Description                 |
| --------- | ------------------------------------------ | -------- | --------------------------- |
| `kind`    | `"configurationError"` \| `"genericError"` | ✅        | The error category.         |
| `message` | `string`                                   | ✅        | Developer-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.

```tsx Buy with a stored card theme={null}
import {
  useMoonPay,
  type BuyEvent,
  type ChallengeEvent,
} from "@moonpay/platform-sdk-react-native";

export function CardCheckout({ cardId }: { cardId: string }) {
  const { client } = useMoonPay();

  const start = async () => {
    // 1. Get a quote for the selected stored card.
    const quoteResult = await client.getQuote({
      source: { asset: { code: "USD" }, amount: "100.00" },
      destination: { asset: { code: "ETH" } },
      wallet: { address: "0x1234567890abcdef1234567890abcdef12345678" },
      paymentMethod: { type: "card", id: cardId },
    });
    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,
      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,
        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](/platform/guides/pay-with-card) guide. For details on the challenge flow, see [Handle challenges](/platform/guides/handling-challenges).

<Accordion title="TS Definitions">
  ```ts types.ts theme={null}
  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;
  };
  ```
</Accordion>
