As a developer building for African markets, you’ve likely done everything by the book. You’ve implemented standard security checks like the Address Verification System (AVS), your checkout flow is PCI DSS compliant, and you think you’ve built a solid wall against fraudsters. Yet, you still notice a worrying trend of chargeback rates being high, particularly from customers in specific countries. You’ve tried blocking some obvious fraud, but sophisticated attacks are still slipping through. What’s going on?
This blog post is for developers who want to move beyond basic fraud detection mechanisms and build a more resilient defense layer for local types of fraud in Nigeria. You’ll learn how to leverage Flutterwave’s powerful and flexible APIs and dashboard tools to detect and neutralize threats unique to Africa, turning your payment integration into an intelligent, adaptive fraud detection system.
How Local Fraud Is Carried Out
The nature of fraud is that it can be carried out in various ways, and it differs from region to region. The methods, motivations, and social dynamics that define a scam in Nigeria are different from those in Kenya or South Africa. You can’t exactly use the same fraud detection strategies trained on global data from America or Europe to prevent the different kinds of fraud we have in Africa. It might catch the obvious, but not everything.
Most of the time, fraud attacks are caused by simple user habits like using weak passwords, clicking suspicious links, or poor security practices on mobile devices. These behaviors create vulnerable entry points for attackers. In Nigeria, for instance, these lapses are exploited through sophisticated social engineering like romance scams or business email compromise, which prey on human psychology.
To build a proper local defense, you first need to understand the local attacker. Let’s look at a case study of fraud patterns in Nigeria.
Case Study of Fraud Patterns in Nigeria
Many things come to mind when people think of fraud in Nigeria. One of the most popular scams in Nigeria is Advance-Fee Fraud (AFF) or "419" scam. These kinds of scams involve using social engineering to manipulate people into divulging confidential information or sending money. The most popular forms of this kind of fraud today are:
Romance and Dating Scams: In this type of scam, a fraudster creates a fake online profile, often impersonating a notable person. They then spend a significant amount of time building a trusted relationship with their target. Once trust is established, they fabricate a crisis, such as a medical emergency or a business problem, and request financial assistance.
Business Email Compromise (BEC): This kind of scam involves attackers impersonating a company executive or a trusted vendor. They then deceive an employee or a client into making an unauthorized wire transfer to an account the attacker controls.
Key Data Signals for Developers
To counter these social engineering tactics, you must translate these human behaviors into detectable data signals. The schemes described above, while social in nature, leave digital footprints. Here are the key fraud signals you should watch for:
-
Cross-Border Mismatch: A primary target for these scams are foreigners. This often results in transactions where the merchant is in Nigeria, but the payment originates from a non-Nigerian card or IP address. While not inherently fraudulent, this
ip_country != card_country
mismatch is a strong signal when combined with other factors. - New User, High-Value, Urgent Shipping: A classic pattern for romance or merchandise scams involves a newly created user account making an unusually high-value purchase and requesting expedited shipping. The fraudster wants to get the goods or cash out before the real cardholder notices.
- Payment Method as a Signal: Fraudsters often prefer payment methods that are final and hard to dispute, such as bank transfers. A new user from a high-risk region opting for a bank transfer over a card payment for a large amount could be a red flag.
- Identity Deception: The use of fake profiles means you might see a mismatch between the name on the account and the name on the payment card. The email address might also be a signal, for example, an email with a high percentage of numbers or a suspicious domain.
How do you then improve your fraud detection patterns to catch local fraud? Let’s look at some ways to do this below.
Your First Line of Defense with Flutterwave
You've seen how to identify key fraud signals. Now, let’s look at how you can establish your first layer of defense. Before implementing a complex solution for local fraud detection, you need to establish your first layer of defense.
Your first layer of protection should be simple, broad, and easy to implement. Flutterwave provides you with core security features embedded into its payment processing engine, allowing you, as the developer, to implement more fine-grained fraud detection. Here are some ways you can use Flutterwave as your first line of defense:
Leveraging Data from Flutterwave API Responses
By leveraging the data from Flutterwave’s API responses, you can build a customized anti-fraud defense. For every transaction request, the Flutterwave API returns a response packed with valuable data. You can use this information to build your own machine learning risk-scoring and decision-making logic in your application's backend.
Instead of relying on a UI, you can write code to analyze these key data points:
- Geographic Information: The API response for card payments contains the card's country, which you can compare with the user’s IP address. Does a card from Germany being used from an IP address in a high-risk country make sense for your business? This allows you to programmatically detect the "Geographic Mismatch" we discussed earlier.
- Customer Information: The customer's email and name fields can be analyzed. Does the email look disposable? Does the name on the account match the name provided for the card?
- Transaction Behavior: You can log transaction data on your end. Is it the same IP address attempting multiple transactions with different cards in a short period? This is a classic velocity check (more on this later) that you can build yourself using the data provided by Flutterwave.
Verifying Transactions Server-Side
Never, ever trust a callback from the client-side to confirm a successful payment. A malicious actor can easily fake this request. The only source of truth is a direct, server-to-server verification with Flutterwave's API.
After a customer completes a payment on your frontend, your client-side code will receive a transaction_id
. Your backend must use this ID to call the Flutterwave transaction verification endpoint. Before you provide any goods or services, your server must confirm that the status is "successful" and that the amount and currency match the intended order value.
Here’s how you can do it in Node.js:
// Server-Side Verification
const axios = require('axios');
async function verifyPayment(transactionId) {
try {
const url = `https://api.flutterwave.com/v3/transactions/${transactionId}/verify`;
const response = await axios.get(url, {
headers: {
'Authorization': `Bearer ${process.env.FLW_SECRET_KEY}`
}
});
const { status, currency, amount, customer } = response.data.data;
// Your business logic to confirm the order details
const orderAmount = 1500;
const orderCurrency = "NGN";
if (status === "successful" && amount === orderAmount && currency === orderCurrency) {
console.log("Payment successful. Fulfilling order for:", customer.email);
// Grant value to the customer here
} else {
console.log("Payment verification failed or details mismatch.");
// Do not grant value
}
} catch (error) {
console.error("An error occurred during verification:", error.response? error.response.data : error.message);
}
}
Hardening Your Payouts and Access Controls
The last thing you want is a compromised API key. An attacker could use this to drain your Flutterwave balance via the payouts API. The features in your Flutterwave dashboard are designed to prevent exactly this. Think of them as architectural decisions that limit what an attacker can do, even if they breach your initial defenses. To secure your payouts and admin access, here are three essential steps you can activate directly from your Flutterwave dashboard:
IP Whitelisting For API Payouts: This is the most important defense for payouts. This ensures that payout requests can only be initiated from a list of trusted IP addresses you specify. If an attacker accesses your API key and tries to use it from their own server, the request will fail. To set it up, log into your Flutterwave Dashboard → Settings → Whitelisted IP Addresses → Add Whitelist IP. Add the static IP addresses of your backend servers.
Toggle for Payout Source: If you don't use the Flutterwave API for payouts, turn it off. This simple toggle completely removes an entire attack vector. A feature that doesn't exist can't be exploited. To set it up, go to your Dashboard → Settings → Business Preference → Security. Under "Transfer Preferences," select "Enable Transfers via Dashboard only."
Enabling 2-Factor Authentication (2FA): Enabling 2FA for both logins and transfers protects your dashboard from unauthorized access due to stolen credentials from phishing or other breaches. It adds a second layer of security, requiring something the user has (like their phone) in addition to something they know (their password). You can set it up by navigating to your Dashboard → Settings → Business Preference → Security. Enable 2FA for Transfers and choose your preferred method.
You’ve seen how you can improve your fraud screening and some ways to prevent fraud by using Flutterwave. Let’s look at something more advanced.
How to Build a Custom Analytics Layer for Local Fraud Detection
With the secure foundation you’ve built with Flutterwave, you can now get proactive in creating advanced rules for local threats by building a custom Analytics layer. A custom analytics layer is an intelligent system that sits on top of your payment gateway to screen for threats specific to your business.
With a generic fraud screen check, there is no memory/state, so transactions are screened individually. To counter localized threats, which often involve patterns, you need a stateful system with memory that remembers past events and uses that history to spot suspicious behavior that a single transaction might not reveal.
The Architecture: Webhooks + Your Database = Real-Time Intelligence
The architecture for this custom layer is simple and powerful. It consists of two main components:
- Flutterwave Webhooks: These are your real-time event streams. When a transaction happens, such as a chargeback is initiated or a transfer is completed, Flutterwave sends a notification to your webhook endpoint. This is the trigger for your custom logic.
- Your Database: This is your system's memory. It can be a fast in-memory store like Redis for temporary data (perfect for velocity checks) or a persistent relational database like PostgreSQL for long-term data (like blacklists).
The flow is straightforward: A Flutterwave webhook fires, your API endpoint receives it, verifies the signature, and then reads from or writes to your database to enforce your custom rules. Let’s build out this flow, starting with implementing a dynamic blacklist for blacklisting fraudulent IP addresses.
Implementing Dynamic Blacklists
A dynamic blacklist is a list of items, like IP addresses, that are automatically added and removed based on some predefined criteria. We need a dynamic blacklist to store IP addresses associated with a chargeback. Here is how to set it up:
Step 1: Set Up a **blacklist**
***Table in* Your Database
Create a blacklist table in your database that allows you to categorize and store blacklisted email or IP.
CREATE TABLE blacklist (
id SERIAL PRIMARY KEY,
entity_type VARCHAR(50) NOT NULL, -- e.g., 'email', 'ip_address'
entity_value VARCHAR(255) NOT NULL UNIQUE,
reason VARCHAR(255),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
Step 2: Create a Webhook Handler to Listen for Chargeback Events
When you receive a chargeback.initiated
webhook, your handler should parse the payload and add the customer's email and the transaction's IP address to your blacklist table.
Here's a snippet demonstrating the logic (this logic assumes webhook verification is set up and database connection is established):
# Dynamic Blacklisting Webhook Handler
const express = require("express");
const { Pool } = require("pg");
const app = express();
app.use(express.json());
// Configure your PostgreSQL connection
const pool = new Pool({
// your user, host, database, password, port
});
async function addToBlacklist(entityType, entityValue, reason) {
const client = await pool.connect();
try {
const query = `
INSERT INTO blacklist (entity_type, entity_value, reason)
VALUES ($1, $2, $3)
ON CONFLICT (entity_value) DO NOTHING
`;
await client.query(query, [entityType, entityValue, reason]);
console.log(`Successfully blacklisted ${entityType}: ${entityValue}`);
} catch (error) {
console.error(`Error adding to blacklist: ${error}`);
} finally {
client.release();
}
}
app.post("/flw-webhook", async (req, res) => {
const payload = req.body;
const eventType = payload.event;
if (eventType === "chargeback.initiated" && payload.data) {
const { customer, ip_address: ipToBlacklist } = payload.data;
const emailToBlacklist = customer?.email;
if (emailToBlacklist) {
await addToBlacklist("email", emailToBlacklist, "Chargeback initiated");
}
if (ipToBlacklist) {
await addToBlacklist("ip_address", ipToBlacklist, "Chargeback initiated");
}
}
res.status(200).json({ status: "success" });
});
Now, in your application's checkout flow, before you even initiate a payment with Flutterwave, you should check if the customer's email or IP address is in your blacklist.
Implementing Velocity Checks
Velocity checks are your best defense against automated attacks. Fraudsters rely on speed, performing actions like card testing (trying many stolen card numbers quickly) or rapid cash-outs after an account takeover. Velocity checks turn this speed against them by monitoring the rate of specific events over a set period.
The core components of a velocity check are:
- Data Element: The thing you're counting (e.g., IP address, card number, user ID).
- Quantity: The threshold that triggers an alert (e.g., 10 attempts).
- Timeframe: The window for the check (e.g., 1 hour).
Redis is an excellent tool for implementing velocity checks due to its high speed and ability to set expirations on keys. Here is how a Velocity check is implemented:
import Redis from 'ioredis';
// Assumes Redis is running on localhost. Configure as needed.
const redis = new Redis();
const ONE_HOUR = 3600; // in seconds
const TEN_MINUTES = 600;
/**
* Detects Card Testing.
* Blocks an IP if it uses too many different cards within an hour.
*/
export async function checkCardVelocity(ipAddress, cardId) {
const key = `velocity:ip:${ipAddress}:cards:1h`;
const cardLimit = 5; // Max 5 unique cards per IP per hour
const [currentCount] = await redis.multi()
.scard(key) // Get current number of unique cards
.sadd(key, cardId) // Add the new card to the set
.expire(key, ONE_HOUR) // Reset the 1-hour expiration
.exec();
if (currentCount[1] > cardLimit) {
return { decision: 'BLOCK', reason: 'Card velocity limit exceeded.' };
}
return { decision: 'ALLOW' };
}
/**
* Prevents Rapid ATO Cash-out.
* Flags a user spending too much money too quickly.
*/
export async function checkSpendingVelocity(userId, amount) {
const key = `velocity:user:${userId}:amount:10m`;
const spendingLimit = 50000; // e.g., 50,000 NGN in 10 minutes
const totalSpent = await redis.incrby(key, amount);
if (totalSpent === amount) {
// If this is the first spend, set the expiration for the window
await redis.expire(key, TEN_MINUTES);
}
if (totalSpent > spendingLimit) {
return { decision: 'FLAG_FOR_REVIEW', reason: 'Spending velocity exceeded.' };
}
return { decision: 'ALLOW' };
}
/**
* Limits Failed Transactions.
* Temporarily locks an account after too many failed payment attempts.
*/
export async function checkFailureVelocity(userId) {
const key = `velocity:user:${userId}:failures:1h`;
const failureLimit = 10;
const failureCount = await redis.incr(key);
if (failureCount === 1) {
// On the first failure, set the window
await redis.expire(key, ONE_HOUR);
}
if (failureCount > failureLimit) {
return { decision: 'LOCK', reason: 'Too many failed transactions.' };
}
return { decision: 'ALLOW' };
}
By implementing these custom, stateful rules, you build an intelligent defense layer that is specifically tuned to the behaviors you observe in your user base.
Wrapping Up
In this guide, you’ve learned how to create a multi-layered defense: first, by securing your integration with Flutterwave’s core features, and then by building a custom, intelligent layer on top.
By using webhooks to power dynamic blacklists and implementing velocity checks with tools like Redis, you can turn your system from a static gateway into a proactive defense that understands and neutralizes local threats.
If you’re ready to build a secure payment experience, check out Flutterwave today.
Top comments (1)
Thank you