Spam Score Interpretation

Phone spam scores quantify the likelihood that a phone number is associated with spam, scams, or fraudulent activity. Understanding how these scores are calculated, what thresholds to use, and how to combine them with other signals is essential for effective fraud prevention. This guide provides a deep dive into spam score interpretation and practical application.

Key Takeaways

  • Spam scores aggregate consumer reports, call patterns, and carrier intelligence
  • Threshold selection depends on your use case and tolerance for false positives
  • Combine spam scores with line type, carrier, and behavioral signals
  • Scores change over time - implement appropriate caching strategies

What is a Phone Spam Score?

A phone spam score is a numerical value, typically 0-100, indicating the likelihood that a phone number is associated with spam, scam, or fraudulent activity. Higher scores indicate higher risk.

Score Sources

  • Consumer complaints - Reports submitted to FTC, carriers, and spam-blocking apps
  • Call pattern analysis - High volume, short duration, geographic spread
  • Honeypot data - Numbers calling phone numbers that shouldn't receive calls
  • Carrier intelligence - Internal carrier spam detection systems
  • Industry databases - Shared blocklists and fraud consortiums
  • STIR/SHAKEN data - Attestation failures and patterns

Score Components

While exact algorithms are proprietary, spam scores typically weight these factors:

Component Description Typical Weight
Report Volume Number of consumer complaints High
Report Recency How recently complaints were filed High
Report Diversity Complaints from multiple sources Medium-High
Call Patterns Volume, timing, duration patterns Medium
Robocaller Flags Known automated calling systems High
Carrier Risk Historical spam rates from carrier Low-Medium

Understanding Score Ranges

Spam scores are typically presented on a 0-100 scale. Here's how to interpret different ranges:

Score Interpretation Guide

Score Range Risk Level Typical Characteristics
0-10 Minimal Clean number, no complaints, established history
11-25 Low Minor/old complaints, or similar to spam numbers
26-40 Moderate Some recent complaints, patterns warrant monitoring
41-60 Elevated Multiple complaints, risky call patterns
61-80 High Many complaints, known spam associations
81-100 Critical Confirmed spam/scam, robocaller databases

What Each Range Means in Practice

0-10: Minimal Risk

Numbers in this range are likely legitimate with clean histories. However, don't assume zero risk - new fraud numbers haven't accumulated complaints yet. Use additional signals like line type and velocity for new numbers.

11-25: Low Risk

These numbers may have minor historical complaints or characteristics similar to spam numbers (e.g., VoIP from a carrier with spam history). Generally safe to proceed with normal verification.

26-40: Moderate Risk

Warrants attention. Recent complaints may be accumulating, or patterns are emerging. Consider requiring additional verification for sensitive actions.

41-60: Elevated Risk

Significant spam signals present. Challenge with additional verification (SMS OTP, voice call) before allowing high-value actions. Monitor closely after allowing.

61-80: High Risk

Strong spam association. Block or require substantial verification (document upload, video verification). Only proceed if verification passes and user demonstrates legitimate intent.

81-100: Critical Risk

Confirmed spam or scam association. Block automatically for most use cases. Only allow with exceptional verification and manual review for legitimate appeals.

Get real-time phone spam scores. VeriRoute Intel provides 0-100 spam scores plus spam type classification.

Get Free API Key

Selecting Appropriate Thresholds

Choosing the right threshold depends on your risk tolerance, user base, and the action being protected.

Threshold Considerations

  • False positive cost - What's the impact of blocking a legitimate user?
  • False negative cost - What's the impact of letting fraud through?
  • User base characteristics - Are your users likely to have VoIP, prepaid, etc.?
  • Action sensitivity - Account creation vs. $10,000 transaction
  • Verification options - Can you challenge instead of block?

Threshold Recommendations by Use Case

Use Case Block Threshold Challenge Threshold Rationale
Account Registration 80+ 50+ Allow most signups, challenge suspicious
SMS 2FA Setup 70+ 40+ Don't send OTPs to likely spam numbers
Financial Transaction 60+ 35+ Higher fraud cost justifies more friction
High-Value Transaction 50+ 25+ Maximum protection for big transactions
Inbound Call Screening 85+ 60+ Don't want to block legitimate callers
Lead Verification 70+ 45+ Don't waste sales resources on bad leads

