# Set up bank transfers

The customer pays an order by bank transfer. You create the order on your server and open Glomo checkout. Checkout shows the customer the bank account details, the amount to send and a payment reference. The customer sends the transfer from their own bank. Glomo matches the incoming transfer to the order, and a webhook tells you the result.

> **Other bank-transfer flows: confirm with Glomo.** This page covers one payment per order, collected through checkout. If your integration needs a different bank-transfer setup, confirm the flow with Glomo before you build it.


## What the customer sees

- **One or more receiving accounts.** Checkout can offer a local-transfer account, an international (SWIFT) account, or both. Checkout chooses the currency of the local account from the customer's `country`. Which accounts appear, and in which currencies, depends on how your Glomo account is set up. Confirm this with Glomo.
- **Never per order or per customer.** The receiving account is either your own Glomo account or a shared Glomo account. Different orders, and different customers, can show the same account details. The payment reference is what identifies the order.
- **The amount to send, per account.** It is in that account's currency. It includes any fees that your pricing puts on the customer, and the tax on those fees. When the account currency is not the order currency, it also includes the conversion.
- **A payment reference.** This is `GLM` followed by five characters. The customer must include it in the transfer.


## Before you start

- Bank transfer must be active on your Glomo account. If you send `bank_transfer` in `payment_methods` and it is not active, the order is rejected with `400`.
- Server calls use your secret key. Checkout uses your public key. See [Authentication](/platform/authentication).
- Configure a webhook endpoint. Bank-transfer outcomes arrive asynchronously, so webhooks are how you learn the result. See [Webhooks](/platform/webhooks).


## Integration steps

### Step 1 - Create the customer

`createCustomer` - `POST /customer`

Call this once per payer and store the returned `id` against your user. Reuse it on later orders. Send `name`, `customer_type`, `email`, `address`, `city`, `state`, `country` and `pincode`. Set `country` correctly, because checkout uses it to choose the local-currency account. See [Create a customer](/api-reference/openapi/customer/createcustomer) for the validation rules.

```curl
curl -i -X POST \
  https://api.glomopay.com/api/v1/customer \
  -H 'Authorization: Bearer <YOUR_SECRET_KEY>' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "John Doe",
    "customer_type": "individual",
    "email": "customer@example.com",
    "address": "123 Main St",
    "city": "Anytown",
    "state": "Anystate",
    "country": "ARE",
    "pincode": "560048"
  }'
```

### Step 2 - Create the order

`createOrder` - `POST /orders`

Create the order from your server when the customer starts to pay. The key fields are these:

| Field | Value |
|  --- | --- |
| `customer_id` | The customer `id` from step 1. Required. |
| `amount` | The amount to collect, in the smallest unit of `currency`. `10000` is 100.00 AED. |
| `currency` | The order currency. One of the values that [Create an order](/api-reference/openapi/orders/createorder) lists. |
| `payment_methods` | Include `bank_transfer`. Send only `["bank_transfer"]` to offer bank transfer alone. If you leave out the field, checkout offers every payment method that is active on your account. |
| `request_id` | Your unique ID for this order. A second order with the same `request_id` is rejected with `400` and `Order already exists for this request_id`. |
| `purpose_code` | A payin code from [Payin purpose codes](/payin/purpose-codes). Optional. If you leave it out, your account's default applies, if one is set. |
| `reference_number` | Optional. Your own reference for reconciliation. |


```curl
curl -i -X POST \
  https://api.glomopay.com/api/v1/orders \
  -H 'Authorization: Bearer <YOUR_SECRET_KEY>' \
  -H 'Content-Type: application/json' \
  -d '{
    "customer_id": "cust_E602dMzgjpDC",
    "amount": 10000,
    "currency": "AED",
    "payment_methods": ["bank_transfer"],
    "purpose_code": "P1401",
    "reference_number": "R0001",
    "request_id": "unique_order_request_id_123"
  }'
```

The response is the order. Pass its `id` (for example `order_678a4c88hpAjf`) to checkout. An order that is `active` can be paid.

### Step 3 - Open checkout

Open Glomo checkout in the browser with the order `id` and your public key. The [Checkout](/payin/checkout) guide has the full setup, including the CSP and popup requirements.

