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:
- Reads your company's phone numbers from a config file
- Checks each number against the ScamVerify™ API
- Compares results against a stored baseline
- Detects new complaints, score increases, and verdict changes
- Sends alerts through Slack or email when changes are detected
- 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 dotenvCreate 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.commkdir brand-monitor && cd brand-monitor
python -m venv venv
source venv/bin/activate
pip install requests python-dotenv scheduleCreate 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.comCreate 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.jspython monitor.pyThe 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.targetOption 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/lookupendpoint