Airwallex logo
Airwallex logo

Accept bank transfer payments

You can accept payments in three simple steps:

  1. Initialize a Payment Intent
  2. Generate Transfer Instructions
  3. Query the Payment Status

Step 1. Initialize a Payment Intent

Create a Payment Intent using the Create a Payment Intent API API.

Example request:

Shell
1curl --request POST \
2--url 'https://api-demo.airwallex.com/api/v1/pa/payment_intents/create' \
3--header 'Content-Type: application/json' \
4--header 'Authorization: Bearer <your_bearer_token>' \
5--data-raw '{
6 "request_id": "ed11e38a-7234-11ea-aa94-7fd44ffd1b89",
7 "customer_id": "cus_hkdm564srha6ejk55lt",
8 "amount": 100,
9 "currency": "USD",
10 "order": {
11 "products": [
12 {
13 "name": "apple",
14 "quantity": 5,
15 "unit_price": 20,
16 }
17 ],
18 "shipping": {
19 "first_name": "John",
20 "last_name": "Shooper",
21 "address": {
22 "country_code": "US",
23 "city": "New York",
24 "state": "NY",
25 "postcode": "10001",
26 "street": "16 Sandilands Road"
27 }
28 }
29 },
30 "merchant_order_id": "85d7b0e0-7235-11ea-862e-9f6aa1adfca6",
31 "return_url": "https://www.airwallex.com"
32}'

Step 2. Generate Transfer Instructions

Confirm the Payment Intent with Bank transfer, using the Confirm a Payment Intent API API.

JSON
1{
2 "request_id": "5460fc4a-cb3d-417a-a2e9-697f9e54026e",
3 "payment_method": {
4 "type": "bank_transfer",
5 "bank_transfer": {
6 "shopper_email": "[email protected]"
7 }
8 }
9}

If you are using the dedicated account per customer model, we will send below error message if a customer_id is not provided.

JSON
1{
2 "code": "validation_error",
3 "message": "customer_id is required for the dedicated account per customer model.",
4 "trace_id": "7b944a3b3a0afcc61529f33ac76ad1a0",
5 "details": {}
6}

The transfer instructions provided will be tailored to the currency of the payment intent. We provide 2 options:

  1. Redirect your customer to Airwallex transfer instruction page by using next_action.url.
  2. Display the transfer instruction in your customized format, using the details in next_action.data.transfer_instructions.

USD

JSON
1{
2 // ... other fields omitted.
3 "next_action": {
4 "type": "redirect",
5 "method": "GET",
6 "url": "https://api-staging.airwallex.com/pa/redirect/sg/sgst9qnk8h4wqqbt2ar_q5j0wu?checksum=1537c94a4978",
7 "data": {
8 "transfer_instructions": {
9 "account_name": "Tromp, Littel and Luettgen",
10 "account_number": "8453687204",
11 "bank_name": "Community Federal Savings Bank",
12 "reference": "QCNLQC",
13 "routing_number": "026073150"
14 }
15 }
16 }
17}

EUR

JSON
1{
2 // ... other fields omitted.
3 "next_action": {
4 "type": "redirect",
5 "method": "GET",
6 "url": "https://api-staging.airwallex.com/pa/redirect/sg/sgstdr286h4xzn9nydl_n41u6d?checksum=6631bfa9ca08",
7 "data": {
8 "transfer_instructions": {
9 "account_name": "Tromp, Littel and Luettgen",
10 "bank_name": "Airwallex (Netherlands) B.V.",
11 "iban": "NL71AINH0034616122",
12 "reference": "4L6R9C",
13 "swift_code": "AINHNL22"
14 }
15 }
16 }
17}

GBP

JSON
1{
2 // ... other fields omitted.
3 "next_action": {
4 "type": "redirect",
5 "method": "GET",
6 "url": "https://api-staging.airwallex.com/pa/redirect/sg/sgsttg5tbh4xzpux89l_po4l7q?checksum=158a4fe0fcc2",
7 "data": {
8 "transfer_instructions": {
9 "account_name": "Tromp, Littel and Luettgen",
10 "account_number": "04411119",
11 "bank_name": "Modulr FS Limited",
12 "reference": "D5NCUC",
13 "sort_code": "000000"
14 }
15 }
16 }
17}

SGD

