> For a complete page index, fetch https://docs.transak.com/llms.txt

# How to use Payment Methods: Bank Transfer

In this tutorial guide, we will walk through placing a order using payment method `bank transfer` using Transak's Whitelabel APIs.

## What We'll Build

A full bank transfer checkout flow: <br />
`quote → virtual account → order → confirmation`.

| API                                                                        | Purpose                                                                |
| -------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| `GET /api/v2/lookup/quotes`                                                | Get a quote scoped to `sepa_bank_transfer` or `gbp_bank_transfer`      |
| `POST /api/whitelabel/payments/bank-transfer/create-virtual-account`       | Create a virtual bank account (VBA) to receive the user's transfer     |
| `GET /api/whitelabel/payments/bank-transfer/get-payment-instrument-status` | Poll until the VBA is `COMPLETED`                                      |
| `GET /api/whitelabel/payments/bank-transfer/get-vasps`                     | *(Custodial wallets only)* Fetch VASP list for the destination address |
| `POST /api/whitelabel/payments/bank-transfer/create-order`                 | Create the order against the confirmed VBA                             |
| `POST /api/whitelabel/payments/bank-transfer/confirm-payment`              | Confirm the payment and complete the order flow                        |

## Authentication Headers

These APIs support both onboarding paths from [Authentication APIs](/guides/how-to-use-authentication-apis). The headers you pass depend on which path your integration uses.

Pass these headers on every bank transfer API call when your platform owns the user's authentication:

| Header              | Description                            |
| ------------------- | -------------------------------------- |
| `x-user-identifier` | The authenticated user's email address |
| `x-access-token`    | Your Partner Access Token              |

Pass this header on every bank transfer API call when Transak owns the user's authentication:

| Header          | Description                                                |
| --------------- | ---------------------------------------------------------- |
| `Authorization` | The `accessToken` returned from `POST /api/v2/auth/verify` |

## Step 1 — Get a Bank Transfer Quote

Bank transfer orders use the same quote endpoint as other payment methods. Pass `paymentMethod` as either `sepa_bank_transfer` (for EUR) or `gbp_bank_transfer` (for GBP). The returned `quoteId` must be passed to every subsequent API call.

Choose the payment method that matches the user's currency.

```bash title="SEPA (EUR)"
curl "https://api-gateway-stg.transak.com/api/v2/lookup/quotes\
?fiatCurrency=EUR\
&cryptoCurrency=ETH\
&isBuyOrSell=BUY\
&fiatAmount=100\
&network=ethereum\
&paymentMethod=sepa_bank_transfer" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "x-user-ip: USER_IP_ADDRESS"
```

```bash title="GBP Bank Transfer"
curl "https://api-gateway-stg.transak.com/api/v2/lookup/quotes\
?fiatCurrency=GBP\
&cryptoCurrency=ETH\
&isBuyOrSell=BUY\
&fiatAmount=100\
&network=ethereum\
&paymentMethod=gbp_bank_transfer" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "x-user-ip: USER_IP_ADDRESS"
```

Save the `quoteId` immediately — it is required for every subsequent step.

```ts
const { quoteId, cryptoAmount, totalFee, feeBreakdown, expiresAt } = data;
setQuoteId(quoteId);
```

Display `cryptoAmount`, `totalFee`, and `feeBreakdown` on a confirmation screen before proceeding.

```ts
// feeBreakdown: [{ name: "Network fee", value: 1.20 }, { name: "Transaction fee", value: 0.80 }]
```

