ScamVerify™
Tutorials

Monitor Your Brand for Phone Number Spoofing

Detect when your company phone numbers appear in scam complaint databases using the ScamVerify™ API.

This tutorial walks you through building a brand monitoring service that checks your company's phone numbers against the ScamVerify™ threat intelligence database every day. When a number shows new complaints or a risk score increase, the service sends an alert via email or Slack so your team can respond quickly. Phone number spoofing costs businesses millions in lost trust. This tool helps you catch it early.

Prerequisites

  • A ScamVerify™ API key (get one at scamverify.ai/settings/api)
  • Node.js 18 or later (Node.js example) or Python 3.9 or later (Python example)
  • A Slack webhook URL or Resend API key for alerts

What You Will Build

A daily monitoring job that:

  1. Reads your company's phone numbers from a config file
  2. Checks each number against the ScamVerify™ API
  3. Compares results against a stored baseline
  4. Detects new complaints, score increases, and verdict changes
  5. Sends alerts through Slack or email when changes are detected
  6. Saves the new baseline for the next run

Set up the project

mkdir brand-monitor && cd brand-monitor
npm init -y
npm install node-cron dotenv

Create a .env file:

SCAMVERIFY_API_KEY=sv_live_your_key_here
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T00/B00/xxxx
RESEND_API_KEY=re_your_key_here
ALERT_EMAIL=security@yourcompany.com
mkdir brand-monitor && cd brand-monitor
python -m venv venv
source venv/bin/activate
pip install requests python-dotenv schedule

Create a .env file:

SCAMVERIFY_API_KEY=sv_live_your_key_here
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T00/B00/xxxx
RESEND_API_KEY=re_your_key_here
ALERT_EMAIL=security@yourcompany.com

Create the phone number config

Create a numbers.json file listing your company's phone numbers with labels:

{
  "numbers": [
    { "number": "5551234567", "label": "Main Office" },
    { "number": "5559876543", "label": "Sales Line" },
    { "number": "5555551212", "label": "Support Hotline" },
    { "number": "5558675309", "label": "Customer Service" },
    { "number": "5552223333", "label": "Billing Department" }
  ]
}

Add or remove numbers as your company's phone inventory changes.

Build the monitoring script

// monitor.js
require('dotenv').config();
const fs = require('fs');
const path = require('path');
const cron = require('node-cron');

const API_KEY = process.env.SCAMVERIFY_API_KEY;
const BASE_URL = 'https://scamverify.ai/api/v1';
const BASELINE_FILE = path.join(__dirname, 'baseline.json');
const NUMBERS_FILE = path.join(__dirname, 'numbers.json');

// Load the phone number list
function loadNumbers() {
  const data = JSON.parse(fs.readFileSync(NUMBERS_FILE, 'utf8'));
  return data.numbers;
}

// Load the baseline (previous scan results)
function loadBaseline() {
  if (!fs.existsSync(BASELINE_FILE)) return {};
  return JSON.parse(fs.readFileSync(BASELINE_FILE, 'utf8'));
}

// Save the new baseline
function saveBaseline(baseline) {
  fs.writeFileSync(BASELINE_FILE, JSON.stringify(baseline, null, 2));
}

// Call ScamVerify API for a single phone number
async function lookupPhone(phoneNumber) {
  const response = await fetch(`${BASE_URL}/phone/lookup`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ phone_number: phoneNumber }),
  });

  if (!response.ok) {
    throw new Error(`API error ${response.status} for ${phoneNumber}`);
  }
  return response.json();
}

// Compare current result against baseline
function detectChanges(number, label, current, previous) {
  const changes = [];

  if (!previous) {
    // First scan for this number
    if (current.risk_score > 0) {
      changes.push({
        type: 'new_risk',
        number,
        label,
        message: `First scan detected risk score ${current.risk_score}/100 (${current.verdict})`,
        score: current.risk_score,
        verdict: current.verdict,
      });
    }
    return changes;
  }

  // Score increased
  if (current.risk_score > previous.risk_score) {
    changes.push({
      type: 'score_increase',
      number,
      label,
      message: `Risk score increased from ${previous.risk_score} to ${current.risk_score}`,
      previousScore: previous.risk_score,
      currentScore: current.risk_score,
      verdict: current.verdict,
    });
  }

  // Verdict worsened
  const severity = { safe: 0, low_risk: 1, medium_risk: 2, high_risk: 3, scam: 4 };
  if ((severity[current.verdict] || 0) > (severity[previous.verdict] || 0)) {
    changes.push({
      type: 'verdict_change',
      number,
      label,
      message: `Verdict changed from ${previous.verdict} to ${current.verdict}`,
      previousVerdict: previous.verdict,
      currentVerdict: current.verdict,
    });
  }

  return changes;
}