Threshold Tuning Process

def tune_thresholds(historical_data, target_false_positive_rate=0.02):
    """
    Tune spam score thresholds based on historical data.

    Args:
        historical_data: List of {spam_score, was_fraud} records
        target_false_positive_rate: Maximum acceptable FP rate
    """

    # Sort by spam score
    sorted_data = sorted(historical_data, key=lambda x: x['spam_score'])

    results = []

    for threshold in range(0, 101, 5):
        # Calculate metrics at this threshold
        blocked = [d for d in sorted_data if d['spam_score'] >= threshold]
        allowed = [d for d in sorted_data if d['spam_score'] < threshold]

        true_positives = sum(1 for d in blocked if d['was_fraud'])
        false_positives = sum(1 for d in blocked if not d['was_fraud'])
        true_negatives = sum(1 for d in allowed if not d['was_fraud'])
        false_negatives = sum(1 for d in allowed if d['was_fraud'])

        total_fraud = true_positives + false_negatives
        total_legit = true_negatives + false_positives

        precision = true_positives / (true_positives + false_positives) if blocked else 0
        recall = true_positives / total_fraud if total_fraud else 0
        fpr = false_positives / total_legit if total_legit else 0

        results.append({
            'threshold': threshold,
            'precision': precision,
            'recall': recall,
            'false_positive_rate': fpr,
            'blocked_count': len(blocked)
        })

    # Find optimal threshold meeting FPR constraint
    valid_thresholds = [r for r in results if r['false_positive_rate'] <= target_false_positive_rate]

    if valid_thresholds:
        # Among valid thresholds, pick one with best recall
        optimal = max(valid_thresholds, key=lambda x: x['recall'])
        return optimal
    else:
        # Can't meet constraint - return threshold with lowest FPR
        return min(results, key=lambda x: x['false_positive_rate'])

Combining Spam Score with Other Signals

Spam scores are most effective when combined with other phone intelligence and behavioral signals.

Signal Combination Strategy

def calculate_combined_phone_risk(phone_data, context):
    """
    Combine spam score with other signals for comprehensive risk assessment.

    Args:
        phone_data: API response with spam, lrn, cnam data
        context: Additional context (transaction amount, user history, etc.)

    Returns:
        Combined risk score and recommendation
    """

    # Base spam score (0-100)
    spam_score = phone_data.get('spam', {}).get('score', 0)

    # Apply modifiers based on other signals
    modifiers = []

    # Line type modifier
    line_type = phone_data.get('lrn', {}).get('line_type')
    if line_type == 'voip':
        modifiers.append({'factor': 'voip', 'adjustment': 15})
    elif line_type == 'landline':
        modifiers.append({'factor': 'landline', 'adjustment': -5})

    # Number age modifier
    activation_date = phone_data.get('lrn', {}).get('activation_date')
    if activation_date:
        days_old = (datetime.now() - parse_date(activation_date)).days
        if days_old < 7:
            modifiers.append({'factor': 'very_new', 'adjustment': 20})
        elif days_old < 30:
            modifiers.append({'factor': 'new', 'adjustment': 10})
        elif days_old > 365:
            modifiers.append({'factor': 'established', 'adjustment': -5})

    # Robocaller flag (binary, high impact)
    if phone_data.get('spam', {}).get('is_robocaller'):
        modifiers.append({'factor': 'robocaller', 'adjustment': 30})

    # CNAM presence
    if not phone_data.get('cnam', {}).get('name'):
        modifiers.append({'factor': 'no_cnam', 'adjustment': 5})

    # Recent porting (possible SIM swap)
    if phone_data.get('lrn', {}).get('ported'):
        port_date = phone_data.get('lrn', {}).get('port_date')
        if port_date:
            days_since_port = (datetime.now() - parse_date(port_date)).days
            if days_since_port < 7:
                modifiers.append({'factor': 'recent_port', 'adjustment': 15})

    # Context-based modifiers
    if context.get('transaction_amount', 0) > 1000:
        modifiers.append({'factor': 'high_value', 'adjustment': 10})

    # Calculate final score
    total_adjustment = sum(m['adjustment'] for m in modifiers)
    combined_score = min(100, max(0, spam_score + total_adjustment))

    # Determine action
    if combined_score >= 70:
        action = 'block'
    elif combined_score >= 45:
        action = 'challenge'
    elif combined_score >= 25:
        action = 'monitor'
    else:
        action = 'allow'

    return {
        'base_spam_score': spam_score,
        'combined_score': combined_score,
        'modifiers': modifiers,
        'action': action
    }