```ts title="getBankTransferQuote.ts"
const BASE = "https://api-gateway-stg.transak.com";

interface Quote {
  quoteId: string;
  cryptoAmount: number;
  fiatAmount: number;
  totalFee: number;
  feeBreakdown: { id: string; name: string; value: number }[];
  expiresAt: string;
}

async function getBankTransferQuote(params: {
  fiatCurrency: "EUR" | "GBP";
  cryptoCurrency: string;
  fiatAmount: number;
  network: string;
  apiKey: string;
  userIp: string;
}): Promise<Quote> {
  const paymentMethod =
    params.fiatCurrency === "GBP" ? "gbp_bank_transfer" : "sepa_bank_transfer";

  const qs = new URLSearchParams({
    fiatCurrency: params.fiatCurrency,
    cryptoCurrency: params.cryptoCurrency,
    isBuyOrSell: "BUY",
    fiatAmount: String(params.fiatAmount),
    network: params.network,
    paymentMethod,
  });

  const res = await fetch(`${BASE}/api/v2/lookup/quotes?${qs}`, {
    headers: { "x-api-key": params.apiKey, "x-user-ip": params.userIp },
  });
  const json = await res.json();
  if (!res.ok) throw new Error(json.message ?? `HTTP ${res.status}`);
  return json.data as Quote;
}
```

## Step 2 — Create a Virtual Bank Account

A Virtual Bank Account (VBA) is a unique bank account number Transak generates for this transaction. Your user transfers fiat to that account, and Transak detects the payment automatically.

Pass only the `quoteId` from Step 1 in the request body.

```bash
curl -X POST https://api-gateway-stg.transak.com/api/whitelabel/payments/bank-transfer/create-virtual-account \
  -H "x-api-key: YOUR_API_KEY" \
  -H "x-user-ip: USER_IP_ADDRESS" \
  -H "Authorization: USER_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "quoteId": "QUOTE_ID"
  }'
```

The response returns a `status` and a `message`. A status of `INITIATED` means the VBA setup has started successfully.

```ts
const { status, message } = data;
// status: "INITIATED" | "COMPLETED" | "FAILED"
```

```ts title="createVirtualAccount.ts"
const BASE = "https://api-gateway-stg.transak.com";

interface VirtualAccountResponse {
  status: "INITIATED" | "COMPLETED" | "FAILED";
  message: string;
}

async function createVirtualAccount(params: {
  quoteId: string;
  apiKey: string;
  userIp: string;
  accessToken: string;
}): Promise<VirtualAccountResponse> {
  const res = await fetch(
    `${BASE}/api/whitelabel/payments/bank-transfer/create-virtual-account`,
    {
      method: "POST",
      headers: {
        "x-api-key": params.apiKey,
        "x-user-ip": params.userIp,
      },
      body: JSON.stringify({ quoteId: params.quoteId }),
    }
  );
  const json = await res.json();
  if (!res.ok) throw new Error(json.message ?? `HTTP ${res.status}`);
  return json.data as VirtualAccountResponse;
}
```

## Step 3 — Poll Payment Instrument Status

After creating the VBA, poll this endpoint until the status is `COMPLETED`. Only then should you call Create Order.

Do not create the order before the payment instrument status is `COMPLETED`. Creating an order against an unconfirmed VBA will fail.

Pass `paymentInstrumentId` (either `sepa_bank_transfer` or `gbp_bank_transfer` — the same payment method from Step 1) and the `quoteId` from Step 1.

```bash
curl "https://api-gateway-stg.transak.com/api/whitelabel/payments/bank-transfer/get-payment-instrument-status\
?paymentInstrumentId=sepa_bank_transfer\
&quoteId=QUOTE_ID" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "x-user-ip: USER_IP_ADDRESS" \
```

The status is nested under the payment method key inside `paymentInstruments`.

```ts
const instruments = data.paymentInstruments;
const status = instruments["sepa_bank_transfer"]?.status;
// or instruments["gbp_bank_transfer"]?.status
```

| Status      | Meaning                  | Action                        |
| ----------- | ------------------------ | ----------------------------- |
| `INITIATED` | VBA setup is in progress | Keep polling                  |
| `COMPLETED` | VBA is ready             | Proceed to Step 4 / Step 5    |
| `FAILED`    | VBA setup failed         | Show error, ask user to retry |