// Send alert to Slack
async function sendSlackAlert(changes) {
  const webhookUrl = process.env.SLACK_WEBHOOK_URL;
  if (!webhookUrl) return;

  const blocks = [
    {
      type: 'header',
      text: { type: 'plain_text', text: '\ud83d\udea8 ScamVerify\u2122 Brand Alert' },
    },
    {
      type: 'section',
      text: {
        type: 'mrkdwn',
        text: `*${changes.length} change(s) detected* in your company phone numbers:`,
      },
    },
  ];

  for (const change of changes) {
    blocks.push({
      type: 'section',
      text: {
        type: 'mrkdwn',
        text: `*${change.label}* (\`${change.number}\`)\n${change.message}`,
      },
    });
  }

  blocks.push({
    type: 'context',
    elements: [
      { type: 'mrkdwn', text: `Scan completed at ${new Date().toISOString()}` },
    ],
  });

  await fetch(webhookUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ blocks }),
  });
}

// Send alert via email (Resend)
async function sendEmailAlert(changes) {
  const apiKey = process.env.RESEND_API_KEY;
  const alertEmail = process.env.ALERT_EMAIL;
  if (!apiKey || !alertEmail) return;

  const changeList = changes
    .map(c => `- ${c.label} (${c.number}): ${c.message}`)
    .join('\n');

  await fetch('https://api.resend.com/emails', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${apiKey}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      from: 'alerts@yourcompany.com',
      to: alertEmail,
      subject: `ScamVerify Brand Alert: ${changes.length} change(s) detected`,
      text: `ScamVerify\u2122 Brand Monitor detected changes:\n\n${changeList}\n\nReview at https://scamverify.ai`,
    }),
  });
}

// Main scan function
async function runScan() {
  console.log(`[${new Date().toISOString()}] Starting brand monitor scan...`);

  const numbers = loadNumbers();
  const baseline = loadBaseline();
  const newBaseline = {};
  const allChanges = [];

  for (const { number, label } of numbers) {
    try {
      const result = await lookupPhone(number);
      newBaseline[number] = {
        risk_score: result.risk_score,
        verdict: result.verdict,
        explanation: result.explanation,
        scanned_at: new Date().toISOString(),
      };

      const changes = detectChanges(number, label, result, baseline[number]);
      allChanges.push(...changes);

      console.log(`  ${label} (${number}): score=${result.risk_score}, verdict=${result.verdict}`);

      // Respect rate limits: brief pause between requests
      await new Promise(resolve => setTimeout(resolve, 500));
    } catch (err) {
      console.error(`  Error scanning ${label} (${number}): ${err.message}`);
    }
  }

  // Save updated baseline
  saveBaseline(newBaseline);

  // Send alerts if changes detected
  if (allChanges.length > 0) {
    console.log(`  ${allChanges.length} change(s) detected, sending alerts...`);
    await sendSlackAlert(allChanges);
    await sendEmailAlert(allChanges);
  } else {
    console.log('  No changes detected.');
  }

  console.log('Scan complete.\n');
}

// Run once immediately, then daily at 8 AM
runScan();
cron.schedule('0 8 * * *', runScan);

console.log('Brand monitor started. Scanning daily at 8:00 AM.');
# monitor.py
import os
import json
import time
from datetime import datetime
from pathlib import Path
import requests
import schedule
from dotenv import load_dotenv

load_dotenv()

API_KEY = os.environ["SCAMVERIFY_API_KEY"]
BASE_URL = "https://scamverify.ai/api/v1"
BASELINE_FILE = Path(__file__).parent / "baseline.json"
NUMBERS_FILE = Path(__file__).parent / "numbers.json"


def load_numbers():
    with open(NUMBERS_FILE) as f:
        return json.load(f)["numbers"]


def load_baseline():
    if not BASELINE_FILE.exists():
        return {}
    with open(BASELINE_FILE) as f:
        return json.load(f)


def save_baseline(baseline):
    with open(BASELINE_FILE, "w") as f:
        json.dump(baseline, f, indent=2)


def lookup_phone(phone_number):
    response = requests.post(
        f"{BASE_URL}/phone/lookup",
        headers={
            "Authorization": f"Bearer {API_KEY}",
            "Content-Type": "application/json",
        },
        json={"phone_number": phone_number},
        timeout=30,
    )
    response.raise_for_status()
    return response.json()


def detect_changes(number, label, current, previous):
    changes = []

    if not previous:
        if current["risk_score"] > 0:
            changes.append({
                "type": "new_risk",
                "number": number,
                "label": label,
                "message": f"First scan detected risk score {current['risk_score']}/100 ({current['verdict']})",
            })
        return changes

    if current["risk_score"] > previous["risk_score"]:
        changes.append({
            "type": "score_increase",
            "number": number,
            "label": label,
            "message": f"Risk score increased from {previous['risk_score']} to {current['risk_score']}",
        })

    severity = {"safe": 0, "low_risk": 1, "medium_risk": 2, "high_risk": 3, "scam": 4}
    if severity.get(current["verdict"], 0) > severity.get(previous.get("verdict", "safe"), 0):
        changes.append({
            "type": "verdict_change",
            "number": number,
            "label": label,
            "message": f"Verdict changed from {previous['verdict']} to {current['verdict']}",
        })

    return changes


