How to use Payment Methods: Bank Transfer

Place a crypto buy order via SEPA or GBP bank transfer
View as Markdown

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:
quote → virtual account → order → confirmation.

APIPurpose
GET /api/v2/lookup/quotesGet a quote scoped to sepa_bank_transfer or gbp_bank_transfer
POST /api/whitelabel/payments/bank-transfer/create-virtual-accountCreate a virtual bank account (VBA) to receive the user’s transfer
GET /api/whitelabel/payments/bank-transfer/get-payment-instrument-statusPoll 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-orderCreate the order against the confirmed VBA
POST /api/whitelabel/payments/bank-transfer/confirm-paymentConfirm the payment and complete the order flow

Authentication Headers

These APIs support both onboarding paths from 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:

HeaderDescription
x-user-identifierThe authenticated user’s email address
x-access-tokenYour Partner Access Token

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.

1

Request the quote

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

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"
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"
2

Store the quoteId

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

1const { quoteId, cryptoAmount, totalFee, feeBreakdown, expiresAt } = data;
2setQuoteId(quoteId);
3

Show the fee breakdown to the user

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

1// feeBreakdown: [{ name: "Network fee", value: 1.20 }, { name: "Transaction fee", value: 0.80 }]
getBankTransferQuote.ts
1const BASE = "https://api-gateway-stg.transak.com";
2
3interface Quote {
4 quoteId: string;
5 cryptoAmount: number;
6 fiatAmount: number;
7 totalFee: number;
8 feeBreakdown: { id: string; name: string; value: number }[];
9 expiresAt: string;
10}
11
12async function getBankTransferQuote(params: {
13 fiatCurrency: "EUR" | "GBP";
14 cryptoCurrency: string;
15 fiatAmount: number;
16 network: string;
17 apiKey: string;
18 userIp: string;
19}): Promise<Quote> {
20 const paymentMethod =
21 params.fiatCurrency === "GBP" ? "gbp_bank_transfer" : "sepa_bank_transfer";
22
23 const qs = new URLSearchParams({
24 fiatCurrency: params.fiatCurrency,
25 cryptoCurrency: params.cryptoCurrency,
26 isBuyOrSell: "BUY",
27 fiatAmount: String(params.fiatAmount),
28 network: params.network,
29 paymentMethod,
30 });
31
32 const res = await fetch(`${BASE}/api/v2/lookup/quotes?${qs}`, {
33 headers: { "x-api-key": params.apiKey, "x-user-ip": params.userIp },
34 });
35 const json = await res.json();
36 if (!res.ok) throw new Error(json.message ?? `HTTP ${res.status}`);
37 return json.data as Quote;
38}

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.

1

Create the VBA

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

$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"
> }'
2

Check the response status

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

1const { status, message } = data;
2// status: "INITIATED" | "COMPLETED" | "FAILED"
createVirtualAccount.ts
1const BASE = "https://api-gateway-stg.transak.com";
2
3interface VirtualAccountResponse {
4 status: "INITIATED" | "COMPLETED" | "FAILED";
5 message: string;
6}
7
8async function createVirtualAccount(params: {
9 quoteId: string;
10 apiKey: string;
11 userIp: string;
12 accessToken: string;
13}): Promise<VirtualAccountResponse> {
14 const res = await fetch(
15 `${BASE}/api/whitelabel/payments/bank-transfer/create-virtual-account`,
16 {
17 method: "POST",
18 headers: {
19 "x-api-key": params.apiKey,
20 "x-user-ip": params.userIp,
21 },
22 body: JSON.stringify({ quoteId: params.quoteId }),
23 }
24 );
25 const json = await res.json();
26 if (!res.ok) throw new Error(json.message ?? `HTTP ${res.status}`);
27 return json.data as VirtualAccountResponse;
28}

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.

1

Poll the status endpoint

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

$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" \
2

Read the nested status

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

1const instruments = data.paymentInstruments;
2const status = instruments["sepa_bank_transfer"]?.status;
3// or instruments["gbp_bank_transfer"]?.status
3

Handle each status

