Stripe Fraud Detection: Pattern Analysis Guide
Updated October 2026
Introduction to Fraud Pattern Detection in Stripe
Fraud detection is one of the most critical aspects of running an online business. Every fraudulent transaction not only costs you the product or service value but also results in chargeback fees, increased processing costs, and potential damage to your merchant account standing. For businesses processing payments through Stripe, understanding how to identify and analyze fraud patterns is essential for protecting revenue and maintaining customer trust.
To find the rows in your own data that do not fit the usual pattern, try the free anomaly detection tool.
This tutorial walks through detecting suspicious patterns in your Stripe charges with Python and pandas: velocity, geographic mismatches and unusual amounts. You will build review signals, learn how to calibrate them against your own confirmed fraud and dispute outcomes, and see where Stripe Radar fits. It is a Python tutorial: you need Python 3 with the stripe, pandas, numpy and scipy packages. The output blocks come from a synthetic data set generated for this tutorial (about 560 made-up charges with a few planted card-testing bursts), not from a real store, so your counts will differ.
Fraud patterns in payment processing typically manifest in several ways: unusually high transaction velocity from a single source, geographic mismatches between customer data and transaction origin, systematic testing of stolen card numbers, and behavioral anomalies that deviate from legitimate customer patterns. By learning to recognize these patterns, you can build review signals that put the most suspicious charges in front of a person first. Whether they reduce your fraud losses, and what they cost in declined legitimate customers, depends on how you calibrate them against your own confirmed outcomes.
Prerequisites and Data Requirements
Before diving into fraud pattern detection, ensure you have the following prerequisites in place:
Required Access and Tools
- Stripe Account Access: You need administrative or developer access to your Stripe account to view detailed transaction data and configure fraud prevention settings.
- API Keys: If you plan to programmatically analyze transactions, you'll need your Stripe API keys (use test mode keys for initial experimentation).
- Transaction History: The code below pulls the last 90 days. More history gives a steadier picture of what normal looks like for your business, and the right amount depends on your volume and seasonality.
- Data Analysis Tools: Python 3 with the
stripe,pandas,numpyandscipypackages. The steps below are written in Python; a spreadsheet can reproduce the counts but not the scripts.
Essential Data Fields
When exporting transaction data from Stripe, ensure you include these critical fields:
- Transaction ID
- Customer ID
- Card fingerprint (anonymized card identifier)
- Transaction amount and currency
- Transaction timestamp
- IP address (only if your checkout stores it in metadata; a Stripe Charge has no buyer IP field)
- Billing country and postal code
- Shipping country and postal code
- Risk score (Stripe Radar score if available)
- Transaction status (succeeded, failed, disputed)
- Failure code and message
- Customer email domain
- Card country
- CVC and ZIP check results
Understanding Your Baseline
Before identifying fraudulent patterns, you must understand what "normal" looks like for your business. Calculate these baseline metrics:
- Average transaction value and standard deviation
- Typical geographic distribution of customers
- Normal transaction frequency per customer
- Standard time-of-day and day-of-week patterns
- Typical ratio of new vs. returning customers
- Expected failure rate for legitimate transactions
These baselines will serve as your reference points when identifying anomalies. For advanced statistical approaches to establishing baselines and detecting significant deviations, you can compare each new day's counts with the spread of your own past daily counts, which is the idea behind the amount z-score in Step 5.
Step 1: Understand Fraud Pattern Detection Fundamentals
Fraud in payment systems follows predictable patterns. Understanding these patterns is the first step toward effective detection and prevention.
Common Fraud Patterns in Stripe Transactions
1. Card Testing (Carding)
Fraudsters use automated bots to test stolen card numbers by making small purchases. Key indicators include:
- Multiple low-value transactions (often $1-5) in rapid succession
- High failure rate followed by successful transactions
- Multiple different cards from the same IP address
- Sequential card numbers being tested
2. Velocity Abuse
Abnormally high transaction frequency indicating fraudulent activity:
- Several transactions from the same card within an hour, many more than your customers normally make
- Multiple transactions from the same IP but different cards
- Sudden spike in transaction volume from a previously inactive customer
3. Geographic Anomalies
Mismatches in geographic data suggesting fraudulent transactions:
- IP address location differs from billing country
- Card country doesn't match billing or shipping address
- Impossible travel patterns (transactions from different countries within minutes)
- Country combinations that have caused you disputes before (varies by business type)
4. Amount Pattern Irregularities
Unusual transaction amounts that deviate from normal patterns:
- Transaction amounts just below your review threshold
- Round numbers that don't match typical product pricing
- Amounts significantly higher than customer's historical average
Expected Outcome
After completing this step, you should be able to list 3-5 specific fraud patterns most relevant to your business model and understand the key indicators for each pattern.
Step 2: Access and Export Your Stripe Transaction Data
To analyze fraud patterns effectively, you need comprehensive transaction data. Stripe provides multiple methods for accessing this information.
Method 1: Using the Stripe Dashboard
- Log into your Stripe Dashboard at dashboard.stripe.com
- Navigate to Payments in the left sidebar
- Click the Export button in the top right
- Select your date range (recommend starting with the last 90 days)
- Choose Detailed export to include all available fields
- Select CSV format and click Export
Method 2: Using the Stripe API
For programmatic access, use the Stripe API. The script below needs a few things to be true. Stripe stores amounts in the currency's smallest unit, so the code converts them (whole units for zero-decimal currencies such as JPY, as described in Stripe's currencies documentation). It exports the billing and shipping countries that Step 4 needs. And the buyer's IP address and the country looked up from it are not fields on a Stripe charge: the script reads them from charge metadata, which exists only if your own checkout writes it there. Without those two columns, skip the IP-based checks.
import stripe
import pandas as pd
from datetime import datetime, timedelta
stripe.api_key = 'sk_test_your_key_here'
# Currencies whose Stripe amount is already in whole units (no cents).
# Check https://docs.stripe.com/currencies for the full zero-decimal list.
ZERO_DECIMAL = {'bif', 'clp', 'djf', 'gnf', 'jpy', 'kmf', 'krw', 'mga',
'pyg', 'rwf', 'vnd', 'vuv', 'xaf', 'xof', 'xpf'}
def to_major_units(amount, currency):
return amount if currency in ZERO_DECIMAL else amount / 100
def get(obj, *path):
"""Walk attributes, returning None if any link is missing."""
for p in path:
obj = getattr(obj, p, None) if obj is not None else None
return obj
# Retrieve charges from the last 90 days (auto_paging_iter handles pagination)
ninety_days_ago = int((datetime.now() - timedelta(days=90)).timestamp())
charges = stripe.Charge.list(limit=100, created={'gte': ninety_days_ago})
rows = []
for charge in charges.auto_paging_iter():
card = get(charge, 'payment_method_details', 'card')
meta = charge.metadata or {}
rows.append({
'id': charge.id,
'amount': to_major_units(charge.amount, charge.currency),
'currency': charge.currency,
'created': datetime.fromtimestamp(charge.created),
'customer': charge.customer,
'email': get(charge, 'billing_details', 'email'),
'status': charge.status,
'risk_score': get(charge, 'outcome', 'risk_score'), # Radar for Fraud Teams only
'card_country': get(card, 'country'),
'card_fingerprint': get(card, 'fingerprint'),
'cvc_check': get(card, 'checks', 'cvc_check'),
'billing_country': get(charge, 'billing_details', 'address', 'country'),
'shipping_country': get(charge, 'shipping', 'address', 'country'),
# Stripe does not put the buyer's IP on the Charge. These two columns exist only
# if your checkout writes them to metadata (IP and a country looked up from it).
'ip': meta.get('ip_address'),
'ip_country': meta.get('ip_country'),
})
df = pd.DataFrame(rows)
df.to_csv('stripe_transactions.csv', index=False)
print(f"Exported {len(df)} transactions")
print(f"File saved: stripe_transactions.csv")
print(f"Columns: {len(df.columns)}")
print(f"Currencies: {', '.join(sorted(df['currency'].unique()))}")
print(f"Date range: {df['created'].min():%Y-%m-%d} to {df['created'].max():%Y-%m-%d}")
Expected Output
You should now have a CSV file containing your transaction data with all relevant fields. On the synthetic data set a successful export prints the following; on your own account the counts and dates will be yours:
Exported 564 transactions
File saved: stripe_transactions.csv
Columns: 15
Currencies: jpy, usd
Date range: 2026-07-28 to 2026-10-23
Verification
Open your exported file and verify it contains:
- At least 100+ transactions (for meaningful pattern analysis)
- All critical fields populated (ID, amount, timestamp, customer info, billing and shipping country)
- Card details, and risk scores if you use Radar for Fraud Teams (otherwise that column is empty)
Step 3: Analyze Transaction Velocity Patterns
Velocity analysis identifies unusually rapid transaction sequences that often indicate fraud. Legitimate customers rarely make multiple purchases within minutes, while fraudsters testing cards or exploiting accounts do.
Calculate Velocity Metrics
Use this Python script to identify velocity-based patterns. It counts, for every charge, how many charges the same card (or IP, or email) made in the trailing window, separately for each currency so amounts are never added across currencies:
import pandas as pd
REQUIRED = ['id', 'amount', 'currency', 'created', 'card_fingerprint', 'ip', 'email']
df = pd.read_csv('stripe_transactions.csv')
missing = [c for c in REQUIRED if c not in df.columns]
assert not missing, f"Re-run the export: missing columns {missing}"
df['created'] = pd.to_datetime(df['created'])
df = df.sort_values('created').reset_index(drop=True)
def calculate_velocity(df, group_by, window_minutes):
"""For every transaction, count the transactions with the same group value
(and the same currency) in the trailing window, the transaction itself included."""
cols = ['group_type', 'group_value', 'transaction_id', 'timestamp',
'velocity', 'time_window', 'currency', 'total_amount']
parts = []
for (value, currency), g in df.dropna(subset=[group_by]).groupby([group_by, 'currency']):
r = g.set_index('created')['amount'].rolling(f'{window_minutes}min')
parts.append(pd.DataFrame({
'group_type': group_by, 'group_value': value,
'transaction_id': g['id'].values, 'timestamp': g['created'].values,
'velocity': r.count().astype(int).values,
'time_window': window_minutes, 'currency': currency,
'total_amount': r.sum().values}))
# An explicit column schema keeps the result usable when nothing qualifies.
return pd.concat(parts, ignore_index=True) if parts else pd.DataFrame(columns=cols)
velocity_by_card = calculate_velocity(df, 'card_fingerprint', 60)
velocity_by_ip = calculate_velocity(df, 'ip', 60)
velocity_by_email = calculate_velocity(df, 'email', 120)
# Illustrative review rules, not tested fraud cut-offs: calibrate them in Step 5
flag_card = velocity_by_card[velocity_by_card['velocity'] >= 3]
flag_ip = velocity_by_ip[velocity_by_ip['velocity'] >= 5]
print(f"Card fingerprints with 3+ charges in 60 min: {flag_card['group_value'].nunique()}")
print(f"IP addresses with 5+ charges in 60 min: {flag_ip['group_value'].nunique()}")
print()
print("Worst IP windows:")
print(flag_ip.sort_values('velocity', ascending=False).head(3)
[['group_value', 'velocity', 'currency', 'total_amount']].to_string(index=False))
# Attach each charge's card-velocity so later steps can use it, and save
df = df.merge(velocity_by_card[['transaction_id', 'velocity']]
.rename(columns={'transaction_id': 'id', 'velocity': 'velocity_60min'}),
on='id', how='left')
df['velocity_60min'] = df['velocity_60min'].fillna(1).astype(int)
df.to_csv('stripe_transactions_enriched.csv', index=False)
flag_card.to_csv('velocity_fraud_flags.csv', index=False)
Expected Output
Card fingerprints with 3+ charges in 60 min: 3
IP addresses with 5+ charges in 60 min: 3
Worst IP windows:
group_value velocity currency total_amount
203.0.113.10 7 usd 43.0
203.0.113.11 7 usd 43.0
203.0.113.12 7 usd 43.0
Interpreting Velocity Results
When reviewing velocity patterns, these starting thresholds are illustrative review rules, not tested fraud cut-offs. The right numbers depend on your business (a card used for several subscriptions looks very different from a one-off retail card). Calibrate them by comparing the flagged charges with your confirmed fraud and your legitimate repeat purchases:
- Lower concern: 1-2 transactions per card per hour (often a customer buying twice)
- Worth a look: 3-4 transactions per card per hour (an illustrative trigger for manual review)
- Review first: 5+ transactions per card per hour (consistent with card testing, but also with a legitimate bulk buyer; check the failures and the cards involved)
If you later have enough confirmed fraud labels, a supervised classifier such as AdaBoost can learn which of these signals matter for your business; see the AdaBoost guide. Until you have labels, treat the counts above as review signals.
Step 4: Identify Geographic Anomalies
Geographic mismatches are strong fraud indicators. Fraudsters often use stolen cards from one country while operating from another, creating detectable geographic inconsistencies.
Analyze Geographic Patterns
import pandas as pd
df = pd.read_csv('stripe_transactions_enriched.csv')
needed = ['card_country', 'ip_country', 'billing_country', 'shipping_country']
missing = [c for c in needed if c not in df.columns]
assert not missing, f"Re-run the Step 2 export: missing columns {missing}"
high_risk_combinations = [
{'card_country': 'US', 'ip_country': 'NG'}, # examples only: build yours from your own disputes
{'card_country': 'GB', 'ip_country': 'RU'},
{'card_country': 'CA', 'ip_country': 'RO'},
]
def check_geographic_risk(row):
risk_score = 0
risk_factors = []
if pd.notna(row['card_country']) and pd.notna(row['ip_country']):
if row['card_country'] != row['ip_country']:
risk_score += 2
risk_factors.append('card_ip_mismatch')
if pd.notna(row['billing_country']) and pd.notna(row['shipping_country']):
if row['billing_country'] != row['shipping_country']:
risk_score += 1
risk_factors.append('billing_shipping_mismatch')
for combo in high_risk_combinations:
if (row['card_country'] == combo['card_country'] and
row['ip_country'] == combo['ip_country']):
risk_score += 5
risk_factors.append('high_risk_country_combo')
countries = {row['card_country'], row['ip_country'], row['billing_country']}
if len([c for c in countries if pd.notna(c)]) >= 3:
risk_score += 3
risk_factors.append('multiple_country_mismatch')
return pd.Series({'geo_risk_score': risk_score,
'geo_risk_factors': ', '.join(risk_factors)})
df[['geo_risk_score', 'geo_risk_factors']] = df.apply(check_geographic_risk, axis=1)
high_geo_risk = df[df['geo_risk_score'] >= 3].sort_values(
['geo_risk_score', 'id'], ascending=[False, True])
print(f"Transactions with geographic risk factors: {len(high_geo_risk)}")
print()
print("Top geographic risk patterns:")
print(high_geo_risk[['id', 'amount', 'card_country', 'ip_country',
'geo_risk_score', 'geo_risk_factors']].head(5).to_string(index=False))
df.to_csv('stripe_transactions_enriched.csv', index=False)
high_geo_risk.to_csv('geographic_fraud_flags.csv', index=False)
Expected Output
Transactions with geographic risk factors: 40
Top geographic risk patterns:
id amount card_country ip_country geo_risk_score geo_risk_factors
ch_00314 96.22 US NG 8 card_ip_mismatch, billing_shipping_mismatch, high_risk_country_combo
ch_00014 58.51 US NG 7 card_ip_mismatch, high_risk_country_combo
ch_00026 79.31 US NG 7 card_ip_mismatch, high_risk_country_combo
ch_00034 148.90 US NG 7 card_ip_mismatch, high_risk_country_combo
ch_00044 40.45 US NG 7 card_ip_mismatch, high_risk_country_combo
Verification
Review the flagged transactions and verify:
- Geographic mismatches are correctly identified
- Risk scores align with your business's risk tolerance
- High-risk country combinations reflect actual fraud patterns you've experienced
- False positives (legitimate international customers) are minimized
Step 5: Review Amount and Behavior Patterns
Transaction amounts and purchasing behaviors often reveal fraud. Fraudsters typically exhibit different spending patterns than legitimate customers.
Detect Amount Anomalies
import pandas as pd
import numpy as np
df = pd.read_csv('stripe_transactions_enriched.csv')
df['created'] = pd.to_datetime(df['created'])
# Amounts in different currencies cannot be averaged together: use the main currency
main_currency = df['currency'].value_counts().index[0]
df = df[df['currency'] == main_currency].sort_values('created').copy()
print(f"Analysing {len(df)} charges in {main_currency.upper()}")
mean_amount = df['amount'].mean()
std_amount = df['amount'].std()
median_amount = df['amount'].median()
print(f"Mean: {mean_amount:.2f}")
print(f"Median: {median_amount:.2f}")
print(f"Std Dev: {std_amount:.2f}")
# Amount z-score (how many standard deviations from the mean)
df['amount_zscore'] = ((df['amount'] - mean_amount) / std_amount).abs()
amount_outliers = df[df['amount_zscore'] > 3]
print(f"\nAmount outliers (>3 std dev): {len(amount_outliers)}")
# Round amounts are only a weak hint: legitimate prices are often round too
df['is_round_amount'] = df['amount'].isin([10, 25, 50, 100, 200, 250, 500, 1000])
print(f"Round-number transactions: {int(df['is_round_amount'].sum())}")
df['customer_transaction_number'] = df.groupby('customer').cumcount() + 1
first_transaction_high_value = df[
(df['customer_transaction_number'] == 1) &
(df['amount'] > mean_amount + 2 * std_amount)
]
print(f"First-time transactions with unusually high amounts: {len(first_transaction_high_value)}")
behavior_flags = pd.concat([
amount_outliers[['id', 'customer', 'amount', 'amount_zscore']].assign(flag_type='amount_outlier'),
first_transaction_high_value[['id', 'customer', 'amount']].assign(flag_type='first_transaction_high_value')
])
behavior_flags.to_csv('behavior_fraud_flags.csv', index=False)
print(f"\nBehavioral flags saved: {len(behavior_flags)} rows")
df.to_csv('stripe_transactions_enriched.csv', index=False)
Expected Output
Analysing 562 charges in USD
Mean: 82.56
Median: 72.63
Std Dev: 48.40
Amount outliers (>3 std dev): 10
Round-number transactions: 3
First-time transactions with unusually high amounts: 14
Behavioral flags saved: 24 rows
Key Behavioral Indicators
- Amount Outliers: Transactions more than 3 standard deviations from the mean. The mean and standard deviation are pulled by the outliers themselves and by skewed order values, so the median-based variant is often steadier for real order data
- Round Numbers: Exact round amounts are a weak hint on their own, since many legitimate prices are round; look at them together with velocity
- High First Purchase: New customers making unusually large first purchases
- Variance Ratio: Customers with highly inconsistent purchase amounts
Step 6: Combine the Signals into a Review Score
Manual analysis is valuable for understanding patterns. A scoring script turns the signals from Steps 3 to 5 into one review order. Run it on a schedule you control if you want it repeated; nothing here watches your Stripe account on its own.
Create a Review Score
import pandas as pd
def calculate_fraud_score(transaction):
"""
Review-priority score combining the checks above (illustrative weights, not
fitted to fraud labels). Returns 0-100: higher = look at it sooner
"""
score = 0
risk_factors = []
# Velocity risk (0-25 points)
if transaction.get('velocity_60min', 0) >= 5:
score += 25
risk_factors.append('extreme_velocity')
elif transaction.get('velocity_60min', 0) >= 3:
score += 15
risk_factors.append('high_velocity')
# Geographic risk (0-30 points)
if transaction.get('geo_risk_score', 0) >= 5:
score += 30
risk_factors.append('high_geo_risk')
elif transaction.get('geo_risk_score', 0) >= 3:
score += 15
risk_factors.append('medium_geo_risk')
# Amount risk (0-20 points)
if transaction.get('amount_zscore', 0) > 3:
score += 20
risk_factors.append('amount_outlier')
elif transaction.get('amount_zscore', 0) > 2:
score += 10
risk_factors.append('high_amount')
# Behavioral risk (0-25 points)
if transaction.get('customer_transaction_number', 0) == 1:
if transaction.get('amount', 0) > 500:
score += 25
risk_factors.append('high_first_purchase')
elif transaction.get('amount', 0) > 200:
score += 15
risk_factors.append('elevated_first_purchase')
# Additional risk factors (bonus points)
if transaction.get('cvc_check') == 'fail':
score += 10
risk_factors.append('cvc_fail')
if transaction.get('is_round_amount'):
score += 5
risk_factors.append('round_amount')
# Cap at 100
score = min(score, 100)
return {
'fraud_score': score,
'risk_factors': ', '.join(risk_factors),
'risk_level': 'HIGH' if score >= 50 else 'MEDIUM' if score >= 30 else 'LOW'
}
# Apply to the charges enriched in Steps 3 to 5 (the main currency)
df = pd.read_csv('stripe_transactions_enriched.csv')
fraud_scores = df.apply(calculate_fraud_score, axis=1, result_type='expand')
df = pd.concat([df, fraud_scores], axis=1)
# Summary by risk level
print("Fraud Risk Summary:")
print(df['risk_level'].value_counts())
print(f"\nHigh-risk transactions requiring review: {len(df[df['risk_level'] == 'HIGH'])}")
# Save prioritized review list
high_risk = df[df['risk_level'] == 'HIGH'].sort_values('fraud_score', ascending=False)
high_risk[['id', 'customer', 'amount', 'fraud_score', 'risk_factors']].to_csv(
'fraud_review_queue.csv',
index=False
)
print("\nTop 5 highest-risk transactions:")
print(high_risk[['id', 'amount', 'fraud_score', 'risk_factors']].head())
Expected Output
Fraud Risk Summary:
risk_level
LOW 518
MEDIUM 40
HIGH 4
Name: count, dtype: int64
High-risk transactions requiring review: 4
Top 5 highest-risk transactions:
id amount fraud_score risk_factors
236 ch_00562 5.00 55 high_velocity, high_geo_risk, cvc_fail
373 ch_00555 5.00 55 high_velocity, high_geo_risk, cvc_fail
518 ch_00548 5.00 55 high_velocity, high_geo_risk, cvc_fail
155 ch_00200 239.61 50 high_geo_risk, amount_outlier
The weights and cut-offs (50 and 30) are illustrative and were chosen only so this synthetic data set produces a short review list. Every flagged charge here is either a planted pattern or a chance outlier or mismatch; on real data you must check the flags against confirmed outcomes before trusting them. The script scores the main currency only, because the amount checks in Step 5 cannot mix currencies.
Integrate with Stripe Radar
Custom Radar rules and manual review need a Radar plan that includes them (Radar for Fraud Teams, now Radar Plus or Pro). If yours does, configure rules based on your findings:
- Navigate to Radar → Rules in your Stripe Dashboard
- Create custom rules based on your risk thresholds:
- Block if velocity exceeds 5 transactions per hour from same card
- Review if card country differs from IP country
- Block specific high-risk country combinations
- Review first-time transactions over $500
For a broader view of how analysis pipelines can be organised, see AI-first data analysis pipelines.
Interpreting Your Results
Understanding Fraud Scores
Your fraud detection analysis will produce scores and flags across multiple dimensions. Here's how to interpret and act on these results:
Score Ranges and Actions
These ranges match the cut-offs in the script above and are illustrative. The share of each band that is truly fraudulent is something only your confirmed outcomes can tell you.
| Score Range | Risk Level | Recommended Action |
|---|---|---|
| 0-29 | Low | Approve; look at the trend of flags over time |
| 30-49 | Medium | Manual review, request additional verification |
| 50-100 | High | Intensive manual review; block only once you have checked the rule against your own history |
Common False Positive Scenarios
Not all flagged transactions are fraudulent. Be aware of these legitimate scenarios that may trigger fraud alerts:
- International Business Travelers: Customers traveling internationally will show geographic mismatches between card country and IP location
- VPN Users: Privacy-conscious customers using VPNs will show IP locations that don't match their actual location
- Gift Purchases: Legitimate customers buying gifts will have billing and shipping address mismatches
- Bulk Purchasers: Corporate buyers or resellers may make multiple rapid purchases legitimately
- Sales Events: During promotions, legitimate customers may make multiple purchases to get best deals
Reducing False Positives
To minimize rejecting legitimate transactions:
- Whitelist Known Customers: Reduce scoring for customers with positive payment history
- Contextual Rules: Adjust thresholds during known sales events or peak seasons
- Customer Communication: For medium-risk transactions, reach out to verify before blocking
- Gradual Implementation: Start with monitoring mode before automatically blocking
- Continuous Refinement: Review blocked transactions monthly and adjust rules based on outcomes
Key Performance Indicators (KPIs)
Track these metrics to measure fraud detection effectiveness, and set your own targets from your history and your card networks' dispute programs:
- Detection Rate: Percentage of confirmed fraud that your rules flagged
- False Positive Rate: Percentage of legitimate transactions incorrectly flagged
- Chargeback Rate: Disputes as percentage of total transactions
- Average Review Time: Time to manually review flagged transactions
- Fraud Loss Ratio: Fraud losses as percentage of revenue
Run It on Your Own Stripe Export
Export your payments from Stripe as a CSV and upload it at MCP Analytics. The analysis runs a fraud pattern screen (velocity, geography and amount outliers) on your file and shows the result with the method used. You choose the columns; nothing connects to your Stripe account.
Common Issues and Solutions
Issue 1: High False Positive Rate
Symptom: Your fraud detection is flagging too many legitimate transactions, causing customer friction and lost sales.
Solutions:
- Adjust Score Thresholds: Raise the score required to trigger blocking, and review flagged charges by hand before you block any
- Weight Factors Differently: Reduce weight for factors that frequently flag legitimate customers (e.g., geographic mismatches for international businesses)
- Implement Customer Whitelisting: Automatically approve repeat customers with clean payment history
- Add Context Rules: Reduce scoring during sales events when legitimate velocity increases
- Review Manually Before Blocking: Use a two-tier system: auto-flag for review, manual decision to block
Issue 2: Missing Transaction Data
Symptom: Key fields like IP address, card country, or risk scores are missing from your exports.
Solutions:
- Enable Stripe Radar: The numeric
outcome.risk_scoreis only available with Radar for Fraud Teams (Stripe's current plan names are Radar Plus and Pro);risk_levelis available with Radar - Capture IP at Checkout: Ensure your integration passes customer IP to Stripe using
payment_intent.metadata.ip_address - Use Detailed Exports: Select "Detailed export" option when exporting from Dashboard
- Check API Version: Older API versions may not include all fields; update to latest version
- Store Additional Metadata: Pass custom metadata fields during payment creation for business-specific tracking
Issue 3: API Rate Limiting
Symptom: Scripts fail or slow down when retrieving large transaction datasets via API.
Solutions:
import stripe
import time
stripe.api_key = 'sk_test_your_key_here'
stripe.max_network_retries = 2 # the SDK retries connection errors and 429s itself
def get_all_charges_with_retry(created_after, max_retries=3):
for attempt in range(max_retries):
try:
return list(stripe.Charge.list(
limit=100, created={'gte': created_after}).auto_paging_iter())
except stripe.RateLimitError:
if attempt == max_retries - 1:
raise
wait_time = 2 ** attempt # exponential backoff
print(f"Rate limit hit. Waiting {wait_time}s...")
time.sleep(wait_time)
Issue 4: Unable to Identify Card Testing
Symptom: Multiple small failed transactions aren't being flagged effectively.
Solutions:
- Include Failed Transactions: Analyze both successful and failed charges, as card testing shows high failure rates
- Pattern Sequential Cards: Look for sequential card numbers being tested (analyze last 4 digits patterns)
- Track Failure Codes: Multiple "card_declined" failures from same IP is strong indicator
- Watch Micro-Transactions: Flag when multiple small transactions (for example under $5) occur from the same source
- Implement CAPTCHA: Add CAPTCHA to checkout for transactions triggering velocity rules
Issue 5: Legitimate Business Patterns Flagged
Symptom: Your business model naturally creates patterns that look like fraud (e.g., subscription service, B2B bulk orders).
Solutions:
- Create Business-Specific Rules: Exclude subscription renewal patterns from velocity checks
- Identify Corporate Customers: Tag B2B customers and apply different thresholds
- Time-Based Rules: Adjust rules based on time of day (B2B purchases typically during business hours)
- Amount-Based Exceptions: If your products have specific price points, whitelist those exact amounts
- Customer Journey Analysis: Track normal customer progression (trial → paid, small → large orders) and exclude from flags
Issue 6: Slow Analysis Performance
Symptom: Scripts take too long to process large transaction datasets.
Solutions:
- Use Pandas Efficiently: Leverage vectorized operations instead of iterative apply() functions
- Process in Batches: Analyze transactions in monthly batches rather than all at once
- Filter Early: Remove irrelevant transactions before complex calculations
- Use Database Storage: Store transaction data in PostgreSQL or SQLite for faster querying
- Implement Caching: Cache calculated metrics (baselines, customer profiles) to avoid recalculation
Your turn
Bring your own data and the question you actually need answered.
MCP AnalyticsData science by email Send us your data and your question, and we will reply. ds@mcpanalytics.ai
Explore more: Stripe Analytics: all tools, tutorials, and guides →
Not sure which plan? Compare plans →