JSON
1{
2 // ... other fields omitted.
3 "next_action": {
4 "type": "redirect",
5 "method": "GET",
6 "url": "https://api-staging.airwallex.com/pa/redirect/sg/sgsttg5tbh4xzpux89l_po4l7q?checksum=158a4fe0fcc2",
7 "data": {
8 "transfer_instructions": {
9 "account_name": "Airwallex (Singapore) Pte Ltd",
10 "account_number": "885388629",
11 "bank_name": "DBS Bank Ltd",
12 "reference": "4ALTYC",
13 "swift_code": "DBSSSGSG"
14 }
15 }
16 }
17}

AUD

JSON
1{
2 // ... other fields omitted.
3 "next_action": {
4 "type": "redirect",
5 "method": "GET",
6 "url": "https://api-staging.airwallex.com/pa/redirect/sg/sgsttg5tbh4xzpux89l_po4l7q?checksum=158a4fe0fcc2",
7 "data": {
8 "transfer_instructions": {
9 "account_name": "Tromp, Littel and Luettgen",
10 "account_number": "12345678",
11 "bank_name": "Australia and New Zealand Banking Group Limited",
12 "reference": "4ALTYC",
13 "bsb_code": "013943"
14 }
15 }
16 }
17}

Handling virtual account availability issues

Some currencies, especially SGD, have a limit on the number of virtual bank account numbers that can be created within a short period. If this limit is reached, the system will return the following error code.

To prevent disruptions, please contact your Account Manager in advance if you anticipate high transaction volumes to request a limit increase.

JSON
1{
2 "code": "provider_unavailable",
3 "message": "The payment provider is temporarily unavailable. Please try again later.",
4 "trace_id": "f75a5ac8986c7b6dbd33eb29b2683565",
5 "details": {
6 "original_response_code": "account_limit_reached"
7 }
8}

Step 3. Query the Payment Status

To get the payment result, we recommend polling the status of the Payment Intent via the Retrieve a Payment Intent API API.

In addition, Airwallex will notify you of the payment result asynchronously via webhooks. Please refer to the webhook documentation to set up your webhook accordingly.

Sample webhook

JSON
1{
2 "id":"evt_sgstrwnsqh50bjcpqce_j43hcy",
3 "name":"payment_attempt.capture_requested",
4 "account_id":"acct_mqOgoGfwMYWniICB7RXLYg",
5 "data":{
6 "object":{
7 // other fields omitted ...
8 "status":"CAPTURE_REQUESTED",
9 "amount":150,
10 "captured_amount":150,
11 "currency":"SGD",
12 "id":"att_sgstspr5qh50bj6g4ej_j43hcy",
13 "merchant_order_id":"e22ddc16-8154-4b81-91e2-049fe068adab",
14 "payment_intent_id":"int_sgstrwnsqh50bj43hcy",
15 "payment_method":{
16 "bank_transfer":{
17 "shopper_email":"[email protected]",
18 "received_amount": "200",
19 "received_currency": "SGD",
20 "received_at": "2025-03-02T16:51:57+0000"
21 },
22 "type":"bank_transfer"
23 }
24 }
25 },
26 "created_at":"2025-02-27T09:52:07+0000",
27 "version":"2024-02-22"
28}

Since bank transfers depend on when and how much the customer sends, the actual received_amount may differ from the expected amount. Webhook status updates are also determined based on your reconciliation preferences.

DescriptionEvent
Customer transferred the exact amount.payment_intent.succeeded
payment_attempt.capture_requested
payment_attempt.capture_amount = payment_intent.amount

Customer transferred more than the expected amount.
The extra amount is set to be returned.

payment_intent.succeeded
payment_attempt.capture_requested
payment_attempt.capture_amount = payment_intent.amount
The extra amount will be returned.

Customer transferred less than the expected amount.
The partial amount is set to be accepted after the expiry time.

payment_intent.succeeded
payment_attempt.capture_requested
payment_attempt.capture_amount = payment_attempt.received_amount

Customer transferred less than the expected amount.
The partial amount is set to be returned after the expiry time.

payment_intent.requires_payment_method
payment_attempt.expired
The received amount will be returned.

No transfer or transfer received after expiry time.

payment_intent.requires_payment_method
payment_attempt.expired
The received amount will be returned.

Test bank transfer in demo environment

You can simulate different payment and refund scenarios in the demo environment by creating different payment intent requests and then simulating your customer's action of sending a transfer. No actual bank transfers are required for this test.

POST /api/v1/simulation/pa/shopper_actions/transfer

JSON
1{
2 "account_number": "E1D5C0E9CB", \\REQUIRED
3 "amount": "520", \\REQUIRED
4 "reference": "D5NCUC" \\OPTIONAL
5}

See example of how different models reconcile payments with incoming funds:

  1. Dedicated account per payment example
  2. Dedicated account per customer example
Was this page helpful?