> ## 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 bank transfer

> Let customers buy crypto with a SEPA bank transfer and render the deposit details natively.

Use this guide to execute a bank-transfer transaction after you have a
[connected customer](/platform/guides/connect-a-customer). You get a quote,
execute it through the same headless [Buy frame](/platform/frames/buy) you use
for cards, then render the deposit details so the customer can send funds from
their bank. MoonPay runs the payment pipeline; you own the purchase UI and the
deposit screen.

Bank transfers support SEPA (EUR) for centralized assets. They don't cover DeFi
assets — offer a card or wallet method for those. 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 bank transfers 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 BF as Buy frame

    Note over C,BF: Prerequisite: customer is connected

    FE->>API: GET /platform/v1/payment-methods
    API-->>FE: { paymentMethodConfigs (includes sepa) }

    C->>FE: Enters amount, selects bank transfer
    FE->>API: POST /platform/v1/quotes/buy (paymentMethod.type: "sepa")
    API-->>FE: { quote, exchangeRateType: "floating" }

    C->>FE: Confirms purchase
    FE->>BF: Render buy frame (signature)
    BF-->>FE: complete({ transaction: { id, status: "pending" } })

    FE->>API: GET /platform/v1/transactions/{id}
    API-->>FE: { bankTransferDepositInfo }
    FE->>C: Render deposit details (IBAN, BIC, reference)

    Note over FE,C: Customer sends the transfer (minutes to days), including the reference
    loop Poll until terminal status
        FE->>API: GET /platform/v1/transactions/{id}
    end
    Note over FE,API: Or subscribe to the transaction-updated webhook
