Build a Phone Number Checker Chrome Extension
Right-click any phone number on a webpage to instantly check its scam risk using the ScamVerify™ API.
This tutorial walks you through building a Chrome extension that lets you highlight any phone number on a webpage, right-click it, and instantly check its scam risk through the ScamVerify™ API. The result appears in a clean popup with a color-coded verdict.
Prerequisites
- Google Chrome browser
- A ScamVerify™ API key (get one at scamverify.ai/settings/api)
- Basic familiarity with HTML, CSS, and JavaScript
What You Will Build
A Chrome extension that:
- Adds a "Check with ScamVerify™" option to the right-click context menu
- Sends the selected text to the ScamVerify™ phone lookup API
- Displays the risk score, verdict, and explanation in a popup
- Color-codes results by severity
Create the project structure
Create a folder for the extension with these files:
mkdir scamverify-extension && cd scamverify-extension
touch manifest.json background.js popup.html popup.js popup.cssYour directory should look like this:
scamverify-extension/
manifest.json
background.js
popup.html
popup.js
popup.cssWrite the manifest
Create manifest.json using Manifest V3. This file declares the extension's permissions, background service worker, and popup.
{
"manifest_version": 3,
"name": "ScamVerify Phone Checker",
"version": "1.0.0",
"description": "Right-click any phone number to check its scam risk with ScamVerify\u2122.",
"permissions": [
"contextMenus",
"storage",
"activeTab"
],
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup.html",
"default_title": "ScamVerify\u2122 Phone Checker"
},
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
}You can use any 16x16, 48x48, and 128x128 PNG images as icons. For testing, the extension works without icons.
Build the background service worker
The background script creates the context menu item and handles the API call when the user right-clicks selected text.
// background.js
// Create context menu item on install
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create({
id: 'scamverify-check',
title: 'Check "%s" with ScamVerify\u2122',
contexts: ['selection'],
});
});
// Normalize selected text to a phone number (digits only)
function normalizePhone(text) {
const digits = text.replace(/\D/g, '');
// Handle 10-digit (US) or 11-digit (1 + US) numbers
if (digits.length === 10) return digits;
if (digits.length === 11 && digits.startsWith('1')) return digits;
return null;
}
// Call ScamVerify API
async function lookupPhone(phoneNumber, apiKey) {
const response = await fetch('https://scamverify.ai/api/v1/phone/lookup', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ phone_number: phoneNumber }),
});
if (response.status === 401) {
throw new Error('Invalid API key. Check your settings.');
}
if (response.status === 402) {
throw new Error('Quota exhausted. Upgrade your plan at scamverify.ai.');
}
if (response.status === 429) {
throw new Error('Rate limited. Please wait a moment and try again.');
}
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return response.json();
}
// Handle context menu click
chrome.contextMenus.onClicked.addListener(async (info) => {
if (info.menuItemId !== 'scamverify-check') return;
const selectedText = info.selectionText || '';
const phone = normalizePhone(selectedText);
if (!phone) {
chrome.storage.local.set({
lastResult: { error: `"${selectedText}" does not look like a valid phone number.` },
});
return;
}
// Get the API key from storage
const { apiKey } = await chrome.storage.local.get('apiKey');
if (!apiKey) {
chrome.storage.local.set({
lastResult: { error: 'No API key configured. Click the extension icon to add one.' },
});
return;
}
// Set loading state
chrome.storage.local.set({ lastResult: { loading: true, phone } });
try {
const result = await lookupPhone(phone, apiKey);
chrome.storage.local.set({
lastResult: { phone, ...result },
});
} catch (err) {
chrome.storage.local.set({
lastResult: { error: err.message, phone },
});
}
});Create the popup HTML
The popup displays the most recent scan result and provides a settings area to enter the API key.
<!-- popup.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="container">
<h1>ScamVerify™ Checker</h1>
<!-- Settings section -->
<div id="settings-section">
<label for="api-key-input">API Key</label>
<div class="input-row">
<input type="password" id="api-key-input" placeholder="sv_live_..." />
<button id="save-key-btn">Save</button>
</div>
<p id="key-status" class="status-text"></p>
</div>
<hr />
<!-- Result section -->
<div id="result-section">
<p class="hint">Highlight a phone number on any webpage, right-click, and select "Check with ScamVerify™".</p>
</div>
</div>
<script src="popup.js"></script>
</body>
</html>Style the popup
/* popup.css */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 340px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 14px;
color: #1a1a1a;
background: #ffffff;
}
.container {
padding: 16px;
}
h1 {
font-size: 16px;
font-weight: 700;
margin-bottom: 12px;
color: #031c2e;
}
label {
font-size: 12px;
font-weight: 600;
color: #555;
display: block;
margin-bottom: 4px;
}
.input-row {
display: flex;
gap: 8px;
}
input {
flex: 1;
padding: 6px 10px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 13px;
}
button {
padding: 6px 14px;
background: #031c2e;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
}
button:hover {
background: #043c52;
}
.status-text {
font-size: 12px;
margin-top: 4px;
color: #36a64f;
}
hr {
margin: 14px 0;
border: none;
border-top: 1px solid #eee;
}
.hint {
color: #888;
font-size: 13px;
line-height: 1.4;
}
.result-card {
border: 1px solid #ddd;
border-radius: 10px;
padding: 14px;
border-left: 4px solid #808080;
}
.result-card.safe { border-left-color: #36a64f; }
.result-card.low_risk { border-left-color: #2196F3; }
.result-card.medium_risk { border-left-color: #FF9800; }
.result-card.high_risk { border-left-color: #f44336; }
.result-card.scam { border-left-color: #d32f2f; }
.result-phone {
font-size: 15px;
font-weight: 700;
margin-bottom: 8px;
}
.result-verdict {
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
margin-bottom: 4px;
}
.result-score {
font-size: 13px;
color: #555;
margin-bottom: 8px;
}
.result-explanation {
font-size: 13px;
line-height: 1.5;
color: #333;
}
.error-text {
color: #d32f2f;
font-size: 13px;
}
.loading {
color: #888;
font-style: italic;
}Build the popup logic
The popup script loads the saved API key, listens for the save button, and renders the latest scan result.
// popup.js
const apiKeyInput = document.getElementById('api-key-input');
const saveKeyBtn = document.getElementById('save-key-btn');
const keyStatus = document.getElementById('key-status');
const resultSection = document.getElementById('result-section');
// Load saved API key
chrome.storage.local.get('apiKey', ({ apiKey }) => {
if (apiKey) {
apiKeyInput.value = apiKey;
keyStatus.textContent = 'Key saved';
}
});
// Save API key
saveKeyBtn.addEventListener('click', () => {
const key = apiKeyInput.value.trim();
if (!key.startsWith('sv_')) {
keyStatus.textContent = 'Invalid key (must start with sv_)';
keyStatus.style.color = '#d32f2f';
return;
}
chrome.storage.local.set({ apiKey: key }, () => {
keyStatus.textContent = 'Key saved';
keyStatus.style.color = '#36a64f';
});
});
// Render the last result
function renderResult(data) {
if (!data) return;
if (data.loading) {
resultSection.innerHTML = `<p class="loading">Checking ${data.phone}...</p>`;
return;
}
if (data.error) {
resultSection.innerHTML = `<p class="error-text">${data.error}</p>`;
return;
}
const verdictLabel = data.verdict.replace('_', ' ').toUpperCase();
resultSection.innerHTML = `
<div class="result-card ${data.verdict}">
<div class="result-phone">${data.phone}</div>
<div class="result-verdict">${verdictLabel}</div>
<div class="result-score">Risk Score: ${data.risk_score}/100 • Confidence: ${Math.round(data.confidence * 100)}%</div>
<div class="result-explanation">${data.explanation}</div>
</div>
`;
}
// Load and display the latest result
chrome.storage.local.get('lastResult', ({ lastResult }) => {
renderResult(lastResult);
});
// Listen for changes (updates while popup is open)
chrome.storage.onChanged.addListener((changes) => {
if (changes.lastResult) {
renderResult(changes.lastResult.newValue);
}
});Load and test the extension
- Open Chrome and navigate to
chrome://extensions - Enable Developer mode (toggle in the top right)
- Click Load unpacked and select your
scamverify-extensionfolder - Click the extension icon in the toolbar and enter your ScamVerify™ API key
- Navigate to any webpage with a phone number, highlight it, right-click, and select Check "..." with ScamVerify™
- Click the extension icon again to see the result
The extension stores your API key locally in Chrome's extension storage. It is never sent anywhere other than the ScamVerify™ API.
Next Steps
- Add URL checking by detecting
https://links in the selected text - Add a badge on the extension icon showing the last verdict (green, yellow, red)
- Publish to the Chrome Web Store for your team
- Add a history view that shows all recent checks