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.

Use this guide to execute a card transaction after you have a connected 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 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 on mobile).
  • A destination wallet address for the purchased crypto.

Flow overview

1

List payment methods

Fetch the customer’s available payment method types and stored cards.
const paymentMethodsResult = await client.getPaymentMethods();

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

console.log(paymentMethodsResult.value);
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 valueSuggested 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.”
If paymentMethods is empty and paymentMethodConfigs includes type: "card", guide the customer to add one using the Add Card frame (Step 2).
2

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 reference.
Add a card
import type { AddCardEvent } from "@moonpay/platform";

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

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.
const quoteResult = await client.getQuote({
  source: "USD",
  destination: "ETH",
  sourceAmount: "100.00",
  walletAddress: "0x1234567890abcdef1234567890abcdef12345678",
  paymentMethod: { type: "card", id: cardId },
});

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

console.log(quoteResult.value);
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.
4

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 reference.
setupBuy
import type { BuyEvent } from "@moonpay/platform";

const buyResult = await client.setupBuy({
  quote: quoteResult.value.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
}
5

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 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.
Handle challenges
import type { ChallengeEvent } from "@moonpay/platform";

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;
      }
    },
  });
}
When you receive complete, cancelled, or error from the challenge frame, call buyResult.value.dispose() to also tear down the buy frame.
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.
6

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.
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.status)) return res.value.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.
Webhook support is coming soon. Until then, use polling to track transaction status.
7

Delete a stored card (optional)

Let customers remove stored cards at any time.
Delete a stored card
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.