# How to check fraud risk in Stripe payments

- Tool: Stripe
- Difficulty: Beginner
- Time required: 10 minutes
- Compatibility: Stripe API v2024-12+, Node.js 18+
- Last updated: March 2026

## TL;DR

Every Stripe payment receives a Radar risk score from 0-100 and a risk level (normal, elevated, highest). You can access this data on every charge object to build fraud monitoring, flag high-risk transactions for review, and track fraud patterns. This guide covers reading risk scores, setting up review queues, and building a fraud monitoring endpoint.

## Understanding and Monitoring Fraud Risk in Stripe Payments

Stripe Radar assigns a risk score (0 to 100) and a risk level (normal, elevated, highest) to every payment. These are available on the charge object's outcome field. Higher scores indicate greater fraud risk. You can use these scores to flag transactions in your internal systems, create custom alerts, and feed data into your own fraud review processes.

## Before you start

- A Stripe account with completed payments (test or live)
- Node.js 18 or later installed
- Stripe Node.js SDK: npm install stripe

## Step-by-step guide

### 1. Read the risk score on a charge

Every successful or failed charge includes an outcome object with the Radar risk assessment. Access it via the API.

```
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

async function getRiskAssessment(chargeId) {
  const charge = await stripe.charges.retrieve(chargeId);
  const outcome = charge.outcome;

  console.log('Risk score:', outcome.risk_score); // 0-100
  console.log('Risk level:', outcome.risk_level); // normal, elevated, highest
  console.log('Type:', outcome.type); // authorized, blocked, manual_review
  console.log('Reason:', outcome.reason); // rule match or null
  console.log('Seller message:', outcome.seller_message);

  return outcome;
}

getRiskAssessment('ch_chargeId');
```

**Expected result:** Console displays the risk score (0-100), risk level, and outcome type for the charge.

### 2. Check verification signals

Beyond the overall risk score, Stripe provides individual verification checks for CVC, address, and ZIP code. These signals help identify suspicious transactions.

```
async function getVerificationDetails(chargeId) {
  const charge = await stripe.charges.retrieve(chargeId);
  const checks = charge.payment_method_details?.card?.checks;

  if (checks) {
    console.log('CVC check:', checks.cvc_check); // pass, fail, unavailable, unchecked
    console.log('Address line 1:', checks.address_line1_check);
    console.log('ZIP check:', checks.address_postal_code_check);
  }

  // Flag suspicious combinations
  const suspicious =
    checks?.cvc_check === 'fail' ||
    (checks?.address_postal_code_check === 'fail' && charge.amount > 10000);

  if (suspicious) {
    console.log('WARNING: Suspicious verification results');
  }

  return checks;
}

getVerificationDetails('ch_chargeId');
```

**Expected result:** Console shows individual verification check results and flags suspicious combinations.

### 3. Build a fraud monitoring endpoint

Create an API endpoint that returns risk information for recent payments, useful for building internal fraud dashboards.

```
const express = require('express');
const app = express();
app.use(express.json());

app.get('/api/fraud/recent', async (req, res) => {
  try {
    const charges = await stripe.charges.list({
      limit: 50,
      created: {
        gte: Math.floor(Date.now() / 1000) - 86400, // Last 24 hours
      },
    });

    const riskReport = charges.data.map(charge => ({
      id: charge.id,
      amount: charge.amount,
      currency: charge.currency,
      status: charge.status,
      riskScore: charge.outcome?.risk_score,
      riskLevel: charge.outcome?.risk_level,
      outcomeType: charge.outcome?.type,
      cvcCheck: charge.payment_method_details?.card?.checks?.cvc_check,
      zipCheck: charge.payment_method_details?.card?.checks?.address_postal_code_check,
      email: charge.billing_details?.email,
      country: charge.payment_method_details?.card?.country,
      created: new Date(charge.created * 1000).toISOString(),
    }));

    // Sort by risk score descending
    riskReport.sort((a, b) => (b.riskScore || 0) - (a.riskScore || 0));

    res.json({
      total: riskReport.length,
      highRisk: riskReport.filter(r => r.riskLevel === 'highest').length,
      elevated: riskReport.filter(r => r.riskLevel === 'elevated').length,
      charges: riskReport,
    });
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

app.listen(3000);
```

**Expected result:** API returns a sorted list of recent charges with risk scores, helping you identify high-risk transactions quickly.

### 4. Set up webhook alerts for high-risk payments

Use webhooks to get real-time alerts when Radar flags a payment as high-risk or sends it to manual review.

```
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['stripe-signature'];
  let event;
  try {
    event = stripe.webhooks.constructEvent(
      req.body,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET
    );
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  switch (event.type) {
    case 'charge.succeeded': {
      const charge = event.data.object;
      if (charge.outcome?.risk_level === 'elevated' ||
          charge.outcome?.risk_level === 'highest') {
        console.log(`HIGH RISK PAYMENT: ${charge.id}`);
        console.log(`Score: ${charge.outcome.risk_score}`);
        console.log(`Amount: ${charge.amount} ${charge.currency}`);
        // Send Slack/email alert to your fraud team
      }
      break;
    }
    case 'radar.early_fraud_warning.created': {
      const warning = event.data.object;
      console.log(`EARLY FRAUD WARNING: charge ${warning.charge}`);
      console.log(`Fraud type: ${warning.fraud_type}`);
      // Immediately review or refund the charge
      break;
    }
    case 'review.opened': {
      console.log('Manual review opened:', event.data.object.charge);
      break;
    }
  }

  res.json({ received: true });
});
```