```js
import { GlomoCheckoutApi } from 'https://unified-sdk.glomopay.com/index.js';

const checkout = new GlomoCheckoutApi({
  orderId: 'order_678a4c88hpAjf', // the order id from step 2
  publicKey: '<YOUR_PUBLIC_KEY>',
  callbackUrl: 'https://yourwebsite.com/payment-result', // optional, see step 5
});

document.getElementById('pay-button').addEventListener('click', () => checkout.open());
```

### Step 4 - The customer sends the transfer

The customer picks a receiving account in checkout and sends the amount shown from their own bank. Glomo matches the incoming transfer to the order as follows:

- **The payment reference.** When the transfer carries the reference, Glomo matches it to the open order with that reference.
- **The amount and currency.** These must equal the amount and currency that checkout showed for that account. If the customer opens checkout again later, the amount shown can change. The transfer must then match the new amount.
- **The sender details.** If the reference is missing, Glomo can still match the transfer from the sender's account number and name. It compares them with the customer's name and with the details that the customer confirms in checkout (step 5).
- **The order must still be open.** A transfer for an order that is no longer `active`, for example because it expired, does not reopen the order.


If Glomo cannot match the transfer, the payment goes to `action_required`. See [When the transfer doesn't match](#when-the-transfer-doesnt-match).

### Step 5 - Handle the submitted event

After the transfer, checkout can ask the customer to confirm the account they paid from: the last 4 characters of the account number and the name on the account. When they submit, checkout emits `payment.bank_transfer_submitted`.

**This event means the customer says that they sent the transfer. It does not mean that money arrived.** Show a pending state, and wait for the webhook (step 7). The customer can close checkout without submitting, so do not depend on this event.

Without a `callbackUrl`, your handler receives this payload:

```json
{
  "status": "bank_transfer_submitted",
  "orderId": "order_678a4c88hpAjf",
  "senderAccountNumber": "7890",
  "senderName": "John Doe",
  "payerRelationship": ""
}
```

`senderAccountNumber` is the last 4 characters of the account the customer paid from. `senderName` is the name on that account. The customer usually types both. Checkout sometimes prefills them from the customer's details, for example from a registered bank account, or it uses the customer's name when they confirm that the account is their own. Treat them as what the customer told checkout, not as verified details. `payerRelationship` can be empty.

```js
checkout.on('payment.bank_transfer_submitted', (response) => {
  // Show "transfer pending". Do not mark the order paid.
});
```

With a `callbackUrl`, the SDK redirects the browser instead of calling your handler. The same fields arrive as query parameters:

```
https://yourwebsite.com/payment-result?orderId=order_678a4c88hpAjf&status=bank_transfer_submitted&senderAccountNumber=7890&senderName=John+Doe&payerRelationship=
```

If the order is already paid while the customer is still on the confirmation screen, checkout emits `payment.success` instead, or redirects with `status=success`. For a bank transfer, this success carries an empty `paymentId` and an empty `signature`.

### Step 6 - Verify the signature

- **Checkout events are not signed for bank transfers.** `payment.bank_transfer_submitted` and its redirect have no signature. The bank-transfer `payment.success` has an empty `signature`. You cannot verify them, so never mark an order paid from the browser. The `order_id|payment_id|status` signature recipe in the [Checkout](/payin/checkout) guide applies only when the event carries a `paymentId` and a `signature`.
- **Confirm on your server.** Use a verified webhook (step 7), or call [Retrieve an order](/api-reference/openapi/orders/getorderbyid) and check for `status: paid`.
- **Verify every webhook.** Glomo signs each webhook with HMAC SHA-256 over the raw request body and sends the signature in `X-Glomopay-Signature`. See [Webhook Authentication & Security](/platform/webhooks#webhook-authentication--security).


### Step 7 - Handle the payment webhooks

These webhooks matter for a bank transfer. See [Payment webhooks](/platform/webhooks/payment) for the payloads.

| `entity_type` | `event_type` | Meaning |
|  --- | --- | --- |
| `payment` | `success` | The transfer was received, matched to the order and processed. This is the funds-received signal. Check `funds_available` and `compliance_status` in `data`. |
| `payment` | `funds_available` | Funds that were held on a `success` webhook with `funds_available: false` are now available. |
| `payment` | `action_required` | A transfer arrived, but Glomo could not complete it. Read `error_code`. See [When the transfer doesn't match](#when-the-transfer-doesnt-match). |
| `orders` | `paid` | The order is paid. |


About `funds_available`: read it from the `success` webhook, not from the payment returned by an API call. A payment read soon after it succeeds can briefly show `funds_available: false` before the `success` webhook is sent.

- On the `success` webhook, `data.funds_available: true` means that the funds are available in your Glomo balance, for payouts and settlement.
- On the `success` webhook, `data.funds_available: false` means that the funds are held. A later webhook with `event_type: funds_available` tells you when they become available.
- A payment can be `success` while `compliance_status` is `action_required`. The money was received, but it is held until you answer the RFI. See [Compliance Reviews & RFIs on Successful Payments](/request-for-information/compliance-reviews-and-rfis-on-successful-payments).


Do not depend on the order in which the `payment` and `orders` webhooks arrive. A retried delivery can repeat an event, so process each one idempotently.

In the payment, `payment_amount` and `payment_currency` are what the customer sent. `converted_amount` and `converted_currency` are the order side.

## When the transfer doesn't match

A transfer that Glomo cannot match leaves the payment in `action_required`, with `payment_method.type: bank_transfer` and one of these `error_code` values:

- `PAYIN_NOT_FOUND`: no open order matched the transfer. No open order had the transfer's reference or sender together with the same amount and currency, or the order was no longer open.
- `PAYIN_AMOUNT_MISMATCH`: the payment is connected to an order, but the amount received does not equal the amount that the order expects.


To resolve these payments, see [Resolving Action Required Payments](/payin/action-required).

## Test it in sandbox

In sandbox, [Create a mock payment](/api-reference/openapi/payment/createmockpayment) sends a mock bank transfer into one of your virtual accounts. Glomo processes it like a real transfer. It matches the transfer to an open order or payment link

- **With `payin_id`.** The transfer goes to the virtual account that checkout offers the payin's customer for `transfer_type`. The amount defaults to the payin's quoted amount, and the sender defaults to the payin's customer. The reference defaults to the payin's payment reference, so the transfer matches by reference. Do not send `currency`, or the call returns `400`.
- **Without `payin_id`.** Send `amount` and `currency`. The transfer goes to your virtual account for that currency and `transfer_type`. It matches an open payin only by the payin's payment reference, or by the sender's name or account together with the payin's quoted amount.


A `local` transfer uses local rails, and a `swift` transfer uses SWIFT rails. If there is no virtual account for the transfer, the call returns `400` with `No active <transfer_type> virtual account for this payment`.

The examples use the order `order_678a4c88hpAjf`. Its payment reference is `GLMHPAJF`. [Create a mock payment](/api-reference/openapi/payment/createmockpayment) shows the full response for each case.

### Matched transfer

```json
{ "payin_id": "order_678a4c88hpAjf", "transfer_type": "local" }
```

Expect a payment with `status: success`, then the `payment` `success` and `orders` `paid` webhooks.

### Overpayment or underpayment

Send an `amount` that is different from the quoted amount:

```json
{ "payin_id": "order_678a4c88hpAjf", "transfer_type": "local", "amount": 12000 }
```

The default reference still connects the transfer to the order. Expect `status: action_required` with `error_code: PAYIN_AMOUNT_MISMATCH`.

### Unmatched transfer

```json
{ "amount": 10000, "currency": "USD", "transfer_type": "local" }
```

Expect `status: action_required` with `error_code: PAYIN_NOT_FOUND`. Resolve it as in [When the transfer doesn't match](#when-the-transfer-doesnt-match).

### Sanctions hit

Create an order whose quoted amount is exactly `6623`. Then mock it with its `payin_id` only:

```json
{ "payin_id": "order_68f1c1d0sQ4nK", "transfer_type": "local" }
```

The result depends on post-payment screening for bank transfers on your account:

- **Off.** Expect `status: action_required` with `error_code: SANCTION_HIT`.
- **On.** Expect `status: success` with `compliance_status: under_review`. After the screening, Glomo sends the `payment` `compliance_action_required` webhook with an RFI, and holds settlement until you resolve it. See [Compliance Reviews & RFIs on Successful Payments](/request-for-information/compliance-reviews-and-rfis-on-successful-payments).


Glomo screens a transfer only after it matches a payin and passes the amount check. A transfer that stops with `PAYIN_NOT_FOUND` or `PAYIN_AMOUNT_MISMATCH` is not screened.