
You are building a loan app and you've picked Flutterwave 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, 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 with API keys
- A running Blnk instance (self-hosted or Blnk Cloud)
- Basic familiarity with REST APIs and webhooks
This guide uses the Flutterwave v3 API 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 and mapping out your internal balances.
To create a ledger for QuickCash customers:
```
--CODE language-bash--
curl -X POST "http://localhost:5001/ledgers" \
-H "Content-Type: application/json" \
-d '{
"name": "QuickCash Loans — Nigeria",
"meta_data": {
"description": "Ledger for QuickCash customers in Nigeria"
}
}'
```
```
--CODE language-json--
// Response
{
"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:

- 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. 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 in Blnk to represent them on the ledger:
```
--CODE language-bash--
curl -X POST "http://localhost:5001/identities" \
-H "Content-Type: application/json" \
-d '{
"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"
}
}'
```
```
--CODE language-json--
// Response
{
"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 linked to the identity. This is where Blnk tracks everything the customer owes and repays.
```
--CODE language-bash--
curl -X POST "http://localhost:5001/balances" \
-H "Content-Type: application/json" \
-d '{
"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"
}
}'
```
```
--CODE language-json--
// Response
{
"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.

3. Set up a virtual account for repayments
Finally, create a static virtual account 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.
```
--CODE language-bash--
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"
}'
```
```
--CODE language-json--
// Response
{
"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.

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:
- Record an inflight transaction in Blnk from the customer to
@CustomerLoans-NGN - Initiate the payout via Flutterwave's Transfer API
- Listen for Flutterwave's webhook
- 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.
```
--CODE language-bash--
curl -X POST "http://localhost:5001/transactions" \
-H "Content-Type: application/json" \
-d '{
"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
}
}'
```
```
--CODE language-json--
// Response
{
"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"
}
```

Here's what happens:
- The customer's balance enters a held state — the debit is recorded as inflight, not yet applied
allow_overdraftis set totruebecause the customer's balance starts at zero and needs to go negative to reflect the debt- The transaction stays in
INFLIGHTstatus until you commit or void it - The
referenceis the thread connecting this Blnk transaction to the Flutterwave payout
Blnk uses 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 using the same reference:
```
--CODE language-bash--
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"
}'
```
```
--CODE language-json--
// Response
{
"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
NEWstatus - The
referencematches the one in Blnk, linking both records - You'll receive a
transfer.completedwebhook when the transfer succeeds or fails
3. Commit on success
When Flutterwave's webhook confirms the payout succeeded, commit the inflight transaction in Blnk:
```
--CODE language-bash--
curl -X PUT "http://localhost:5001/transactions/inflight/txn_disbursement-blnk-id" \
-H "Content-Type: application/json" \
-d '{
"status": "commit"
}'
```

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:
```
--CODE language-bash--
curl -X PUT "http://localhost:5001/transactions/inflight/txn_disbursement-id" \
-H "Content-Type: application/json" \
-d '{
"status": "void"
}'
```

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:
```
--CODE language-bash--
curl -X POST "http://localhost:5001/transactions" \
-H "Content-Type: application/json" \
-d '{
"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"
}'
```
```
--CODE language-json--
// Response
{
"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-NGNcaptures 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 to your server.
```
--CODE language-json--
{
"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:
```
--CODE language-bash--
curl -X POST "http://localhost:5001/transactions" \
-H "Content-Type: application/json" \
-d '{
"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-NGNis 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.

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:
```
--CODE language-bash--
curl -X GET "http://localhost:5001/balances/bln_aduke-balance-id"
```
```
--CODE language-json--
// Response
{
"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"
}
}
```

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 to get notified when a loan is fully repaid:
```
--CODE language-bash--
curl -X POST "http://localhost:5001/balance-monitors" \
-H "Content-Type: application/json" \
-d '{
"balance_id": "bln_aduke-balance-id",
"condition": {
"field": "balance",
"operator": ">=",
"value": 0,
"precision": 100
},
"description": "Loan fully repaid - Aduke Okonkwo"
}'
```
When the balance hits zero or above, Blnk sends a balance.monitorwebhook. 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:
- Swap the currency in your Blnk balances and transactions to
"currency": "GHS" - Create a new ledger for GHS customers (e.g., "QuickCash Loans — Ghana")
- Use Ghana-specific bank codes from Flutterwave's bank list endpoint for payouts
- 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 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-NGNbased on the loan terms. Blnk's scheduling features can help automate this. - Late fees: Use balance monitors 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 or separate ledgers to distinguish between personal loans, business loans, and buy-now-pay-later
Ready to start building? Check out the Blnk docs and the Flutterwave v3 API reference to dive deeper.
Heading 1
Heading 2
Heading 3
Heading 4
Heading 5
Heading 6
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Ledger architecture refers to how you group your balances to work for your application
Ordered list
- Item 1
- Item 2
- Item 3
Unordered list
- Item A
- Item B
- Item C
Bold text
Emphasis
Superscript
Subscript