def send_slack_alert(changes):
    webhook_url = os.environ.get("SLACK_WEBHOOK_URL")
    if not webhook_url:
        return

    text = f"*ScamVerify\u2122 Brand Alert: {len(changes)} change(s) detected*\n\n"
    for change in changes:
        text += f"\u2022 *{change['label']}* (`{change['number']}`): {change['message']}\n"

    requests.post(webhook_url, json={"text": text}, timeout=10)


def send_email_alert(changes):
    api_key = os.environ.get("RESEND_API_KEY")
    alert_email = os.environ.get("ALERT_EMAIL")
    if not api_key or not alert_email:
        return

    change_list = "\n".join(
        f"- {c['label']} ({c['number']}): {c['message']}" for c in changes
    )

    requests.post(
        "https://api.resend.com/emails",
        headers={"Authorization": f"Bearer {api_key}"},
        json={
            "from": "alerts@yourcompany.com",
            "to": alert_email,
            "subject": f"ScamVerify Brand Alert: {len(changes)} change(s)",
            "text": f"ScamVerify\u2122 Brand Monitor detected changes:\n\n{change_list}",
        },
        timeout=10,
    )


def run_scan():
    print(f"[{datetime.now().isoformat()}] Starting brand monitor scan...")

    numbers = load_numbers()
    baseline = load_baseline()
    new_baseline = {}
    all_changes = []

    for entry in numbers:
        number = entry["number"]
        label = entry["label"]
        try:
            result = lookup_phone(number)
            new_baseline[number] = {
                "risk_score": result["risk_score"],
                "verdict": result["verdict"],
                "explanation": result.get("explanation", ""),
                "scanned_at": datetime.now().isoformat(),
            }
            changes = detect_changes(number, label, result, baseline.get(number))
            all_changes.extend(changes)
            print(f"  {label} ({number}): score={result['risk_score']}, verdict={result['verdict']}")
            time.sleep(0.5)  # Respect rate limits
        except Exception as e:
            print(f"  Error scanning {label} ({number}): {e}")

    save_baseline(new_baseline)

    if all_changes:
        print(f"  {len(all_changes)} change(s) detected, sending alerts...")
        send_slack_alert(all_changes)
        send_email_alert(all_changes)
    else:
        print("  No changes detected.")

    print("Scan complete.\n")


# Run once immediately, then daily at 8 AM
run_scan()
schedule.every().day.at("08:00").do(run_scan)

print("Brand monitor started. Scanning daily at 8:00 AM.")
while True:
    schedule.run_pending()
    time.sleep(60)

Run the monitor

node monitor.js
python monitor.py

The first run creates the initial baseline in baseline.json. Subsequent runs compare against this baseline and alert you when any number's risk score increases or verdict worsens.

Example output:

[2026-03-10T08:00:00.000Z] Starting brand monitor scan...
  Main Office (5551234567): score=0, verdict=safe
  Sales Line (5559876543): score=0, verdict=safe
  Support Hotline (5555551212): score=35, verdict=medium_risk
  2 change(s) detected, sending alerts...
Scan complete.

Deploy as a background service

For production, run the monitor as a persistent background service.

Option A: Docker

FROM node:18-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
CMD ["node", "monitor.js"]

Option B: Systemd (Linux)

[Unit]
Description=ScamVerify Brand Monitor
After=network.target

[Service]
Type=simple
WorkingDirectory=/opt/brand-monitor
ExecStart=/usr/bin/node monitor.js
Restart=always
EnvironmentFile=/opt/brand-monitor/.env

[Install]
WantedBy=multi-user.target

Option C: Cloud scheduler

Instead of running a persistent process, trigger the scan from a cloud scheduler:

  • AWS: EventBridge rule + Lambda function
  • GCP: Cloud Scheduler + Cloud Function
  • Vercel: Vercel Cron + API route

For the cloud approach, remove the cron.schedule() call and export the runScan function as the handler.

For companies with many phone numbers, use the batch endpoint to check up to 100 numbers in a single API call. This is faster and counts as fewer quota hits.

Next Steps

  • Add historical trend tracking by storing daily scores in a database
  • Build a dashboard that shows risk score trends over time for each number
  • Set up multiple alert thresholds (warning at score 20, critical at score 50)
  • Monitor competitor phone numbers to detect industry-wide spoofing campaigns
  • Add URL monitoring for your company's domains using the /url/lookup endpoint

On this page