Bonaventure OgetoBy Bonaventure Ogeto|

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:

FieldLocationWhat 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

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