Key fraud signals in Paystack transaction data: (1) risk_action — Paystack's own risk score: "allow", "deny", or "review"; (2) ip_address — the customer's IP at charge time; (3) gateway_response — card network response messages like "Do Not Honor" or "Stolen Card"; (4) channel — card charges carry more fraud risk than bank transfers; (5) authorization.reusable — a card that suddenly becomes reusable without prior subscription setup is suspicious; (6) attempts — multiple failed attempts before success on the same reference indicate probing.
When you call GET /transaction/verify/:reference or receive a charge.success webhook, the transaction object contains several fraud-relevant fields:
| Field | Location | What It Signals |
|---|---|---|
risk_action |
Top-level | Paystack's internal risk score: "allow", "deny", or "review". "review" means proceed with caution. |
ip_address |
Top-level | Customer's IP at time of charge. Velocity-check this against recent failed attempts. |
gateway_response |
Top-level | Raw card network response: "Approved", "Do Not Honor", "Stolen Card", "Lost Card", "Restricted Card". |
channel |
Top-level | Payment method: "card", "bank", "ussd", "mobile_money". Card has highest fraud surface. |
authorization.bin |
Nested | First 6 digits of card. Cross-reference against known fraud BINs from your history. |
authorization.country_code |
Nested | Card-issuing country. Mismatch with customer's IP country is a weak fraud signal. |
authorization.reusable |
Nested | If true and the customer has no subscription, the card may have been saved without consent. |
// fraud-score.js
var blockedIPs = new Set(); // Load from Redis or DB
function scoreFraud(transaction) {
var score = 0;
var flags = [];
// Hard signals from Paystack risk engine
if (transaction.risk_action === 'deny') {
score += 100;
flags.push('paystack_risk_deny');
}
if (transaction.risk_action === 'review') {
score += 20;
flags.push('paystack_risk_review');
}
// Gateway response signals
var badResponses = ['Do Not Honor', 'Stolen Card', 'Lost Card', 'Restricted Card', 'Pick Up Card'];
if (badResponses.some(r => transaction.gateway_response?.includes(r))) {
score += 25;
flags.push('bad_gateway_response:' + transaction.gateway_response);
}
// IP velocity signals
if (blockedIPs.has(transaction.ip_address)) {
score += 30;
flags.push('blocked_ip');
}
// Channel signals — card on large amount
if (transaction.channel === 'card' && transaction.amount > 5000000) { // NGN 50,000
score += 10;
flags.push('large_card_charge');
}
// Country mismatch
var cardCountry = transaction.authorization?.country_code;
var ipCountry = resolveIPCountry(transaction.ip_address); // your GeoIP lookup
if (cardCountry && ipCountry && cardCountry !== ipCountry) {
score += 15;
flags.push('country_mismatch:' + cardCountry + '_vs_' + ipCountry);
}
return { score, flags, action: score >= 50 ? 'hold' : score >= 20 ? 'log' : 'pass' };
}
// Use in webhook handler
if (event.event === 'charge.success') {
var fraud = scoreFraud(event.data);
if (fraud.action === 'hold') {
await db.orders.update({ id: orderId, status: 'fraud_hold', fraud_flags: fraud.flags });
await alertTeam('Fraud hold on order ' + orderId + ' score ' + fraud.score);
// Do NOT fulfill the order yet
} else {
await fulfillOrder(orderId);
}
}
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
This guide is part of the Paystack security guide.
KES 2,999
The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.
Start Paystack Course