StatusMeaningAction
INITIATEDVBA setup is in progressKeep polling
COMPLETEDVBA is readyProceed to Step 4 / Step 5
FAILEDVBA setup failedShow error, ask user to retry
pollPaymentInstrumentStatus.ts
1const BASE = "https://api-gateway-stg.transak.com";
2
3type PaymentInstrumentId = "sepa_bank_transfer" | "gbp_bank_transfer";
4
5async function pollPaymentInstrumentStatus(params: {
6 paymentInstrumentId: PaymentInstrumentId;
7 quoteId: string;
8 apiKey: string;
9 userIp: string;
10 accessToken: string;
11 intervalMs?: number;
12 timeoutMs?: number;
13}): Promise<void> {
14 const { intervalMs = 3000, timeoutMs = 120_000 } = params;
15 const deadline = Date.now() + timeoutMs;
16
17 while (Date.now() < deadline) {
18 const qs = new URLSearchParams({
19 paymentInstrumentId: params.paymentInstrumentId,
20 quoteId: params.quoteId,
21 });
22 const res = await fetch(
23 `${BASE}/api/whitelabel/payments/bank-transfer/get-payment-instrument-status?${qs}`,
24 {
25 headers: {
26 "x-api-key": params.apiKey,
27 "x-user-ip": params.userIp,
28 },
29 }
30 );
31 const json = await res.json();
32 if (!res.ok) throw new Error(json.message ?? `HTTP ${res.status}`);
33
34 const status: string =
35 json.data.paymentInstruments[params.paymentInstrumentId]?.status;
36
37 if (status === "COMPLETED") return;
38 if (status === "FAILED") throw new Error("Virtual account setup failed");
39
40 await new Promise(resolve => setTimeout(resolve, intervalMs));
41 }
42
43 throw new Error("Timed out waiting for payment instrument to be COMPLETED");
44}

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.

1

Fetch the VASP list

$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" \
2

Let the user select their VASP

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

1const vasps = data.vasps;
2// [{ vaspId: "binance", name: "Binance", ... }, ...]
3setSelectedVaspId(vasps[0].vaspId);
getVasps.ts
1const BASE = "https://api-gateway-stg.transak.com";
2
3interface Vasp {
4 vaspId: string;
5 name: string;
6}
7
8async function getVasps(params: {
9 apiKey: string;
10 userIp: string;
11 accessToken: string;
12}): Promise<Vasp[]> {
13 const res = await fetch(
14 `${BASE}/api/whitelabel/payments/bank-transfer/get-vasps`,
15 {
16 headers: {
17 "x-api-key": params.apiKey,
18 "x-user-ip": params.userIp,
19 Authorization: params.accessToken,
20 },
21 }
22 );
23 const json = await res.json();
24 if (!res.ok) throw new Error(json.message ?? `HTTP ${res.status}`);
25 return json.data.vasps as Vasp[];
26}

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

1

Create the order

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

$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"
> }'
createOrder.ts
1const BASE = "https://api-gateway-stg.transak.com";
2
3type PaymentInstrumentId = "sepa_bank_transfer" | "gbp_bank_transfer";
4
5interface PaymentDetailField {
6 name: string;
7 value: string;
8}
9
10interface CreateOrderResponse {
11 orderId: string;
12 status: string;
13 paymentDetails: {
14 name: string;
15 fields: PaymentDetailField[];
16 }[];
17}
18
19async function createBankTransferOrder(params: {
20 quoteId: string;
21 walletAddress: string;
22 paymentInstrumentId: PaymentInstrumentId;
23 vaspId?: string;
24 apiKey: string;
25 userIp: string;
26 accessToken: string;
27}): Promise<CreateOrderResponse> {
28 const body: Record<string, string> = {
29 quoteId: params.quoteId,
30 walletAddress: params.walletAddress,
31 paymentInstrumentId: params.paymentInstrumentId,
32 };
33 if (params.vaspId) body.vaspId = params.vaspId;
34
35 const res = await fetch(
36 `${BASE}/api/whitelabel/payments/bank-transfer/create-order`,
37 {
38 method: "POST",
39 headers: {
40 "x-api-key": params.apiKey,
41 "x-user-ip": params.userIp,
42 },
43 body: JSON.stringify(body),
44 }
45 );
46 const json = await res.json();
47 if (!res.ok) throw new Error(json.message ?? `HTTP ${res.status}`);
48 return json.data as CreateOrderResponse;
49}

Step 6 — Confirm Payment

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

1

Confirm the payment

$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"
> }'
2

Show the order confirmation screen

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.

1const { orderId, status } = data;
2// Show order confirmation UI
confirmPayment.ts
1const BASE = "https://api-gateway-stg.transak.com";
2
3async function confirmBankTransferPayment(params: {
4 orderId: string;
5 apiKey: string;
6 userIp: string;
7 accessToken: string;
8}): Promise<{ orderId: string; status: string }> {
9 const res = await fetch(
10 `${BASE}/api/whitelabel/payments/bank-transfer/confirm-payment`,
11 {
12 method: "POST",
13 headers: {
14 "x-api-key": params.apiKey,
15 "x-user-ip": params.userIp,
16 },
17 body: JSON.stringify({ orderId: params.orderId }),
18 }
19 );
20 const json = await res.json();
21 if (!res.ok) throw new Error(json.message ?? `HTTP ${res.status}`);
22 return json.data;
23}

Full Flow (Putting It Together)