```ts title="pollPaymentInstrumentStatus.ts"
const BASE = "https://api-gateway-stg.transak.com";

type PaymentInstrumentId = "sepa_bank_transfer" | "gbp_bank_transfer";

async function pollPaymentInstrumentStatus(params: {
  paymentInstrumentId: PaymentInstrumentId;
  quoteId: string;
  apiKey: string;
  userIp: string;
  accessToken: string;
  intervalMs?: number;
  timeoutMs?: number;
}): Promise<void> {
  const { intervalMs = 3000, timeoutMs = 120_000 } = params;
  const deadline = Date.now() + timeoutMs;

  while (Date.now() < deadline) {
    const qs = new URLSearchParams({
      paymentInstrumentId: params.paymentInstrumentId,
      quoteId: params.quoteId,
    });
    const res = await fetch(
      `${BASE}/api/whitelabel/payments/bank-transfer/get-payment-instrument-status?${qs}`,
      {
        headers: {
          "x-api-key": params.apiKey,
          "x-user-ip": params.userIp,
        },
      }
    );
    const json = await res.json();
    if (!res.ok) throw new Error(json.message ?? `HTTP ${res.status}`);

    const status: string =
      json.data.paymentInstruments[params.paymentInstrumentId]?.status;

    if (status === "COMPLETED") return;
    if (status === "FAILED") throw new Error("Virtual account setup failed");

    await new Promise(resolve => setTimeout(resolve, intervalMs));
  }

  throw new Error("Timed out waiting for payment instrument to be COMPLETED");
}
```

## Step 4 — Get VASPs (Custodial Wallets Only)

Skip this step if your user is sending crypto to a **self-custodial wallet**. This step is only required when the destination is a **custodial wallet** (e.g. an exchange account).

Regulatory travel-rule requirements mean Transak needs to know the Virtual Asset Service Provider (VASP) associated with the destination custodial address. Fetch the list and let the user select their VASP.

```bash
curl "https://api-gateway-stg.transak.com/api/whitelabel/payments/bank-transfer/get-vasps" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "x-user-ip: USER_IP_ADDRESS" \
```

Display the returned VASP list. Store the selected `vaspId` — you will pass it when creating the order.

```ts
const vasps = data.vasps;
// [{ vaspId: "binance", name: "Binance", ... }, ...]
setSelectedVaspId(vasps[0].vaspId);
```

```ts title="getVasps.ts"
const BASE = "https://api-gateway-stg.transak.com";

interface Vasp {
  vaspId: string;
  name: string;
}

async function getVasps(params: {
  apiKey: string;
  userIp: string;
  accessToken: string;
}): Promise<Vasp[]> {
  const res = await fetch(
    `${BASE}/api/whitelabel/payments/bank-transfer/get-vasps`,
    {
      headers: {
        "x-api-key": params.apiKey,
        "x-user-ip": params.userIp,
        Authorization: params.accessToken,
      },
    }
  );
  const json = await res.json();
  if (!res.ok) throw new Error(json.message ?? `HTTP ${res.status}`);
  return json.data.vasps as Vasp[];
}
```

## Step 5 — Create Order

Once the payment instrument status is `COMPLETED`, call the Create Order API.
Pass the `quoteId` from Step 1, the user's `walletAddress`, and `paymentInstrumentId` (`sepa_bank_transfer` or `gbp_bank_transfer`).

`paymentInstrumentId` is the literal string `sepa_bank_transfer` or `gbp_bank_transfer` — the same payment method chosen in Step 1.

```bash
curl -X POST https://api-gateway-stg.transak.com/api/whitelabel/payments/bank-transfer/create-order \
  -H "x-api-key: YOUR_API_KEY" \
  -H "x-user-ip: USER_IP_ADDRESS" \
  -d '{
    "quoteId": "QUOTE_ID",
    "walletAddress": "0xUSER_WALLET_ADDRESS",
    "paymentInstrumentId": "sepa_bank_transfer"
  }'
```

