Customers expect payments to reflect instantly, even when traditional banking processes slow down transactions. It gets even trickier for companies handling a high volume of transactions across multiple payment methods (cards, bank transfers, and mobile money).
In addition to that, fraud and financial discrepancies are common challenges. Many issues come from irregularities in transaction records, making it harder to detect errors or suspicious activity. With payment reconciliation, you can automatically match transactions, identify inconsistencies, make informed decisions, and comply with financial regulations.
However, reconciling bank transfers in real-time requires a blend of robust API integrations, automated webhooks, and smart validation logic.
In this guide, you’ll learn how to implement live reconciliation for bank transfers using Flutterwave’s APIs and webhooks. We’ll cover validating transaction references, updating user balances automatically, and handling edge cases like partial payments.
Before diving into the implementation details, let’s cover the basics of payment reconciliation.
What is Payment Reconciliation?
Payment reconciliation is the process of comparing financial records to confirm that payments made or received match what is recorded in bank statements or a business’s accounting books. For example, an accounting department might reconcile payments by comparing its official bank statement with recorded costs and payments from the past two weeks.
This process helps keep accurate financial records and improve cash flow management. It often involves managing multiple bank accounts, various payment methods, different currencies, and even cross-border transactions. Depending on the business, reconciliation can be done daily, weekly, or monthly. Small businesses with just a few bank records and low transaction volumes can typically handle this using standard accounting software. In contrast, larger organizations with multiple outlets and numerous transactions often face greater challenges in aligning their internal records.
Types of payment reconciliation depend on factors such as the payment method and account type. However, the payment reconciliation process generally falls into the following categories:
- Bank Reconciliation: Compares the transactions recorded in an organization’s cash account with those shown on the bank statement. Businesses use this to verify their available cash balance.
- Intercompany Reconciliation: A type of reconciliation performed by businesses with multiple subsidiaries or divisions. It involves matching and reconciling transactions across different entities within the same corporate group.
- Account Payable Reconciliation: Involves matching supplier invoices and payments with transaction records in the accounts used to make payments. This helps businesses track amounts owed to suppliers and make timely payments.
- Payroll Reconciliation: Compares wages, taxes, deductions, and other payroll-related transactions with bank statements and supporting documents to confirm accuracy.
- Account Receivable Reconciliation: Involves matching customer payments with issued invoices. This helps businesses identify and address discrepancies, such as underpayments or overpayments.
How does Payment Reconciliation Work?
The process of payment reconciliation varies from business to business but generally involves the following steps:
- Collect relevant financial reporting documents and records*.*
- Compare transactions in the accounting system with those on the bank financial statements.
- Identify discrepancies caused by errors, timing differences, or potential fraud.
- Investigate discrepancies to determine their causes and resolve them.
- Record adjustments based on the resolved discrepancies.
- Verify the adjusted records to confirm they align with the bank statement.
- Document the reconciliation process for audit and compliance purposes.
- Review and approve the final reconciliation to ensure completeness and accuracy.
While the reconciliation process may seem straightforward, reconciling bank transfers can be challenging because it falls outside your direct control. Bank transfers require customers to send funds from their bank account to your designated business account, which limits your ability to track payment transactions in real time. As a result, real-time reconciliation becomes difficult.
In addition to the lack of control, bank transfer payments present several challenges:
- Delayed payment reflections: Bank processing times can cause delays before payments appear in your account.
- Incorrect payment references: Customers may enter the wrong payment reference, making it harder to match payments to invoices.
- Installment payments: Customers might pay in installments, complicating the reconciliation process.
To address these challenges, you can use Flutterwave to automate the reconciliation process.
How to Automate Live Reconciliation with Flutterwave
A major reason why reconciling bank transfers is challenging is that it is often beyond your control. From the customer’s perspective, uploading proof of payment should immediately grant them value for their money. This raises important questions. What happens when payment confirmation is delayed, especially if your system is programmed to release value only after receiving confirmation from your bank endpoint? What if the transfer is reversed just seconds after the customer submits their payment confirmation?
This uncertainty makes the reconciliation and confirmation process frustrating for your customers and business. However, you can manage bank transfers in a controlled environment that allows customers to transact securely while enabling your business to reconcile payments in real-time.
You can use Flutterwave Virtual Accounts to generate account details (account number and bank) to securely receive payments into your Flutterwave balance via bank transfers. With this solution, you can create a general account for all customer payments or customer-specific accounts for more targeted reconciliation.
There are two types of virtual accounts you can create with Flutterwave:
- Dynamic Virtual Account: A temporary account typically used for one-time transactions. These accounts are valid only within a set timeframe or until payment is completed. For example, if you operate an e-commerce platform, you can generate a unique dynamic virtual account when a customer selects the "Pay via Bank Transfer" option.
- Static Virtual Account: A long-term account ideal for collecting recurring payments, such as subscriptions. If you run a software-as-a-service (SaaS) platform, you can assign each customer a permanent virtual account for monthly subscription payments. You can also create a static virtual account tied to a specific product that all customers can pay into.
Before diving into the technical implementation, let’s consider a scenario:
Imagine you have a customer named John Doe who subscribes to your monthly wellness program and prefers to pay via bank transfer. In this case, you want to use Flutterwave to create a static virtual account for John Doe to process his monthly subscription. To do this, follow the steps below.
Set up Webhook to Monitor Transfer
Before creating a virtual account, you need to set up your webhook endpoint on Flutterwave. This allows your system to receive alerts for any changes to the virtual account you create.
To set this up, log in to your Flutterwave dashboard and navigate to the Webhooks section under the Settings menu. Add your webhook endpoint, secret hash, and then save. The secret hash allows you to verify that incoming requests are from Flutterwave.
This implementation uses Go, but the approach applies to any other programming language.
Generate a Virtual Account
With your webhook setup, create a virtual account that automatically maps transactions to specific users.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
// Request payload matching Flutterwave's API spec
type VirtualAccountRequest struct {
Email string `json:"email"`
TxRef string `json:"tx_ref"`
Phonenumber string `json:"phonenumber"`
IsPermanent bool `json:"is_permanent"` // Maps to "is_permanent" in JSON
Firstname string `json:"firstname"`
Lastname string `json:"lastname"`
Narration string `json:"narration"`
BVN string `json:"bvn"`
}
// Response structure (simplified example)
type VirtualAccountResponse struct {
Status string `json:"status"`
Message string `json:"message"`
Data struct {
AccountNumber string `json:"account_number"`
BankName string `json:"bank_name"`
FlwRef string `json:"flw_ref"`
} `json:"data"`
}
func CreateVirtualAccount() (*VirtualAccountResponse, error) {
// Construct request payload
payload := VirtualAccountRequest{
Email: "john.doe@sample.com",
TxRef: "apex_tx_ref-002201",
Phonenumber: "08100000000",
IsPermanent: true,
Firstname: "John",
Lastname: "Doe",
Narration: "Wellness program - January",
BVN: "1234567890",
}
// Marshal payload to JSON
jsonData, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("failed to marshal payload: %v", err)
}
// Create HTTP request
req, err := http.NewRequest(
"POST",
"https://api.flutterwave.com/v3/virtual-account-numbers",
bytes.NewBuffer(jsonData),
)
if err != nil {
return nil, fmt.Errorf("failed to create request: %v", err)
}
// Set headers
req.Header.Set("Authorization", "Bearer YOUR_SECRET_KEY") // Replace with your key
req.Header.Set("Content-Type", "application/json")
// Send request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %v", err)
}
defer resp.Body.Close()
// Read response
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %v", err)
}
// Unmarshal JSON response
var result VirtualAccountResponse
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to decode response: %v", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API error: %s (HTTP %d)", result.Message, resp.StatusCode)
}
return &result, nil
}
The snippet above does the following:
- Create structs to represent the request and response payloads.
- Sends a request to the create virtual account endpoint with the required data. An important parameter to note in the payload is
IsPermanent: true
, which means the virtual account created is permanent.
On a successful request, Flutterwave returns a unique account number, bank details, and other information ready to receive payments.
{
"status": "success",
"message": "Virtual account created",
"data": {
"response_code": "02",
"response_message": "Transaction in progress",
"flw_ref": "FLW-d3f049dad861484a9e1504309e07f615",
"order_ref": "URF_1726598678168_8608035",
"account_number": "9949162075",
"frequency": "N/A",
"bank_name": "WEMA BANK",
"created_at": "2024-09-17 18:44:38",
"expiry_date": "2024-09-17 19:44:38",
"note": "Please make a bank transfer to testing functions FLW",
"amount": "101.40"
}
}
Automating Reconciliation with Webhooks
Now that John Doe can send payments, the next step is to detect and process financial transactions in real-time. Flutterwave provides webhooks that notify your backend when a payment is received.
To do this, first define a webhook struct as shown below:
type WebhookEvent struct {
Event string `json:"event"` // e.g., "charge.completed"
Data struct {
ID int `json:"id"`
TxRef string `json:"tx_ref"` // Your unique reference
FlwRef string `json:"flw_ref"` // Flutterwave's internal reference
Amount float64 `json:"amount"` // Expected amount
ChargedAmount float64 `json:"charged_amount"`
Currency string `json:"currency"` // e.g., "NGN"
Status string `json:"status"` // "successful"
PaymentType string `json:"payment_type"` // "bank_transfer"
CreatedAt string `json:"created_at"` // ISO 8601 timestamp
AccountID int `json:"account_id"` // Virtual account ID
Customer struct {
Email string `json:"email"`
PhoneNumber string `json:"phone_number"`
} `json:"customer"`
} `json:"data"`
EventType string `json:"event.type"` // e.g., "BANK_TRANSFER_TRANSACTION"
}
Next, create a function to handle the incoming webhook:
func WebhookHandler(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
// 1. Verify signature (critical for security)
secretHash := os.Getenv("FLW_WEBHOOK_HASH")
signature := r.Header.Get("x-flutterwave-signature")
body, _ := io.ReadAll(r.Body)
hash := hmac.New(sha256.New, []byte(secretHash))
hash.Write(body)
expectedSignature := hex.EncodeToString(hash.Sum(nil))
if signature != expectedSignature {
w.WriteHeader(http.StatusUnauthorized)
return
}
// 2. Parse payload
var event WebhookEvent
if err := json.Unmarshal(body, &event); err != nil {
w.WriteHeader(http.StatusBadRequest)
log.Printf("Failed to parse webhook: %v", err)
return
}
// 3. Handle events
switch event.Event {
case "charge.completed":
if event.Data.Status == "successful" {
handleSuccessfulPayment(event.Data.TxRef, event.Data.Amount)
}
case "charge.failed":
handleFailedPayment(event.Data.TxRef)
}
w.WriteHeader(http.StatusOK)
}
// Example reconciliation logic
func handleSuccessfulPayment(txRef string, amount float64) {
userID := extractUserIDFromTxRef(txRef) // a helper function to retrieve user ID from the transaction reference
// Atomic balance update (pseudo-code)
err := db.Exec(
"UPDATE users SET balance = balance + $1 WHERE id = $2",
amount, userID,
)
if err != nil {
log.Printf("Failed to update balance: %v", err)
}
}
func handleFailedPayment(txRef string) {
log.Printf("Payment failed for transaction %s. Notify user or retry logic here.", txRef)
}
The snippet above does the following:
- Creates a
WebhookHandler
function that verifies the authenticity of the webhook payload by checking the secret hash to confirm it is coming from Flutterwave. It also checks the payload event to determine if thecharge.completed
is present and treats it as a successful transaction. - Creates a
handleSuccessfulPayment
helper function that reconciles the database based on the amount and the matching user ID. - Creates a
handleFailedPayment
helper function that logs the failed transaction.
Note that in a production environment, you would notify the user, queue for retry, and update the status of the transaction (or any other financial data) in the database when the payment fails.
Handling Partial Payments
In an ideal world, you would expect users to pay the specified amount, but that isn’t the case most times. You may encounter users attempting to game the system by sending a partial amount, hoping to receive value.
To address this issue, first create a struct to track the subscription amount:
type Transaction struct {
TxRef string `db:"tx_ref"`
UserID string `db:"user_id"`
ExpectedAmt float64 `db:"expected_amount"`
ReceivedAmt float64 `db:"received_amount"`
Status string `db:"status"` // "pending", "partial", "completed"
}
Next, update the handleSuccessfulPayment
helper function to compare the expected amount with the received amount:
func handleSuccessfulPayment(txRef string, receivedAmount float64) {
// 1. Get transaction details from DB
var tx Transaction
err := db.Get(&tx, "SELECT * FROM transactions WHERE tx_ref = $1", txRef)
if err != nil {
log.Printf("Transaction not found: %s", txRef)
return
}
// 2. Check if payment matches expected amount
if receivedAmount < tx.ExpectedAmt {
log.Printf("Partial payment for %s: Received %.2f (Expected %.2f)",
txRef, receivedAmount, tx.ExpectedAmt)
// Update DB with partial amount (atomic operation)
_, err := db.Exec(`
UPDATE transactions
SET received_amount = received_amount + $1,
status = 'partial'
WHERE tx_ref = $2`,
receivedAmount, txRef)
if err != nil {
log.Printf("Failed to update partial payment: %v", err)
return
}
// Notify user (e.g., email/SMS)
notifyPartialPayment(tx.UserID, receivedAmount)
return
}
// 3. Full payment received
_, err = db.Exec(`
UPDATE transactions
SET received_amount = $1,
status = 'completed'
WHERE tx_ref = $2`,
receivedAmount, txRef)
if err != nil {
log.Printf("Failed to mark payment complete: %v", err)
}
}
func notifyPartialPayment(userID string, receivedAmount float64) {
log.Printf("⚠️ Partial payment of %.2f received from user %s. Remaining balance pending. Notify user to complete payment.",
receivedAmount,
userID,
)
}
Wrapping Up
Automating bank transfer reconciliation helps maintain smooth operations, reduces manual errors, and improves customer satisfaction. By using Flutterwave’s virtual accounts and webhooks, you can build a real-time reconciliation system that:
- Instantly reflects payments in user accounts
- Handles delayed or partial payments seamlessly
- Prevents mismatches and fraud
Check out these resources to learn more:
Top comments (1)
👍