Signal Weight Matrix

Signal Low Risk Medium Risk High Risk
Spam Score < 25 25-60 > 60
Line Type Landline Mobile VoIP
Number Age > 1 year 1-12 months < 1 month
CNAM Present, matches Present, generic Missing
Porting Not ported Ported > 30 days Ported < 30 days

Understanding Spam Types

Beyond numeric scores, spam type classification provides actionable context:

Common Spam Type Classifications

Spam Type Description Typical Risk
robocaller Automated calling systems High
telemarketer Sales calls (may be legal) Medium
scam_likely Potential scam operation Very High
debt_collector Collection agencies Medium
political Campaign/political calls Low-Medium
survey Research/survey calls Low
nuisance Unwanted but not fraudulent Low
def handle_spam_type(spam_data, context):
    """Apply spam type-specific handling."""

    spam_type = spam_data.get('spam_type')
    spam_score = spam_data.get('score', 0)

    # Type-specific overrides
    if spam_type == 'scam_likely':
        # Always block confirmed scams regardless of score
        return {'action': 'block', 'reason': 'scam_classification'}

    if spam_type == 'robocaller':
        # Block robocallers for phone verification
        if context.get('action') == 'send_otp':
            return {'action': 'block', 'reason': 'robocaller_otp_abuse'}

    if spam_type == 'telemarketer':
        # Telemarketers might be legitimate businesses
        # Only block if score is also high
        if spam_score >= 50:
            return {'action': 'challenge', 'reason': 'telemarketer_high_score'}

    if spam_type == 'debt_collector':
        # Could be legitimate - proceed with caution
        return {'action': 'allow', 'flag': 'debt_collector'}

    # Default: use score-based logic
    return None

Handling Score Changes Over Time

Spam scores are dynamic - they change as new complaints are filed and old ones age out.

Score Volatility Patterns

  • Sudden increases - New complaint wave, often indicates active spam campaign
  • Gradual increases - Accumulating complaints over time
  • Score decreases - Old complaints aging out, number may have changed hands
  • Score stability - Consistent behavior (good or bad)

Caching Strategy

class SpamScoreCache:
    """Intelligent spam score caching."""

    def __init__(self, redis, phone_api):
        self.redis = redis
        self.api = phone_api

    async def get_spam_score(self, phone, force_refresh=False):
        """Get spam score with intelligent caching."""

        cache_key = f"spam:{phone}"

        if not force_refresh:
            cached = await self.redis.get(cache_key)
            if cached:
                data = json.loads(cached)
                age_minutes = (time.time() - data['cached_at']) / 60

                # Score-based TTL: Higher scores refresh more frequently
                max_age = self._get_ttl_minutes(data['score'])

                if age_minutes < max_age:
                    return data

        # Fetch fresh data
        result = await self.api.lookup(phone, {'spam': True})
        spam_data = result.get('spam', {})

        cache_data = {
            'score': spam_data.get('score', 0),
            'is_robocaller': spam_data.get('is_robocaller', False),
            'spam_type': spam_data.get('spam_type'),
            'cached_at': time.time()
        }

        # Cache with score-appropriate TTL
        ttl_seconds = self._get_ttl_minutes(cache_data['score']) * 60
        await self.redis.set(cache_key, json.dumps(cache_data), ex=ttl_seconds)

        return cache_data

    def _get_ttl_minutes(self, score):
        """Higher risk numbers get shorter cache TTL."""
        if score >= 70:
            return 30   # 30 minutes for high risk
        elif score >= 40:
            return 120  # 2 hours for medium risk
        else:
            return 360  # 6 hours for low risk