```ts title="createOrder.ts"
const BASE = "https://api-gateway-stg.transak.com";

type PaymentInstrumentId = "sepa_bank_transfer" | "gbp_bank_transfer";

interface PaymentDetailField {
  name: string;
  value: string;
}

interface CreateOrderResponse {
  orderId: string;
  status: string;
  paymentDetails: {
    name: string;
    fields: PaymentDetailField[];
  }[];
}

async function createBankTransferOrder(params: {
  quoteId: string;
  walletAddress: string;
  paymentInstrumentId: PaymentInstrumentId;
  vaspId?: string;
  apiKey: string;
  userIp: string;
  accessToken: string;
}): Promise<CreateOrderResponse> {
  const body: Record<string, string> = {
    quoteId: params.quoteId,
    walletAddress: params.walletAddress,
    paymentInstrumentId: params.paymentInstrumentId,
  };
  if (params.vaspId) body.vaspId = params.vaspId;

  const res = await fetch(
    `${BASE}/api/whitelabel/payments/bank-transfer/create-order`,
    {
      method: "POST",
      headers: {
        "x-api-key": params.apiKey,
        "x-user-ip": params.userIp,
      },
      body: JSON.stringify(body),
    }
  );
  const json = await res.json();
  if (!res.ok) throw new Error(json.message ?? `HTTP ${res.status}`);
  return json.data as CreateOrderResponse;
}
```

## Step 6 — Confirm Payment

The final step confirms the payment and closes the order flow. Call this immediately after creating the order.

```bash
curl -X POST https://api-gateway-stg.transak.com/api/whitelabel/payments/bank-transfer/confirm-payment \
  -H "x-api-key: YOUR_API_KEY" \
  -H "x-user-ip: USER_IP_ADDRESS" \
  -d '{
    "orderId": "ORDER_ID"
  }'
```

On success, display the `orderId` and instruct the user to complete their bank transfer to the VBA account details shown in Step 2. Transak will detect the incoming payment and process the crypto release.

```ts
const { orderId, status } = data;
// Show order confirmation UI
```

```ts title="confirmPayment.ts"
const BASE = "https://api-gateway-stg.transak.com";

async function confirmBankTransferPayment(params: {
  orderId: string;
  apiKey: string;
  userIp: string;
  accessToken: string;
}): Promise<{ orderId: string; status: string }> {
  const res = await fetch(
    `${BASE}/api/whitelabel/payments/bank-transfer/confirm-payment`,
    {
      method: "POST",
      headers: {
        "x-api-key": params.apiKey,
        "x-user-ip": params.userIp,
      },
      body: JSON.stringify({ orderId: params.orderId }),
    }
  );
  const json = await res.json();
  if (!res.ok) throw new Error(json.message ?? `HTTP ${res.status}`);
  return json.data;
}
```

## Full Flow (Putting It Together)