```

<Steps>
  <Step title="Confirm bank transfer is available">
    Fetch the customer's available payment method types and check for `sepa`.

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

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

      const sepaAvailable = paymentMethodsResult.value.data.paymentMethodConfigs.some(
        (config) =>
          config.type === "sepa" &&
          config.availability.active &&
          !config.capabilities.requiresWidget,
      );
      ```

      ```ts Result theme={null}
      {
        paymentMethodConfigs: [
          {
            type: "sepa",
            capabilities: {
              supportedCurrencies: ["EUR"],
              supportedTransactionTypes: ["buy"],
              allowsDeletion: false,
              requiresWidget: false,
            },
            availability: { active: true },
          },
        ],
        paymentMethods: [],
      }
      ```
    </CodeGroup>

    Show bank transfer in your payment method picker only when a `sepa` config is
    present, `availability.active` is `true`, and `capabilities.requiresWidget` is
    `false`. A `requiresWidget: true` bank transfer isn't eligible for the headless
    flow and must complete in the MoonPay widget instead.

    SEPA doesn't create a stored payment method, so it never appears in
    `paymentMethods`. That array still lists the customer's stored cards, so it can
    be non-empty even when the customer pays by bank transfer.
  </Step>

  <Step title="Get a bank-transfer quote">
    Request a quote with `paymentMethod.type` set to `"sepa"`.

    <CodeGroup>
      ```ts Get quote theme={null}
      const quoteResult = await client.getQuote({
        source: { asset: { code: "EUR" }, amount: "100.00" },
        destination: { asset: { code: "ETH" } },
        wallet: { address: "0x1234567890abcdef1234567890abcdef12345678" },
        paymentMethod: { type: "sepa" },
      });

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

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

      ```ts Result theme={null}
      {
        source: { amount: "100.00", asset: { code: "EUR" } },
        destination: { amount: "0.0234", asset: { code: "ETH" } },
        fees: {
          network: { amount: "1.20", currencyCode: "EUR" },
          moonpay: { amount: "3.50", currencyCode: "EUR" }
        },
        wallet: { address: "0x1234..." },
        paymentMethod: { type: "sepa" },
        expiresAt: "2026-04-29T15:45:00Z",
        executable: true,
        exchangeRate: "4273.50",
        exchangeRateType: "floating",
        signature: "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9..."
      }
      ```
    </CodeGroup>

    Bank transfers are a floating payment method: the quote returns
    `exchangeRateType: "floating"` and the crypto amount is an estimate until the
    customer's funds settle. Render the estimated amount with a tilde (for example,
    `~0.0234 ETH`) and tell the customer the final amount is set at settlement. See
    [Exchange rate type](/platform/sdk-reference/web/get-quote#exchange-rate-type).

    Show the contextual quote summary and fee breakdown before the customer
    confirms. See [Going Live](/platform/overview/going-live#quote-presentation) for
    the canonical requirements. Monitor `expiresAt` and refresh the quote before it
    expires.
  </Step>

  <Step title="Execute the transaction">
    Call `client.setupBuy()` with the quote signature, exactly as you do for cards.
    The headless frame creates the transaction and emits `complete` with the
    transaction `id`. 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,
      container: document.querySelector("#buyContainer"),

      onEvent: (event: BuyEvent) => {
        switch (event.kind) {
          case "complete":
            if (event.payload.transaction.status !== "failed") {
              // Read the deposit details and render them natively (Step 4).
              showDepositDetails(event.payload.transaction.id);
            }
            break;

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

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

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

    A bank transfer can still require a challenge, such as a KYC step-up. Handle it
    exactly as for cards, but route the result back into the deposit flow: the
    Challenge frame creates the transaction and emits it on `complete`, so call
    `showDepositDetails` with that transaction `id`. See
    [Handle challenges](/platform/guides/handling-challenges).

    ```ts Challenge handling theme={null}
    import type { ChallengeEvent } from "@moonpay/platform-sdk-web";

    async function openChallengeFrame(
      challengeUrl: string,
      buyResult: SetupBuyResult,
    ) {
      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 "complete":
              // The challenge frame created the transaction; read the deposit details.
              showDepositDetails(event.payload.transaction.id);
              teardown();
              break;
            case "cancelled":
            case "error":
              teardown();
              break;
          }
        },
      });

      // Dispose both frame handles once the flow reaches a terminal state.
      function teardown() {
        if (challengeResult.ok) challengeResult.value.dispose();
        buyResult.value.dispose();
      }
    }
    ```
  </Step>

  <Step title="Read and render the deposit details">
    Call `client.getTransaction()` with the `id`. For a bank transfer, the response
    includes a `bankTransferDepositInfo` object with the account details and the
    payment reference, plus the exact fiat amount to transfer in `source.amount` (the
    gross amount, including fees). Render these fields in your own UI so the customer
    can pay from their banking app. MoonPay does not render the deposit UI.

    ```ts Read deposit details theme={null}
    async function showDepositDetails(transactionId: string) {
      const result = await client.getTransaction(transactionId);

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

      const transaction = result.value.data;

      renderBankTransferScreen({
        depositInfo: transaction.bankTransferDepositInfo,
        // source.amount is the exact fiat amount, including fees, the customer transfers.
        amount: transaction.source.amount,
        currency: transaction.source.asset.code,
      });
    }
    ```

    `bankTransferDepositInfo` contains:

    | Field              | Type     | Required | Description                                                        |
    | ------------------ | -------- | -------- | ------------------------------------------------------------------ |
    | `reference`        | `string` | ✅        | The payment reference the customer must include with the transfer. |
    | `recipientName`    | `string` | ✅        | The name of the recipient that receives the funds.                 |
    | `recipientAddress` | `string` | ✅        | The address of the recipient.                                      |
    | `iban`             | `string` |          | The IBAN. Present for SEPA (EUR).                                  |
    | `bic`              | `string` |          | The BIC / SWIFT code. Present for SEPA (EUR).                      |
    | `bankName`         | `string` |          | The name of the receiving bank.                                    |
    | `bankAddress`      | `string` |          | The address of the receiving bank.                                 |

    <Warning>
      The customer must include the payment `reference` with their bank transfer.
      Transfers sent without it are rejected. Surface it prominently, for example,
      "Always include your payment reference or your transfer will be rejected."
      MoonPay uses the reference to match the incoming transfer to this transaction.
    </Warning>
  </Step>

  <Step title="Track the transaction">
    A bank transfer can take from a few minutes to a few days, and the transaction
    stays `pending` until settlement. While it's pending, the transaction's
    `destination.amount` is `0`, so keep rendering the estimate from the original
    quote's `destination.amount` with a tilde, and make clear the crypto amount is
    only an estimate until the transfer lands. After settlement, switch to the
    transaction's final `destination.amount`.

    Poll `client.getTransaction()` to track status. Because settlement can run for
    days, you can also subscribe to the
    [`transaction-updated`](/api-reference/widget/webhooks/transaction-updated)
    webhook instead of long-lived polling.

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

    Bank-transfer transactions have the following statuses:

    * **Pending:** The transaction is waiting for the customer's deposit, or the
      deposit has arrived and is being processed.
    * **Complete:** The funds have been received and the crypto delivered to the
      destination wallet.
    * **Failed:** The transfer timed out or was cancelled.

    The `status` stays `pending` from the moment the transaction is created until it
    settles, so it doesn't tell you whether the deposit has arrived yet. For
    finer-grained progress, read the `stages` array: each stage has a `kind` and a
    `status`. When the `waiting_payment` stage's `status` is `"success"`, the
    customer's deposit has arrived. Use that to move your UI from awaiting-transfer
    to processing before the transaction reaches a terminal status.

    <Callout icon="circle-info" iconType="regular">
      Requotes and cancellations for bank transfers are handled by MoonPay's hosted
      flow, not the headless flow. If the settled amount differs from the estimate,
      the customer is emailed and completes the requote there. A cancellation
      surfaces to your app as a `failed` transaction when you poll
      `client.getTransaction()`.
    </Callout>

    <Note>
      Polling is the documented way to track status. If you already consume MoonPay
      webhooks, you can also use the existing
      [`transaction-updated`](/api-reference/widget/webhooks/transaction-updated)
      webhook. See the [webhooks overview](/api-reference/widget/webhooks/overview).
    </Note>
  </Step>
</Steps>