hooks/useBankTransferOrder.ts
1import { useState } from "react";
2
3const BASE = "https://api-gateway-stg.transak.com";
4
5type FlowStep =
6 | "idle"
7 | "quote"
8 | "create_vba"
9 | "polling_vba"
10 | "vasp"
11 | "create_order"
12 | "confirm"
13 | "done"
14 | "error";
15
16interface FlowState {
17 step: FlowStep;
18 quoteId: string;
19 paymentInstrumentId: string;
20 bankDetails: Record<string, string>;
21 orderId: string;
22 error: string | null;
23}
24
25export function useBankTransferOrder(
26 apiKey: string,
27 userIp: string,
28 accessToken: string,
29 isCustodialWallet: boolean
30) {
31 const [state, setState] = useState<FlowState>({
32 step: "idle",
33 quoteId: "",
34 paymentInstrumentId: "",
35 bankDetails: {},
36 orderId: "",
37 error: null,
38 });
39
40 const headers = (extra?: Record<string, string>) => ({
41 "x-api-key": apiKey,
42 "x-user-ip": userIp,
43 Authorization: accessToken,
44 "Content-Type": "application/json",
45 ...extra,
46 });
47
48 async function fetchQuote(fiatCurrency: "EUR" | "GBP", cryptoCurrency: string, fiatAmount: number, network: string) {
49 setState(s => ({ ...s, step: "quote", error: null }));
50 const pm = fiatCurrency === "GBP" ? "gbp_bank_transfer" : "sepa_bank_transfer";
51 const qs = new URLSearchParams({ fiatCurrency, cryptoCurrency, isBuyOrSell: "BUY", fiatAmount: String(fiatAmount), network, paymentMethod: pm });
52 const res = await fetch(`${BASE}/api/v2/lookup/quotes?${qs}`, { headers() { return { "x-api-key": apiKey, "x-user-ip": userIp }; } });
53 const json = await res.json();
54 if (!res.ok) throw new Error(json.message);
55 setState(s => ({ ...s, quoteId: json.data.quoteId, step: "create_vba" }));
56 return json.data;
57 }
58
59 async function createVirtualAccount(walletAddress: string, network: string) {
60 setState(s => ({ ...s, step: "create_vba", error: null }));
61 const res = await fetch(`${BASE}/api/whitelabel/payments/bank-transfer/create-virtual-account`, {
62 method: "POST",
63 headers: headers(),
64 body: JSON.stringify({ quoteId: state.quoteId, walletAddress, network }),
65 });
66 const json = await res.json();
67 if (!res.ok) throw new Error(json.message);
68 setState(s => ({ ...s, paymentInstrumentId: json.data.paymentInstrumentId, bankDetails: json.data.bankDetails, step: "polling_vba" }));
69 }
70
71 async function pollVbaStatus() {
72 const deadline = Date.now() + 120_000;
73 while (Date.now() < deadline) {
74 const res = await fetch(
75 `${BASE}/api/whitelabel/payments/bank-transfer/get-payment-instrument-status?paymentInstrumentId=${state.paymentInstrumentId}`,
76 { headers: headers() }
77 );
78 const json = await res.json();
79 if (!res.ok) throw new Error(json.message);
80 if (json.data.status === "COMPLETED") {
81 setState(s => ({ ...s, step: isCustodialWallet ? "vasp" : "create_order" }));
82 return;
83 }
84 if (json.data.status === "FAILED") throw new Error("Virtual account setup failed");
85 await new Promise(r => setTimeout(r, 3000));
86 }
87 throw new Error("Timed out waiting for VBA");
88 }
89
90 async function createOrder(walletAddress: string, vaspId?: string) {
91 setState(s => ({ ...s, step: "create_order", error: null }));
92 const body: Record<string, string> = { quoteId: state.quoteId, walletAddress, paymentInstrumentId: state.paymentInstrumentId };
93 if (vaspId) body.vaspId = vaspId;
94 const res = await fetch(`${BASE}/api/whitelabel/payments/bank-transfer/create-order`, {
95 method: "POST",
96 headers: headers(),
97 body: JSON.stringify(body),
98 });
99 const json = await res.json();
100 if (!res.ok) throw new Error(json.message);
101 setState(s => ({ ...s, orderId: json.data.orderId, step: "confirm" }));
102 }
103
104 async function confirmPayment() {
105 setState(s => ({ ...s, step: "confirm", error: null }));
106 const res = await fetch(`${BASE}/api/whitelabel/payments/bank-transfer/confirm-payment`, {
107 method: "POST",
108 headers: headers(),
109 body: JSON.stringify({ orderId: state.orderId }),
110 });
111 const json = await res.json();
112 if (!res.ok) throw new Error(json.message);
113 setState(s => ({ ...s, step: "done" }));
114 }
115
116 return { state, fetchQuote, createVirtualAccount, pollVbaStatus, createOrder, confirmPayment };
117}

Bank Transfer APIs