```ts title="hooks/useBankTransferOrder.ts"
import { useState } from "react";

const BASE = "https://api-gateway-stg.transak.com";

type FlowStep =
  | "idle"
  | "quote"
  | "create_vba"
  | "polling_vba"
  | "vasp"
  | "create_order"
  | "confirm"
  | "done"
  | "error";

interface FlowState {
  step: FlowStep;
  quoteId: string;
  paymentInstrumentId: string;
  bankDetails: Record<string, string>;
  orderId: string;
  error: string | null;
}

export function useBankTransferOrder(
  apiKey: string,
  userIp: string,
  accessToken: string,
  isCustodialWallet: boolean
) {
  const [state, setState] = useState<FlowState>({
    step: "idle",
    quoteId: "",
    paymentInstrumentId: "",
    bankDetails: {},
    orderId: "",
    error: null,
  });

  const headers = (extra?: Record<string, string>) => ({
    "x-api-key": apiKey,
    "x-user-ip": userIp,
    Authorization: accessToken,
    "Content-Type": "application/json",
    ...extra,
  });

  async function fetchQuote(fiatCurrency: "EUR" | "GBP", cryptoCurrency: string, fiatAmount: number, network: string) {
    setState(s => ({ ...s, step: "quote", error: null }));
    const pm = fiatCurrency === "GBP" ? "gbp_bank_transfer" : "sepa_bank_transfer";
    const qs = new URLSearchParams({ fiatCurrency, cryptoCurrency, isBuyOrSell: "BUY", fiatAmount: String(fiatAmount), network, paymentMethod: pm });
    const res = await fetch(`${BASE}/api/v2/lookup/quotes?${qs}`, { headers() { return { "x-api-key": apiKey, "x-user-ip": userIp }; } });
    const json = await res.json();
    if (!res.ok) throw new Error(json.message);
    setState(s => ({ ...s, quoteId: json.data.quoteId, step: "create_vba" }));
    return json.data;
  }

  async function createVirtualAccount(walletAddress: string, network: string) {
    setState(s => ({ ...s, step: "create_vba", error: null }));
    const res = await fetch(`${BASE}/api/whitelabel/payments/bank-transfer/create-virtual-account`, {
      method: "POST",
      headers: headers(),
      body: JSON.stringify({ quoteId: state.quoteId, walletAddress, network }),
    });
    const json = await res.json();
    if (!res.ok) throw new Error(json.message);
    setState(s => ({ ...s, paymentInstrumentId: json.data.paymentInstrumentId, bankDetails: json.data.bankDetails, step: "polling_vba" }));
  }

  async function pollVbaStatus() {
    const deadline = Date.now() + 120_000;
    while (Date.now() < deadline) {
      const res = await fetch(
        `${BASE}/api/whitelabel/payments/bank-transfer/get-payment-instrument-status?paymentInstrumentId=${state.paymentInstrumentId}`,
        { headers: headers() }
      );
      const json = await res.json();
      if (!res.ok) throw new Error(json.message);
      if (json.data.status === "COMPLETED") {
        setState(s => ({ ...s, step: isCustodialWallet ? "vasp" : "create_order" }));
        return;
      }
      if (json.data.status === "FAILED") throw new Error("Virtual account setup failed");
      await new Promise(r => setTimeout(r, 3000));
    }
    throw new Error("Timed out waiting for VBA");
  }

  async function createOrder(walletAddress: string, vaspId?: string) {
    setState(s => ({ ...s, step: "create_order", error: null }));
    const body: Record<string, string> = { quoteId: state.quoteId, walletAddress, paymentInstrumentId: state.paymentInstrumentId };
    if (vaspId) body.vaspId = vaspId;
    const res = await fetch(`${BASE}/api/whitelabel/payments/bank-transfer/create-order`, {
      method: "POST",
      headers: headers(),
      body: JSON.stringify(body),
    });
    const json = await res.json();
    if (!res.ok) throw new Error(json.message);
    setState(s => ({ ...s, orderId: json.data.orderId, step: "confirm" }));
  }

  async function confirmPayment() {
    setState(s => ({ ...s, step: "confirm", error: null }));
    const res = await fetch(`${BASE}/api/whitelabel/payments/bank-transfer/confirm-payment`, {
      method: "POST",
      headers: headers(),
      body: JSON.stringify({ orderId: state.orderId }),
    });
    const json = await res.json();
    if (!res.ok) throw new Error(json.message);
    setState(s => ({ ...s, step: "done" }));
  }

  return { state, fetchQuote, createVirtualAccount, pollVbaStatus, createOrder, confirmPayment };
}
```

## Bank Transfer APIs

Generates a unique virtual bank account for the user's transfer.

Poll until the virtual account status is `COMPLETED` before placing the order.

Returns the VASP list for travel-rule compliance. Required only for custodial wallet destinations.

Creates the bank transfer order against the confirmed virtual account.

Confirms the payment and completes the order flow.