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

> Allow customers to buy crypto using stored credit and debit cards.

Use this guide to execute a card transaction after you have a [connected
customer](/platform/guides/connect-a-customer). You will list stored cards, add
new ones through a MoonPay-hosted frame, get a quote, and execute the
transaction — with MoonPay handling PCI-compliant card collection, payment
orchestration, and all verification challenges inside hosted frames.

See the [Going Live](/platform/overview/going-live) section for requirements you
must meet before taking this integration to production.

## Prerequisites

* A MoonPay account with card payments enabled. Contact your MoonPay account
  team to enable it.
* A connected customer (via `client.getConnection()` or `client.connect()`).
* A UI surface where you can render MoonPay frames (iframe on web, or
  [WebView](/platform/overview/requirements#webviews) on mobile).
* 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 ACF as Add Card frame
    participant BF as Buy frame
    participant CF as Challenge frame

    Note over C,CF: Prerequisite: customer is connected

    FE->>API: GET /platform/v1/payment-methods
    API-->>FE: { paymentMethodConfigs, paymentMethods }

    alt No stored cards
        FE->>ACF: Render Add Card frame
        ACF-->>FE: complete({ card: { id, brand, last4, ... } })
    end

    C->>FE: Selects card, enters amount
    FE->>API: POST /platform/v1/quotes/buy
    API-->>FE: { quote with signature }

    C->>FE: Confirms purchase
    FE->>BF: Render buy frame (signature, clientToken)

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

    FE->>API: GET /platform/v1/transactions/{id}
    Note over FE: Poll for final status
```

<Steps>
  <Step title="List payment methods">
    Fetch the customer's available payment method types and stored cards.

    <CodeGroup>
      ```ts List payment methods theme={null}
      const paymentMethodsResult = await client.getPaymentMethods();

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

      console.log(paymentMethodsResult.value.data);
      ```

      ```ts Result theme={null}
      {
        paymentMethodConfigs: [
          {
            type: "card",
            capabilities: {
              supportedCurrencies: ["USD", "EUR", "GBP"],
              supportedTransactionTypes: ["buy"],
              allowsDeletion: true,
              requiresWidget: false,
            },
            availability: { active: true },
          },
        ],
        paymentMethods: [
          {
            id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
            type: "card",
            brand: "visa",
            last4: "4242",
            expirationMonth: "12",
            expirationYear: "2027",
            cardType: "credit",
            availability: { active: true },
          },
        ],
      }
      ```
    </CodeGroup>

    The result contains two collections:

    * **`paymentMethodConfigs`** — available payment method types. Check for
      `type: "card"` to confirm card payments are available for this customer.
    * **`paymentMethods`** — the customer's stored cards. Each entry includes
      `brand`, `last4`, `expirationMonth`, `expirationYear`, `cardType`, and
      `availability`.

    Display active cards in your payment method picker. For inactive cards
    (`availability.active: false`), show the `reasons` value and prompt the user to
    add a new card.

    | `reasons` value | Suggested UX                                     |
    | --------------- | ------------------------------------------------ |
    | `card_expired`  | "This card has expired. Add a new card."         |
    | `card_blocked`  | "This card is no longer available."              |
    | `card_declined` | "This card can't be used. Try a different card." |

    <Tip>
      If `paymentMethods` is empty and `paymentMethodConfigs` includes `type:
              "card"`, guide the customer to add one using the Add Card frame (Step 2).
    </Tip>
  </Step>

  <Step title="Add a card">
    When the customer needs to add a new card, set up the Add Card frame. The frame
    collects card details and billing address inside a PCI-compliant MoonPay-hosted
    UI — card data never touches your domain. For the frame URL, size, and events,
    see the [Add Card frame](/platform/frames/add-card) reference.

    ```ts Add a card theme={null}
    import type { AddCardEvent } from "@moonpay/platform-sdk-web";

    const addCardResult = await client.setupAddCard({
      container: document.querySelector("#addCardContainer"),

      onEvent: (event: AddCardEvent) => {
        switch (event.kind) {
          case "ready":
            // Frame rendered — reveal the modal if it was hidden
            break;

          case "complete":
            // Card added. Use event.payload.card.id to get a quote in Step 3.
            console.log(event.payload.card);
            // { id, brand, last4, cardType, expirationMonth, expirationYear }
            break;

          case "error":
            console.error(event.payload.message);
            break;
        }
      },
    });

    if (!addCardResult.ok) {
      // Handle error setting up the Add Card frame
    }
    ```

    The `complete` event returns the new card's full details including its `id`. Use
    this `id` directly to get a quote in Step 3 — no need to re-fetch payment
    methods.
  </Step>

  <Step title="Get a quote">
    With a stored card selected, request a quote. Pass the card's `id` in
    `paymentMethod` so MoonPay can evaluate card-specific requirements.

    <CodeGroup>
      ```ts Get quote theme={null}
      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) {
        // 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..." },
        paymentMethod: { type: "card", id: "a1b2c3d4-..." },
        expiresAt: "2026-04-29T15:45:00Z",
        executable: true,
        signature: "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9..."
      }
      ```
    </CodeGroup>

    Display the quote in your buy confirmation screen: source amount, destination
    amount, fees, and exchange rate. Monitor `expiresAt` and refresh the quote
    before it expires. If the buy frame is already loaded, call
    `buyResult.value.setQuote(newSignature)` instead of re-creating the frame.
  </Step>

  <Step title="Execute the transaction">
    ### Headless buy frame

    Use `client.setupBuy()` when you want full control over your purchase UI. The
    frame is headless — no visible UI — and emits events for you to handle. For the
    frame URL, parameters, and events, see the [Buy frame](/platform/frames/buy)
    reference.

    ```ts setupBuy theme={null}
    import type { BuyEvent } from "@moonpay/platform-sdk-web";

    const buyResult = await client.setupBuy({
      quote: quoteResult.value.data.signature,

      onEvent: (event: BuyEvent) => {
        switch (event.kind) {
          case "ready":
            // Pipeline starting — show a loading indicator
            break;

          case "complete":
            // Transaction complete. Track status via polling.
            console.log(event.payload.transaction);
            // { id: "txn_01", status: "pending" }
            break;

          case "challenge":
            // Verification required — render the challenge frame
            openChallengeFrame(event.payload.url, buyResult);
            break;

          case "quoteExpired":
            // Fetch a new quote, then update the frame:
            // const newQuote = await client.getQuote({...});
            // event.payload.setQuote(newQuote.value.signature);
            break;

          case "error":
            console.error(event.payload.message);
            break;
        }
      },
    });

    if (!buyResult.ok) {
      // Handle error
    }
    ```
  </Step>

  <Step title="Handle challenges">
    When the buy frame 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 — do not construct the URL yourself.
    For the frame URL, parameters, and events, see the [Challenge
    frame](/platform/frames/challenge) reference.

    The 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,
      buyResult: SetupBuyResult,
    ) {
      // 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.
              buyResult.value.dispose();
              navigateToConfirmation(event.payload.transaction);
              break;

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

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

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

    The 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 frame or challenge frame emits `complete`, the payload includes
    `{ transaction: { id, status } }`. The transaction is created and payment is
    processing.

    ```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));
      }
    }
    ```

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

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

  <Step title="Delete a stored card (optional)">
    Let customers remove stored cards at any time.

    ```ts Delete a stored card theme={null}
    const deleteResult = await client.deletePaymentMethod(paymentMethodId);

    if (!deleteResult.ok) {
      // Handle error
    }
    ```

    * Deleting an already-deleted card returns success (idempotent).
    * Deleting a card with a pending transaction is rejected.
    * Only payment methods with `allowsDeletion: true` can be deleted.
  </Step>
</Steps>
