> ## 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](https://dev.moonpay.com/platform/guides/connect-a-customer.md). You get a quote,
execute it through the same [headless buy
button](https://dev.moonpay.com/platform/frames/buy-button.md#headless-mode) 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](https://dev.moonpay.com/platform/overview/going-live.md) 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](https://dev.moonpay.com/platform/overview/requirements.md#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 button (headless)

    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 headless buy button (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
```

**1. Confirm bank transfer is available**

Fetch the customer's available payment method types and check for `sepa`.

```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: [],
}
```

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.

**2. Get a bank-transfer quote**

Request a quote with `paymentMethod.type` set to `"sepa"`.

```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..."
}
```

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](https://dev.moonpay.com/platform/sdk-reference/web/get-quote.md#exchange-rate-type).

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

**3. Execute the transaction**

Call `client.setupBuyButton()` with the quote signature and `headless: true`,
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 [headless mode](https://dev.moonpay.com/platform/frames/buy-button.md#headless-mode) in the
Buy Button frame reference.

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

const buyResult = await client.setupBuyButton({
  quote: quoteResult.value.data.signature,
  container: document.querySelector("#buyContainer"),
  headless: true,

  onEvent: (event: BuyButtonEvent) => {
    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":
        if (event.payload.code === "quoteExpired") {
          // Fetch a new quote, then update the frame. It retries the purchase.
          // const newQuote = await client.getQuote({...});
          // buyResult.value.setQuote(newQuote.value.data.signature);
          break;
        }
        console.error(event.payload.message);
        buyResult.value.dispose();
        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](https://dev.moonpay.com/platform/guides/handling-challenges.md).

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

async function openChallengeFrame(
  challengeUrl: string,
  buyResult: SetupBuyButtonResult,
) {
  const url = new URL(challengeUrl);
  url.searchParams.set("channelId", crypto.randomUUID());

  const challengeResult = await client.setupChallenge({
    url: 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();
  }
}
```

**4. 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.

**5. 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 a floating estimate, the same estimate the quote
returns, so you don't need to retain the original quote. Render
`destination.amount` with a tilde and make clear the crypto amount is only an
estimate until the transfer lands. At settlement it becomes the final amount
delivered.

Poll `client.getTransaction()` to track status. Because settlement can run for
days, you can also subscribe to the
[`transaction-updated`](https://dev.moonpay.com/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.

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()`.

> [!NOTE]
>
> Polling is the documented way to track status. If you already consume MoonPay
> webhooks, you can also use the existing
> [`transaction-updated`](https://dev.moonpay.com/api-reference/widget/webhooks/transaction-updated)
> webhook. See the [webhooks overview](https://dev.moonpay.com/api-reference/widget/webhooks/overview.md).