**Expected result:** Webhook handler sends alerts for high-risk payments and early fraud warnings in real time.

## Complete code example

File: `fraud-monitoring.js`

```javascript
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const express = require('express');
const app = express();

app.use(express.json());

// Get risk details for a specific charge
app.get('/api/fraud/charge/:id', async (req, res) => {
  try {
    const charge = await stripe.charges.retrieve(req.params.id);
    res.json({
      id: charge.id,
      amount: charge.amount,
      riskScore: charge.outcome?.risk_score,
      riskLevel: charge.outcome?.risk_level,
      outcomeType: charge.outcome?.type,
      sellerMessage: charge.outcome?.seller_message,
      checks: charge.payment_method_details?.card?.checks,
      cardCountry: charge.payment_method_details?.card?.country,
      email: charge.billing_details?.email,
    });
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

// Get summary of recent high-risk charges
app.get('/api/fraud/summary', async (req, res) => {
  try {
    const hours = parseInt(req.query.hours) || 24;
    const charges = await stripe.charges.list({
      limit: 100,
      created: { gte: Math.floor(Date.now() / 1000) - (hours * 3600) },
    });
    const stats = { total: 0, normal: 0, elevated: 0, highest: 0, blocked: 0 };
    charges.data.forEach(c => {
      stats.total++;
      const level = c.outcome?.risk_level || 'normal';
      stats[level] = (stats[level] || 0) + 1;
      if (c.outcome?.type === 'blocked') stats.blocked++;
    });
    res.json(stats);
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

// Webhook for fraud alerts
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['stripe-signature'];
  let event;
  try {
    event = stripe.webhooks.constructEvent(
      req.body, sig, process.env.STRIPE_WEBHOOK_SECRET
    );
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }
  if (event.type === 'radar.early_fraud_warning.created') {
    console.log('FRAUD WARNING:', event.data.object.charge);
  }
  if (event.type === 'charge.succeeded') {
    const c = event.data.object;
    if (c.outcome?.risk_score >= 65) {
      console.log(`High risk: ${c.id} score=${c.outcome.risk_score}`);
    }
  }
  res.json({ received: true });
});

app.listen(3000, () => console.log('Fraud monitoring on port 3000'));
```

## Common mistakes

- **Not checking risk scores on authorized payments** — undefined Fix: A payment can be authorized but still have a high risk score. Monitor elevated-risk payments even when they succeed.
- **Ignoring early fraud warnings** — undefined Fix: Early fraud warnings from card networks strongly indicate fraud. Proactively refund these charges to avoid chargebacks.
- **Only monitoring blocked charges and missing elevated-risk ones** — undefined Fix: Payments with risk_level 'elevated' pass through but may still be fraudulent. Build alerts for scores above a threshold (e.g., 65+).
- **Not passing billing details which reduces Radar accuracy** — undefined Fix: Include email, name, and address on PaymentIntents so Radar has more signals for risk assessment.

## Best practices

- Monitor risk scores on all payments, not just blocked ones
- Set up webhook alerts for charges with risk_score above 65
- Always act on radar.early_fraud_warning.created events — refund proactively
- Build a fraud dashboard that shows risk distribution over time
- Pass complete billing_details on every payment for better Radar accuracy
- Review elevated-risk payments weekly to identify patterns and create block rules
- Track your dispute rate and aim to keep it below 0.75%

## Frequently asked questions

### What does a Radar risk score of 65 mean?

Scores range from 0 (lowest risk) to 100 (highest risk). A score of 65 is considered elevated risk. Stripe classifies payments as normal (0-20), elevated (20-75), or highest (75-100), though exact thresholds vary.

### Can I access risk scores in test mode?

Yes. Test charges include simulated risk scores. Use Stripe's test cards to get different risk levels, though the scores in test mode are not based on real fraud signals.

### What is an early fraud warning?

Card networks (Visa, Mastercard) send early fraud warnings when a cardholder reports unauthorized activity. These arrive within 24-48 hours and precede formal chargebacks. Refunding proactively avoids the chargeback fee.

### Does Radar cost extra?

Basic Radar is free with every Stripe account. Radar for Fraud Teams costs $0.07 per screened transaction and adds manual review queues, custom rules with more attributes, and block lists.

### How do I reduce false positives in fraud blocking?

Start with review rules (manual_review) instead of block rules. Monitor results for a few weeks before promoting to automatic blocks. Use multiple signals (email + card + amount) instead of single attributes.

---

Source: https://www.rapidevelopers.com/stripe-guide/how-to-check-fraud-risk-in-stripe-payments
© RapidDev — https://www.rapidevelopers.com/stripe-guide/how-to-check-fraud-risk-in-stripe-payments
