<!-- Source: https://blnkfinance.com/blog/how-to-build-a-loan-disbursement-and-repayment-app-with-flutterwave-blnk -->

[Blog](https://blnkfinance.com/blog) [Build](https://blnkfinance.com/blog/build)

Apr 23, 2026 10 min read

# How to Build a Loan Disbursement and Repayment App with Flutterwave + Blnk

-   Praise Philemon Co-founder & Head of Revenue

![How to build a loan disbursement and repayment app with Flutterwave + Blnk](https://cdn.sanity.io/images/06pses5q/production/a7bba474ff8e34e828ce6453ddde49a6457461ef-1920x1080.png?w=1600&fit=max&auto=format)

On this page

1.  [Prerequisites](#prerequisites)
2.  [Designing the loan ledger](#designing-the-loan-ledger)
3.  [Onboarding customers](#onboarding-customers)
4.  [Disbursing a loan](#disbursing-a-loan)
5.  [Handling failed disbursements](#handling-failed-disbursements)
6.  [Charging interest](#charging-interest)
7.  [Collecting repayments](#collecting-repayments)
8.  [Tracking loan status](#tracking-loan-status)
9.  [Extending to other countries](#extending-to-other-countries)
10.  [What else can you build?](#what-else-can-you-build)

You are building [a loan app](https://blnkfinance.com/blog/how-to-build-loan-disbursement-interest-and-repayments-with-blnk) and you've picked [Flutterwave](https://flutterwave.com) as your payment provider. Your app approves a customer loan, Flutterwave sends the cash to their bank account, and everyone's happy — until you realize you need to also track who owes what, how much they've repaid, or what happens when a payout fails halfway through.

In this guide, we're building **QuickCash**, a loan app that disburses loans and collects repayments in NGN. Customers sign up, [verify their identity](https://blnkfinance.com/glossary/identity), and request a loan. The money lands in their bank account within minutes.

We'll use Flutterwave to move the money and Blnk to track every movement on the ledger.

Here's what we'll cover:

-   Designing a loan ledger that tracks disbursements, repayments, and interest
-   Onboarding customers
-   Disbursing loans via Flutterwave and recording them with Blnk's **inflight** feature
-   Handling failed disbursements
-   Collecting repayments through Flutterwave **virtual accounts**
-   Tracking outstanding debt in real time

## \# Prerequisites

Before you start, make sure you have:

-   A [Flutterwave account](https://app.flutterwave.com/register) with API keys
-   A running [Blnk instance](https://docs.blnkfinance.com/home/install) (self-hosted or [Blnk Cloud](https://cloud.blnkfinance.com))
-   Basic familiarity with REST APIs and [webhooks](https://docs.blnkfinance.com/webhooks/overview)

This guide uses the [Flutterwave v3 API](https://developer.flutterwave.com/v3.0.0/docs/) for all payment operations.

## \# Designing the loan ledger

Before you touch any code, define how money moves through your system. In Blnk, this starts with creating a [ledger](https://docs.blnkfinance.com/ledgers/general-ledger) and mapping out your [internal balances](https://docs.blnkfinance.com/balances/internal-balances).

To create a ledger for QuickCash customers:

<!-- TypeScript -->
```
const response = await blnk.Ledgers.create({
  name: 'QuickCash Loans — Nigeria',
  meta_data: {
    description: 'Ledger for QuickCash customers in Nigeria',
  },
});
```

<!-- Go -->
```
ledger, resp, err := client.Ledger.Create(blnkgo.CreateLedgerRequest{
  Name: "QuickCash Loans — Nigeria",
  MetaData: blnkgo.MetaData{
    "description": "Ledger for QuickCash customers in Nigeria",
  },
})
```

<!-- Python -->
```
response = blnk.ledgers.create({
  "name": "QuickCash Loans — Nigeria",
  "meta_data": {
    "description": "Ledger for QuickCash customers in Nigeria",
  },
})
```

<!-- Java -->
```
ApiResponse<JsonNode> response = blnk.ledgers().create(
  CreateLedger.create()
    .name("QuickCash Loans — Nigeria")
    .metaData(Map.of("description", "Ledger for QuickCash customers in Nigeria")));
```

<!-- 200 OK -->
```
{
  "ledger_id": "ldg_quickcash-loans-ng-id",
  "name": "QuickCash Loans — Nigeria",
  "created_at": "2025-01-15T10:00:00Z"
}
```

Here's how money moves in QuickCash:

![](https://cdn.sanity.io/images/06pses5q/production/2b2599905152647a4ca6deb39f658b0ec2321d38-2118x1058.png?w=1920&fit=max&auto=format)

Money movement map showing How funds flow between Flutterwave, customer balances, and internal balances for disbursements (top) and repayments (flow)

-   **Disbursement (top flow):** Money leaves Flutterwave and lands in the customer's bank account. On the ledger, Blnk records this as `Customer balance` → `@CustomerLoans-NGN`.
-   **Repayment (bottom flow):** Money comes into Flutterwave when the customer transfers funds to their virtual account. On the ledger, Blnk records this as `@CustomerLoans-NGN` → `Customer balance`, reducing the outstanding debt.

`@CustomerLoans-NGN` holds the **total sum of all outstanding loans** across every customer. Every disbursement increases it; every repayment reduces it. At any point, you can query this single balance to see how much money is out the door.

On the customer side, each individual balance tells you exactly how much that customer owes. When it hits zero, the loan is fully repaid.

## \# Onboarding customers

When a new customer signs up for QuickCash, you collect their KYC details, bank account number, and BVN. The BVN is collected upfront because you'll need it to create a static virtual account later in this flow.

Before proceeding, verify their bank details using Flutterwave's [account resolve endpoint](https://developer.flutterwave.com/v3.0.0/docs/bank-account). This ensures you never store bad account data, and avoids failed payouts later.

By the end of onboarding, the customer has an identity, a wallet balance, and a virtual account. This is everything you need to disburse and collect loans.

1.  Create an identity in Blnk
    
    With the customer's details collected, create an [identity](https://docs.blnkfinance.com/identities/introduction) in Blnk to represent them on the ledger:
    
    <!-- TypeScript -->
    ```
    const response = await blnk.Identities.create({
      identity_type: 'individual',
      first_name: 'Aduke',
      last_name: 'Okonkwo',
      email_address: 'aduke@example.com',
      meta_data: {
        account_number: '0690000031',
        bank_code: '044',
        bank_name: 'Access Bank',
      },
    });
    ```
    
    <!-- Go -->
    ```
    identity, resp, err := client.Identity.Create(blnkgo.Identity{
      IdentityType: "individual",
      FirstName: "Aduke",
      LastName: "Okonkwo",
      EmailAddress: "aduke@example.com",
      MetaData: blnkgo.MetaData{
        "account_number": "0690000031",
        "bank_code": "044",
        "bank_name": "Access Bank",
      },
    })
    ```
    
    <!-- Python -->
    ```
    response = blnk.identity.create({
      "identity_type": "individual",
      "first_name": "Aduke",
      "last_name": "Okonkwo",
      "email_address": "aduke@example.com",
      "meta_data": {
        "account_number": "0690000031",
        "bank_code": "044",
        "bank_name": "Access Bank",
      },
    })
    ```
    
    <!-- Java -->
    ```
    ApiResponse<JsonNode> response = blnk.identity().create(
      CreateIdentity.create()
        .identityType("individual")
        .firstName("Aduke")
        .lastName("Okonkwo")
        .emailAddress("aduke@example.com")
        .metaData(Map.of("account_number", "0690000031", "bank_code", "044", "bank_name", "Access Bank")));
    ```
    
    <!-- 200 OK -->
    ```
    {
      "identity_id": "idt_aduke-identity-id",
      "identity_type": "individual",
      "first_name": "Aduke",
      "last_name": "Okonkwo",
      "email_address": "aduke@example.com",
      "created_at": "2025-01-15T10:05:00Z"
    }
    ```
    
2.  Create a balance
    
    Once the customer's KYC is verified, create a wallet [balance](https://docs.blnkfinance.com/balances/introduction) linked to the identity. This is where Blnk tracks everything the customer owes and repays.
    
    <!-- TypeScript -->
    ```
    const response = await blnk.LedgerBalances.create({
      ledger_id: 'ldg_quickcash-loans-ng-id',
      identity_id: 'idt_aduke-identity-id',
      currency: 'NGN',
      meta_data: {
        account_number: '0690000031',
        bank_code: '044',
        bank_name: 'Access Bank',
        account_type: 'wallet',
      },
    });
    ```
    
    <!-- Go -->
    ```
    balance, resp, err := client.LedgerBalance.Create(blnkgo.CreateLedgerBalanceRequest{
      LedgerID: "ldg_quickcash-loans-ng-id",
      IdentityID: "idt_aduke-identity-id",
      Currency: "NGN",
      MetaData: blnkgo.MetaData{
        "account_number": "0690000031",
        "bank_code": "044",
        "bank_name": "Access Bank",
        "account_type": "wallet",
      },
    })
    ```
    
    <!-- Python -->
    ```
    response = blnk.ledger_balances.create({
      "ledger_id": "ldg_quickcash-loans-ng-id",
      "identity_id": "idt_aduke-identity-id",
      "currency": "NGN",
      "meta_data": {
        "account_number": "0690000031",
        "bank_code": "044",
        "bank_name": "Access Bank",
        "account_type": "wallet",
      },
    })
    ```
    
    <!-- Java -->
    ```
    ApiResponse<JsonNode> response = blnk.ledgerBalances().create(
      CreateLedgerBalance.create()
        .ledgerId("ldg_quickcash-loans-ng-id")
        .identityId("idt_aduke-identity-id")
        .currency("NGN")
        .metaData(Map.of("account_number", "0690000031", "bank_code", "044", "bank_name", "Access Bank", "account_type", "wallet")));
    ```
    
    <!-- 200 OK -->
    ```
    {
      "balance_id": "bln_aduke-balance-id",
      "ledger_id": "ldg_quickcash-loans-ng-id",
      "identity_id": "idt_aduke-identity-id",
      "currency": "NGN",
      "balance": 0,
      "credit_balance": 0,
      "debit_balance": 0,
      "created_at": "2025-01-15T10:06:00Z"
    }
    ```
    
    The customer's bank details are stored in the wallet balance metadata. When it's time to disburse a loan, you pull these details to initiate the Flutterwave payout, keeping the banking info tied directly to the balance.
    
    ![](https://cdn.sanity.io/images/06pses5q/production/b5f03a9017dace6c75b775ae53300c97d9cd57da-3420x2146.png?w=1920&fit=max&auto=format)
    
    The customer's identity and wallet balance at ₦0 after onboarding.
    
3.  Set up a virtual account for repayments
    
    Finally, create a [static virtual account](https://developer.flutterwave.com/v3.0.0/reference/create-a-virtual-account-number-1) through Flutterwave so the customer has a dedicated bank account to send repayments to.
    
    The key detail is to use the `balance_id` returned by Blnk as the `tx_ref`. That directly links the virtual account to the customer’s wallet balance, so when a repayment comes in, Flutterwave’s webhook includes the same `tx_ref` and you instantly know which balance to credit without any extra lookups.
    
    <!-- POST /v3/virtual-account-numbers -->
    ```
    curl --request POST 'https://api.flutterwave.com/v3/virtual-account-numbers' \
      --header 'Authorization: Bearer YOUR_FLW_SECRET_KEY' \
      --header 'Content-Type: application/json' \
      --data-raw '{
        "email": "aduke@example.com",
        "amount": 100000,
        "currency": "NGN",
        "tx_ref": "bln_aduke-balance-id",
        "is_permanent": true,
        "firstname": "Aduke",
        "lastname": "Okonkwo",
        "narration": "QuickCash Loan Repayment",
        "phonenumber": "08100000000",
        "bvn": "1234567890",
        "bank_code": "035"
      }'
    ```
    
    <!-- 200 OK -->
    ```
    {
      "status": "success",
      "message": "Virtual account created",
      "data": {
        "response_code": "02",
        "response_message": "Transaction in progress",
        "flw_ref": "FLW-60cc371ace424e1aa856c8dee118b083",
        "order_ref": "URF_1756805655379_5995535",
        "account_number": "6185071007",
        "frequency": "N/A",
        "bank_name": "WEMA BANK",
        "created_at": "2025-01-15 12:00:00",
        "expiry_date": "N/A",
        "note": "Please make a bank transfer to QuickCash Loan Repayment FLW",
        "amount": "100000.00"
      }
    }
    ```
    
    Because `is_permanent` is `true`, this account doesn't expire. The customer can use it for every repayment going forward.
    
    ![](https://cdn.sanity.io/images/06pses5q/production/432a02484e70fa6fa12e69469a05600147d20b95-3420x2146.png?w=1920&fit=max&auto=format)
    
    Virtual account details for the balance stored in the Blnk metadata
    
    Store the `account_number` and `bank_name` in the customer's record and in the balance metadata as well, so you only have to fetch it from Blnk and display it in your app.
    
    At this point, onboarding is complete.
    

## \# Disbursing a loan

This is where Flutterwave and Blnk work together. The flow:

1.  Record an [inflight](https://docs.blnkfinance.com/transactions/inflight/creating-inflight) transaction in Blnk from the customer to `@CustomerLoans-NGN`
2.  Initiate the payout via Flutterwave's Transfer API
3.  Listen for Flutterwave's webhook
4.  **Commit** the transaction if the payout succeeds, or **void** it if it fails

Your ledger never records a completed transaction until the money has actually moved.

1.  Record the inflight transaction
    
    Use the same reference for both Blnk and Flutterwave — this ties the ledger entry to the payout for end-to-end traceability.
    
    <!-- TypeScript -->
    ```
    const response = await blnk.Transactions.create({
      amount: 100000,
      reference: 'loan_dis_aduke_001',
      currency: 'NGN',
      precision: 100,
      source: 'bln_aduke-balance-id',
      destination: '@CustomerLoans-NGN',
      inflight: true,
      allow_overdraft: true,
      description: 'Loan disbursement - Aduke Okonkwo',
      meta_data: {
        loan_type: 'personal',
        tenure_months: 6,
      },
    });
    ```
    
    <!-- Go -->
    ```
    transaction, resp, err := client.Transaction.Create(
      blnkgo.CreateTransactionRequest{
        ParentTransaction: blnkgo.ParentTransaction{
          Amount: 100000,
          Reference: "loan_dis_aduke_001",
          Currency: "NGN",
          Precision: 100,
          Source: "bln_aduke-balance-id",
          Destination: "@CustomerLoans-NGN",
          Description: "Loan disbursement - Aduke Okonkwo",
          MetaData: blnkgo.MetaData{
            "loan_type": "personal",
            "tenure_months": 6,
          },
        },
        Inflight: true,
        AllowOverdraft: true,
      },
    )
    ```
    
    <!-- Python -->
    ```
    response = blnk.transactions.create({
      "amount": 100000,
      "reference": "loan_dis_aduke_001",
      "currency": "NGN",
      "precision": 100,
      "source": "bln_aduke-balance-id",
      "destination": "@CustomerLoans-NGN",
      "inflight": True,
      "allow_overdraft": True,
      "description": "Loan disbursement - Aduke Okonkwo",
      "meta_data": {
        "loan_type": "personal",
        "tenure_months": 6,
      },
    })
    ```
    
    <!-- Java -->
    ```
    ApiResponse<JsonNode> response = blnk.transactions().create(
      CreateTransactions.create()
        .amount(100000)
        .reference("loan_dis_aduke_001")
        .currency("NGN")
        .precision(100)
        .source("bln_aduke-balance-id")
        .destination("@CustomerLoans-NGN")
        .inflight(true)
        .allowOverdraft(true)
        .description("Loan disbursement - Aduke Okonkwo")
        .metaData(Map.of("loan_type", "personal", "tenure_months", 6)));
    ```
    
    <!-- 200 OK -->
    ```
    {
      "transaction_id": "txn_disbursement-id",
      "source": "bln_aduke-balance-id",
      "destination": "@CustomerLoans-NGN",
      "reference": "loan_dis_aduke_001",
      "amount": 100000,
      "precision": 100,
      "precise_amount": 10000000,
      "currency": "NGN",
      "status": "INFLIGHT",
      "description": "Loan disbursement - Aduke Okonkwo",
      "allow_overdraft": true,
      "inflight": true,
      "created_at": "2025-01-15T10:08:00Z"
    }
    ```
    
    ![](https://cdn.sanity.io/images/06pses5q/production/1c98e77d8aa2969bd9a9eea9b7e40c7c945e2c7f-3420x2146.png?w=1920&fit=max&auto=format)
    
    Inflight transaction record in Blnk representing a loan to be disbursed.
    
    Here's what happens:
    
    -   The customer's balance enters a **held** state — the debit is recorded as inflight, not yet applied
    -   [`allow_overdraft`](https://docs.blnkfinance.com/transactions/overdrafts) is set to `true` because the customer's balance starts at zero and needs to go negative to reflect the debt
    -   The transaction stays in `INFLIGHT` status until you commit or void it
    -   The `reference` is the thread connecting this Blnk transaction to the Flutterwave payout
    
    Blnk uses [precision](https://docs.blnkfinance.com/transactions/precision) to store amounts in the smallest currency unit. With `precision: 100`, an amount of `100000` (₦100,000) is stored as `10,000,000` internally. All balance values in API responses reflect this precision-adjusted format.
    
2.  Initiate the Flutterwave payout
    
    The customer's bank details were already verified during onboarding, so you can go straight to initiating the [transfer](https://developer.flutterwave.com/reference/create-a-transfer) using the same reference:
    
    <!-- POST /v3/transfers -->
    ```
    curl --request POST 'https://api.flutterwave.com/v3/transfers' \
      --header 'Authorization: Bearer YOUR_FLW_SECRET_KEY' \
      --header 'Content-Type: application/json' \
      --data '{
        "account_bank": "044",
        "account_number": "0690000031",
        "amount": 100000,
        "narration": "QuickCash loan disbursement",
        "currency": "NGN",
        "reference": "loan_dis_aduke_001",
        "callback_url": "https://yourapp.com/webhooks/flutterwave",
        "debit_currency": "NGN"
      }'
    ```
    
    <!-- 200 OK -->
    ```
    {
      "status": "success",
      "message": "Transfer Queued Successfully",
      "data": {
        "id": 1933222,
        "account_number": "0690000031",
        "bank_code": "044",
        "full_name": "Aduke Okonkwo",
        "created_at": "2025-01-15T10:10:00.000Z",
        "currency": "NGN",
        "debit_currency": "NGN",
        "amount": 100000,
        "fee": 26.88,
        "status": "NEW",
        "reference": "loan_dis_aduke_001",
        "narration": "QuickCash loan disbursement",
        "complete_message": "",
        "requires_approval": 0,
        "is_approved": 1,
        "bank_name": "ACCESS BANK NIGERIA"
      }
    }
    ```
    
    Here's what happens:
    
    -   Flutterwave initiates the transfer and returns a `NEW` status
    -   The `reference` matches the one in Blnk, linking both records
    -   You'll receive a `transfer.completed` [webhook](https://developer.flutterwave.com/docs/webhooks) when the transfer succeeds or fails
    
3.  Commit on success
    
    When Flutterwave's webhook confirms the payout succeeded, commit the inflight transaction in Blnk:
    
    <!-- TypeScript -->
    ```
    const response = await blnk.Transactions.updateStatus(
      'txn_disbursement-blnk-id',
      {
        status: 'commit',
      },
    );
    ```
    
    <!-- Go -->
    ```
    transaction, resp, err := client.Transaction.Update(
      "txn_disbursement-blnk-id",
      blnkgo.UpdateStatus{
        Status: blnkgo.InflightStatusCommit,
      },
    )
    ```
    
    <!-- Python -->
    ```
    response = blnk.transactions.update_status(
      "txn_disbursement-blnk-id",
      {
        "status": "commit",
      },
    )
    ```
    
    <!-- Java -->
    ```
    ApiResponse<JsonNode> response = blnk.transactions().updateStatus(
      "txn_disbursement-blnk-id",
      UpdateTransactionStatus.create()
        .status("commit"));
    ```
    
    ![](https://cdn.sanity.io/images/06pses5q/production/4cabfa5208ef2b82b10614d7af79afd3f87184cd-3420x2146.png?w=1920&fit=max&auto=format)
    
    The customer's balance is now negative (-₦100,000), reflecting the outstanding loan.
    
    The customer's balance is now negative (e.g., -10,000,000 in precision-adjusted units), reflecting the ₦100,000 they owe.
    

## \# Handling failed disbursements

If Flutterwave's webhook reports a failure, void the inflight transaction instead:

<!-- TypeScript -->
```
const response = await blnk.Transactions.updateStatus(
  'txn_disbursement-id',
  {
    status: 'void',
  },
);
```

<!-- Go -->
```
transaction, resp, err := client.Transaction.Update(
  "txn_disbursement-id",
  blnkgo.UpdateStatus{
    Status: blnkgo.InflightStatusVoid,
  },
)
```

<!-- Python -->
```
response = blnk.transactions.update_status(
  "txn_disbursement-id",
  {
    "status": "void",
  },
)
```

<!-- Java -->
```
ApiResponse<JsonNode> response = blnk.transactions().updateStatus(
  "txn_disbursement-id",
  UpdateTransactionStatus.create()
    .status("void"));
```

![](https://cdn.sanity.io/images/06pses5q/production/94e07c947d0df264690403131afdebfc8e905361-3420x2146.png?w=1920&fit=max&auto=format)

A voided inflight transaction: status reads "VOID" and the customer's balance is unchanged.

Here's what happens:

-   The held amounts are released
-   The customer's balance returns to its previous state
-   No money moved on Flutterwave, no false entry on Blnk — your books stay clean

This is one of the biggest gaps Flutterwave doesn't cover for you. Without inflight, a failed payout could leave your ledger showing a disbursement that never actually happened. Inflight ensures your ledger and your payment provider are always in sync.

## \# Charging interest

QuickCash charges interest on outstanding loans. In Blnk, interest is just another transaction; you debit the customer's balance and credit an internal balance that tracks all interest revenue.

Set up a scheduled job (e.g., a daily or monthly cron) that calculates the interest owed and records it in Blnk. For example, if QuickCash charges 5% monthly interest on Aduke's ₦100,000 loan:

<!-- TypeScript -->
```
const response = await blnk.Transactions.create({
  amount: 5000,
  reference: 'interest_aduke_001_202502',
  currency: 'NGN',
  precision: 100,
  source: 'bln_aduke-balance-id',
  destination: '@QuickCashRevenue-NGN',
  allow_overdraft: true,
  description: 'Monthly interest - Aduke Okonkwo - Feb 2025',
});
```

<!-- Go -->
```
transaction, resp, err := client.Transaction.Create(
  blnkgo.CreateTransactionRequest{
    ParentTransaction: blnkgo.ParentTransaction{
      Amount: 5000,
      Reference: "interest_aduke_001_202502",
      Currency: "NGN",
      Precision: 100,
      Source: "bln_aduke-balance-id",
      Destination: "@QuickCashRevenue-NGN",
      Description: "Monthly interest - Aduke Okonkwo - Feb 2025",
    },
    AllowOverdraft: true,
  },
)
```

<!-- Python -->
```
response = blnk.transactions.create({
  "amount": 5000,
  "reference": "interest_aduke_001_202502",
  "currency": "NGN",
  "precision": 100,
  "source": "bln_aduke-balance-id",
  "destination": "@QuickCashRevenue-NGN",
  "allow_overdraft": True,
  "description": "Monthly interest - Aduke Okonkwo - Feb 2025",
})
```

<!-- Java -->
```
ApiResponse<JsonNode> response = blnk.transactions().create(
  CreateTransactions.create()
    .amount(5000)
    .reference("interest_aduke_001_202502")
    .currency("NGN")
    .precision(100)
    .source("bln_aduke-balance-id")
    .destination("@QuickCashRevenue-NGN")
    .allowOverdraft(true)
    .description("Monthly interest - Aduke Okonkwo - Feb 2025"));
```

<!-- 200 OK -->
```
{
  "transaction_id": "txn_interest-id",
  "source": "bln_aduke-balance-id",
  "destination": "@QuickCashRevenue-NGN",
  "reference": "interest_aduke_001_202502",
  "amount": 5000,
  "precision": 100,
  "precise_amount": 500000,
  "currency": "NGN",
  "status": "APPLIED",
  "description": "Monthly interest - Aduke Okonkwo - Feb 2025",
  "created_at": "2025-02-15T00:00:00Z"
}
```

Here's what happens:

-   The customer's balance goes further negative, reflecting the additional interest owed
-   `@QuickCashRevenue-NGN` captures all interest revenue for QuickCash
-   Interest is tracked as a separate transaction from the principal disbursement, so you can always see how much of a customer's debt is principal vs. interest

Your app controls the interest logic: flat rate, reducing balance, daily accrual, whatever your product requires. Blnk just records the result as a clean ledger entry.

## \# Collecting repayments

Since we created a static virtual account during onboarding, the customer already has a dedicated bank account to send repayments to. When they transfer money to that account, Flutterwave sends a `charge.completed` [webhook](https://developer.flutterwave.com/docs/webhooks) to your server.

<!-- 200 OK -->
```
{
  "event": "charge.completed",
  "data": {
    "tx_ref": "bln_aduke-balance-id",
    "flw_ref": "FLW-a1b2c3d4e5f6",
    "amount": 20000,
    "currency": "NGN",
    "status": "successful",
    "payment_type": "bank_transfer"
  },
  "meta_data": {
    "originatorname": "ADUKE OKONKWO",
    "bankname": "ACCESS BANK"
  },
  "event.type": "BANK_TRANSFER_TRANSACTION"
}
```

Once verified, use the `tx_ref` to identify the customer's balance and the `flw_ref` as the transaction reference in Blnk:

<!-- TypeScript -->
```
const response = await blnk.Transactions.create({
  amount: 20000,
  reference: 'FLW-a1b2c3d4e5f6',
  currency: 'NGN',
  precision: 100,
  source: '@CustomerLoans-NGN',
  destination: 'bln_aduke-balance-id',
  description: 'Loan repayment - Aduke Okonkwo',
});
```

<!-- Go -->
```
transaction, resp, err := client.Transaction.Create(
  blnkgo.CreateTransactionRequest{
    ParentTransaction: blnkgo.ParentTransaction{
      Amount: 20000,
      Reference: "FLW-a1b2c3d4e5f6",
      Currency: "NGN",
      Precision: 100,
      Source: "@CustomerLoans-NGN",
      Destination: "bln_aduke-balance-id",
      Description: "Loan repayment - Aduke Okonkwo",
    },

  },
)
```

<!-- Python -->
```
response = blnk.transactions.create({
  "amount": 20000,
  "reference": "FLW-a1b2c3d4e5f6",
  "currency": "NGN",
  "precision": 100,
  "source": "@CustomerLoans-NGN",
  "destination": "bln_aduke-balance-id",
  "description": "Loan repayment - Aduke Okonkwo",
})
```

<!-- Java -->
```
ApiResponse<JsonNode> response = blnk.transactions().create(
  CreateTransactions.create()
    .amount(20000)
    .reference("FLW-a1b2c3d4e5f6")
    .currency("NGN")
    .precision(100)
    .source("@CustomerLoans-NGN")
    .destination("bln_aduke-balance-id")
    .description("Loan repayment - Aduke Okonkwo"));
```

Here's what happens:

-   The `tx_ref` (`bln_aduke-balance-id`) from the webhook maps directly to the customer's Blnk balance — no lookup needed
-   The `flw_ref` (`FLW-a1b2c3d4e5f6`) is used as the Blnk transaction reference, tying the ledger entry back to the Flutterwave payment
-   `@CustomerLoans-NGN` is debited, reflecting a reduction in outstanding loans
-   The customer's balance is credited, moving it closer to zero

Using `flw_ref` as the Blnk transaction reference also gives you idempotency for free; if Flutterwave sends the same webhook twice, Blnk will reject the duplicate because the reference already exists.

![](https://cdn.sanity.io/images/06pses5q/production/232015dd0e7910bcdb417bc55e8cac6b51ae5be1-3420x2146.png?w=1920&fit=max&auto=format)

The transaction list showing both the disbursement and the repayment, giving a full picture of the loan lifecycle.

Customers can send any amount to the virtual account. A partial payment simply moves the balance closer to zero without fully clearing the debt. An overpayment pushes the balance above zero, which your app can handle as a credit for future loans or trigger a refund.

## \# Tracking loan status

At any point, query a customer's balance to see exactly where they stand:

<!-- TypeScript -->
```
const response = await blnk.LedgerBalances.get(
  'bln_aduke-balance-id',
);
```

<!-- Go -->
```
balance, resp, err := client.LedgerBalance.Get(
  "bln_aduke-balance-id",
)
```

<!-- Python -->
```
response = blnk.ledger_balances.get(
  "bln_aduke-balance-id",
)
```

<!-- Java -->
```
ApiResponse<JsonNode> response = blnk.ledgerBalances().get(
  "bln_aduke-balance-id");
```

<!-- 200 OK -->
```
{
  "balance_id": "bln_aduke-balance-id",
  "ledger_id": "ldg_quickcash-loans-ng-id",
  "identity_id": "idt_aduke-identity-id",
  "currency": "NGN",
  "balance": -8500000,
  "credit_balance": 2000000,
  "debit_balance": 10500000,
  "created_at": "2025-01-15T10:06:00Z",
  "meta_data": {
    "account_number": "0690000031",
    "bank_code": "044",
    "bank_name": "Access Bank",
    "account_type": "wallet"
  }
}
```

![](https://cdn.sanity.io/images/06pses5q/production/d320f16b33451e0cfaa1d566c7216b447fed0f30-3420x2146.png?w=1920&fit=max&auto=format)

The customer's current balance, total debits, total credits, and full transaction history in one view.

Blnk makes this level of visibility possible because all funds — disbursements going out and repayments coming in — flow through a single pooled FBO account in Flutterwave. Flutterwave sees one account. Blnk sees every customer, every loan, and every naira moving between them.

Here's what this tells you (remember, values reflect precision of 100):

-   **balance** (`-8,500,000`): The customer still owes ₦85,000
-   **debit\_balance** (`10,500,000`): Total disbursed + interest — ₦100,000 loan + ₦5,000 interest
-   **credit\_balance** (`2,000,000`): Total repaid so far — ₦20,000

You can also set up a [balance monitor](https://docs.blnkfinance.com/balances/balance-monitoring) to get notified when a loan is fully repaid:

<!-- TypeScript -->
```
const response = await blnk.BalanceMonitors.create({
  balance_id: 'bln_aduke-balance-id',
  condition: {
    field: 'balance',
    operator: '>=',
    value: 0,
    precision: 100,
  },
  description: 'Loan fully repaid - Aduke Okonkwo',
});
```

<!-- Go -->
```
monitor, resp, err := client.BalanceMonitor.Create(blnkgo.MonitorData{
  BalanceID: "bln_aduke-balance-id",
  Condition: map[string]any{
    "field": "balance",
    "operator": ">=",
    "value": 0,
    "precision": 100,
  },
  Description: "Loan fully repaid - Aduke Okonkwo",
})
```

<!-- Python -->
```
response = blnk.balance_monitor.create({
  "balance_id": "bln_aduke-balance-id",
  "condition": {
    "field": "balance",
    "operator": ">=",
    "value": 0,
    "precision": 100,
  },
  "description": "Loan fully repaid - Aduke Okonkwo",
})
```

<!-- Java -->
```
ApiResponse<JsonNode> response = blnk.balanceMonitor().create(
  CreateBalanceMonitor.create()
    .balanceId("bln_aduke-balance-id")
    .condition(Map.of("field", "balance", "operator", ">=", "value", 0, "precision", 100))
    .description("Loan fully repaid - Aduke Okonkwo"));
```

When the balance hits zero or above, Blnk sends a balance.monitor[webhook](https://developer.flutterwave.com/docs/webhooks). Use this to mark the loan as completed in your app, notify the customer, or unlock eligibility for a new loan.

## \# Extending to other countries

Everything we've built focuses on NGN, but the same logic applies to every country Flutterwave supports. The entire product workflow — onboarding, disbursement with inflight, fee recording, virtual account repayments, balance tracking — works identically regardless of currency.

That means expanding to a new market takes drastically less time than building the first one when you use Blnk.

To launch QuickCash in Ghana, for example, you:

1.  **Swap the currency** in your Blnk balances and transactions to `"currency": "GHS"`
2.  **Create a new ledger** for GHS customers (e.g., "QuickCash Loans — Ghana")
3.  **Use Ghana-specific bank codes** from Flutterwave's bank list endpoint for payouts
4.  **Create separate internal balances per currency** (e.g., `@CustomerLoans-GHS`, `@QuickCashRevenue-GHS`) to keep your books organized

That's it. The Blnk side doesn't change. Ledgers, balances, transactions, inflight, balance monitors — they all work the same way. The only things that vary are the Flutterwave endpoints, country-specific bank codes, and virtual account availability (currently supported for NGN and GHS).

Flutterwave supports [bank transfers](https://developer.flutterwave.com/v3.0.0/docs/bank-account) in GHS, KES, GBP, EUR, ZAR, and more — so you can keep expanding with the same ledger design.

## \# What else can you build?

With this foundation in place, you can take QuickCash further:

-   **Interest accrual:** Schedule periodic transactions from the customer's balance to `@QuickCashRevenue-NGN` based on the loan terms. Blnk's [scheduling features](https://docs.blnkfinance.com/transactions/scheduling) can help automate this.
-   **Late fees:** Use [balance monitors](https://docs.blnkfinance.com/balances/balance-monitoring) to detect overdue loans and automatically apply penalty charges
-   **Credit scoring:** Use the customer's repayment history (tracked in Blnk) to adjust eligibility and loan limits
-   **Multiple loan products:** Use [metadata](https://docs.blnkfinance.com/metadata/overview) or separate ledgers to distinguish between personal loans, business loans, and buy-now-pay-later

Ready to start building? Check out the [Blnk docs](https://docs.blnkfinance.com) and the [Flutterwave v3 API reference](https://developer.flutterwave.com/v3.0.0/docs/) to dive deeper.

## Related posts

-   [
    
    Sep 11, 2026Build
    
    ### Building a correct inventory system on a double-entry ledger
    
    Praise Philemon
    
    ](https://blnkfinance.com/blog/building-an-inventory-system-with-a-ledger)
-   [
    
    Sep 7, 2026Build
    
    ### Why one payment does not always equal one transaction.
    
    Emmanuella Etop-Essien
    
    ](https://blnkfinance.com/blog/why-one-payment-does-not-always-equal-one-transaction)
-   [
    
    Sep 3, 2026Build
    
    ### Building a BaaS Ledger, Part 2: Transaction Workflows
    
    Praise Philemon
    
    ](https://blnkfinance.com/blog/how-to-build-a-baas-ledger-part-2)
