Fraud Signals Available in Paystack Transaction Data
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.
Key Fraud Signal Fields in Paystack Transaction Data
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. |
Building a Fraud Score From Paystack Data
// 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);
}
}
Learn More
This guide is part of the Paystack security guide.
Key Takeaways
- ✓Paystack includes a risk_action field on transaction data: "allow", "deny", or "review". Log this for every transaction.
- ✓The ip_address field identifies the customer's IP. Cross-reference it against blocklists and velocity checks.
- ✓gateway_response strings like "Do Not Honor", "Stolen Card", and "Lost Card" are hard fraud signals — escalate these immediately.
- ✓Card channel transactions carry higher fraud risk than bank transfers. Apply extra scrutiny to card charges for high-value orders.
- ✓A transaction with many prior failed attempts before succeeding suggests probing — flag these for manual review.
- ✓Build a simple fraud score by summing risk points: blocked IP (+30), bad gateway_response (+25), risk_action=review (+20), card channel on large amount (+10).
Frequently Asked Questions
- What does risk_action: "review" mean in practice?
- It means Paystack's fraud engine flagged the transaction as potentially suspicious but did not block it outright. You should log these transactions, consider adding a manual review step before fulfilling high-value orders, and monitor whether "review" transactions have a higher chargeback rate in your data.
- Can I access these fraud fields in the Paystack dashboard?
- Some fields like gateway_response and channel are visible in the transaction detail view. risk_action and ip_address are in the API response. For systematic fraud analysis, pull transaction data via the API and store these fields in your own database for querying.
- Should I block transactions with risk_action: "deny"?
- Paystack already blocks deny-rated transactions before they succeed — a charge.success webhook will not fire for them. If you see risk_action: "deny" in a failed transaction notification, record it. If you proactively check via the API, treat deny as a hard stop.
- What is the most reliable fraud signal in Paystack data?
- gateway_response values like "Stolen Card" and "Lost Card" are the most reliable — these come directly from the card-issuing bank and are almost never false positives. risk_action from Paystack's engine is also reliable. IP velocity (same IP, many different emails, rapid succession) is highly predictive for card testing attacks.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.
Apply to the McTaba Marathon