Handling False Positives

No spam detection system is perfect. Handle false positives gracefully:

False Positive Mitigation

  1. Don't block silently - Tell users why they're challenged/blocked
  2. Offer alternatives - Different phone, email verification, document upload
  3. Implement appeals - Let users request review
  4. Track and learn - Log false positives to improve thresholds
class FalsePositiveHandler:
    """Handle potential false positives gracefully."""

    def block_with_appeal(self, phone, spam_score, action):
        """Block action but offer appeal path."""

        return {
            'blocked': True,
            'reason': 'phone_risk_score',
            'appeal_available': True,
            'appeal_options': [
                {
                    'type': 'alternate_phone',
                    'description': 'Use a different phone number'
                },
                {
                    'type': 'email_verification',
                    'description': 'Verify via email instead'
                },
                {
                    'type': 'manual_review',
                    'description': 'Request manual review (24-48 hours)'
                }
            ],
            'message': (
                f"We couldn't verify your phone number. "
                f"Please try an alternative verification method."
            )
        }

    async def process_appeal(self, user_id, phone, appeal_type, evidence):
        """Process a false positive appeal."""

        appeal = {
            'user_id': user_id,
            'phone': phone,
            'appeal_type': appeal_type,
            'evidence': evidence,
            'status': 'pending',
            'created_at': datetime.now()
        }

        # Auto-approve certain cases
        if appeal_type == 'alternate_phone':
            new_phone = evidence.get('new_phone')
            new_score = await self.get_spam_score(new_phone)

            if new_score < 30:
                appeal['status'] = 'auto_approved'
                return {'approved': True, 'new_phone': new_phone}

        # Queue for manual review
        await self.queue_for_review(appeal)
        return {'approved': False, 'status': 'pending_review'}

Best Practices

  1. Don't use spam score alone - Combine with line type, age, and context
  2. Set thresholds per use case - Registration vs. transaction vs. call screening
  3. Challenge before blocking - Give users a chance to verify
  4. Cache intelligently - Shorter TTL for high-risk scores
  5. Track outcomes - Measure false positive/negative rates
  6. Provide appeals - Gracefully handle legitimate users with bad numbers
  7. Monitor score distributions - Watch for anomalies in your traffic

Frequently Asked Questions

Why does a new phone number have a non-zero spam score?

New numbers may inherit some risk from carrier-level factors (carriers with historically high spam), line type (VoIP numbers have baseline risk), or number range characteristics. Additionally, "new" to the current user doesn't mean new to the system - the number may have been previously assigned to someone who received complaints. Scores reflect all available intelligence, not just current-owner activity.

How quickly do spam scores update after complaints?

Score update speed varies by data source. Real-time carrier feeds update within minutes. Consumer complaint aggregators may take hours to days. Industry databases update daily to weekly. For most spam score providers, significant complaint volumes are reflected within hours. VeriRoute Intel processes new intelligence continuously, with typical score updates occurring within 1-4 hours of new report submissions.

Should I block or challenge numbers with medium spam scores?

For medium scores (typically 40-60), challenge rather than block. This balances fraud prevention with user experience. Send an SMS OTP, request voice verification, or ask for an alternate phone. Only escalate to blocking if the challenge fails or if you combine the medium spam score with other high-risk signals (VoIP + new number + high velocity, for example). The goal is graduated friction, not binary decisions.

Do spam scores differ for mobile vs. VoIP numbers?

Spam scores measure complaint/behavior history regardless of line type, so a clean VoIP can score lower than a mobile with spam history. However, VoIP numbers statistically have higher spam rates because they're easier to obtain and dispose of. Best practice is to use spam score AND line type together - a VoIP with score 30 is riskier than a mobile with score 30, even though the scores are identical.

Related Articles

← Back to Phone Fraud Detection

Get Accurate Phone Spam Scores

Real-time spam scores, spam type classification, and phone intelligence. 10 free lookups monthly.