ScamVerify™
Tutorials

Build a WhatsApp Scam Checker Bot

Let users text a phone number to WhatsApp and get an instant risk assessment from the ScamVerify™ API.

This tutorial walks you through building a WhatsApp bot that lets anyone send a phone number and get back an instant scam risk assessment. The bot uses the Meta Cloud API for WhatsApp Business and the ScamVerify™ API for threat intelligence. By the end, you will have a working bot that anyone can message to check suspicious phone numbers.

Prerequisites

  • Node.js 18 or later
  • A ScamVerify™ API key (get one at scamverify.ai/settings/api)
  • A Meta Business account with WhatsApp Business API access
  • ngrok for local development

What You Will Build

A WhatsApp bot that:

  1. Receives incoming messages via the Meta Cloud API webhook
  2. Extracts phone numbers from the message text
  3. Calls the ScamVerify™ phone lookup API
  4. Sends a formatted reply with the risk score, verdict, and explanation
  5. Handles errors and invalid input gracefully

Set up the Meta WhatsApp Business API

  1. Go to developers.facebook.com and create a new app (type: Business)

  2. Add the WhatsApp product to your app

  3. In the WhatsApp section, note your:

    • Phone Number ID (the ID of the test number Meta provides)
    • WhatsApp Business Account ID
    • Temporary Access Token (for development, you will create a permanent one later)
  4. Under Quickstart > Configuration, set up a webhook:

    • Callback URL: Your server URL (set up in the next step)
    • Verify Token: A secret string you choose (e.g., scamverify_webhook_secret)
    • Subscribe to: messages

Meta provides a free test phone number for development. You can send and receive messages without going through the full business verification process.

Set up the project

mkdir whatsapp-scambot && cd whatsapp-scambot
npm init -y
npm install express dotenv

Create a .env file:

SCAMVERIFY_API_KEY=sv_live_your_key_here
WHATSAPP_TOKEN=your-meta-access-token
WHATSAPP_PHONE_ID=your-phone-number-id
WEBHOOK_VERIFY_TOKEN=scamverify_webhook_secret
PORT=3000

Create the ScamVerify™ client

// scamverify.js
class ScamVerifyClient {
  constructor(apiKey) {
    if (!apiKey || !apiKey.startsWith('sv_')) {
      throw new Error('Invalid API key. Keys must start with sv_live_ or sv_test_');
    }
    this.apiKey = apiKey;
    this.baseUrl = 'https://scamverify.ai/api/v1';
  }

  async lookupPhone(phoneNumber) {
    const response = await fetch(`${this.baseUrl}/phone/lookup`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ phone_number: phoneNumber }),
    });

    if (response.status === 429) {
      throw new Error('Rate limited. Please try again in a minute.');
    }
    if (!response.ok) {
      throw new Error(`API error: ${response.status}`);
    }
    return response.json();
  }
}

module.exports = { ScamVerifyClient };

Create the WhatsApp message sender

Build a helper module for sending replies through the Meta Cloud API.

// whatsapp.js
const WHATSAPP_TOKEN = process.env.WHATSAPP_TOKEN;
const WHATSAPP_PHONE_ID = process.env.WHATSAPP_PHONE_ID;

async function sendMessage(to, text) {
  const response = await fetch(
    `https://graph.facebook.com/v18.0/${WHATSAPP_PHONE_ID}/messages`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${WHATSAPP_TOKEN}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        messaging_product: 'whatsapp',
        to,
        type: 'text',
        text: { body: text },
      }),
    }
  );

  if (!response.ok) {
    const error = await response.json();
    console.error('WhatsApp send error:', error);
    throw new Error(`Failed to send message: ${response.status}`);
  }
  return response.json();
}

module.exports = { sendMessage };

Create the message formatter

Format ScamVerify™ results into clean WhatsApp messages with emojis for visual clarity.

// formatter.js

const VERDICT_EMOJI = {
  safe: '\u2705',
  low_risk: '\ud83d\udfe2',
  medium_risk: '\u26a0\ufe0f',
  high_risk: '\ud83d\udea8',
  scam: '\ud83d\udeab',
};

function formatResult(phoneNumber, result) {
  const emoji = VERDICT_EMOJI[result.verdict] || '\u2753';
  const verdictLabel = result.verdict.replace('_', ' ').toUpperCase();
  const confidence = Math.round(result.confidence * 100);

  let message = `${emoji} *ScamVerify\u2122 Report*\n\n`;
  message += `*Number:* ${phoneNumber}\n`;
  message += `*Verdict:* ${verdictLabel}\n`;
  message += `*Risk Score:* ${result.risk_score}/100\n`;
  message += `*Confidence:* ${confidence}%\n\n`;
  message += `${result.explanation}\n\n`;
  message += `_Powered by scamverify.ai_`;

  return message;
}

function formatError(phoneNumber, errorMessage) {
  return `\u274c Could not check *${phoneNumber}*\n\n${errorMessage}\n\nPlease try again later.`;
}

function formatHelp() {
  return (
    `*ScamVerify\u2122 Bot*\n\n` +
    `Send me a phone number and I'll check if it's associated with scams.\n\n` +
    `*How to use:*\n` +
    `Just send a US phone number in any format:\n` +
    `\u2022 (555) 123-4567\n` +
    `\u2022 555-123-4567\n` +
    `\u2022 5551234567\n` +
    `\u2022 +15551234567\n\n` +
    `_Powered by scamverify.ai_`
  );
}

module.exports = { formatResult, formatError, formatHelp };

Build the webhook server

Create the Express server that handles webhook verification, receives incoming messages, and sends replies.

// server.js
require('dotenv').config();
const express = require('express');
const { ScamVerifyClient } = require('./scamverify');
const { sendMessage } = require('./whatsapp');
const { formatResult, formatError, formatHelp } = require('./formatter');

const app = express();
app.use(express.json());

const scamverify = new ScamVerifyClient(process.env.SCAMVERIFY_API_KEY);
const VERIFY_TOKEN = process.env.WEBHOOK_VERIFY_TOKEN;

// Extract a phone number from message text
function extractPhone(text) {
  const cleaned = text.trim();

  // Check for help commands
  if (['help', 'hi', 'hello', 'start', 'menu'].includes(cleaned.toLowerCase())) {
    return { type: 'help' };
  }

  // Extract digits
  const digits = cleaned.replace(/\D/g, '');

  if (digits.length === 10) {
    return { type: 'phone', number: digits };
  }
  if (digits.length === 11 && digits.startsWith('1')) {
    return { type: 'phone', number: digits };
  }

  return { type: 'invalid' };
}

// Webhook verification (Meta sends a GET request to verify your endpoint)
app.get('/webhook', (req, res) => {
  const mode = req.query['hub.mode'];
  const token = req.query['hub.verify_token'];
  const challenge = req.query['hub.challenge'];

  if (mode === 'subscribe' && token === VERIFY_TOKEN) {
    console.log('Webhook verified');
    return res.status(200).send(challenge);
  }
  return res.sendStatus(403);
});

// Incoming message handler
app.post('/webhook', async (req, res) => {
  // Respond immediately to avoid Meta retries
  res.sendStatus(200);

  const body = req.body;

  if (body.object !== 'whatsapp_business_account') return;

  const entries = body.entry || [];
  for (const entry of entries) {
    const changes = entry.changes || [];
    for (const change of changes) {
      const messages = change.value?.messages || [];
      for (const message of messages) {
        if (message.type !== 'text') continue;

        const from = message.from; // sender's phone number
        const text = message.text?.body || '';

        await handleMessage(from, text);
      }
    }
  }
});

async function handleMessage(from, text) {
  const parsed = extractPhone(text);

  if (parsed.type === 'help') {
    await sendMessage(from, formatHelp());
    return;
  }

  if (parsed.type === 'invalid') {
    await sendMessage(
      from,
      'I couldn\'t find a valid US phone number in your message. ' +
      'Send a 10-digit phone number, or type *help* for instructions.'
    );
    return;
  }

  try {
    const result = await scamverify.lookupPhone(parsed.number);
    await sendMessage(from, formatResult(parsed.number, result));
  } catch (err) {
    console.error(`Lookup error for ${parsed.number}:`, err.message);
    await sendMessage(from, formatError(parsed.number, err.message));
  }
}

// Health check
app.get('/health', (req, res) => {
  res.json({ status: 'healthy' });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`WhatsApp bot running on port ${PORT}`);
});

Run and test

Start the server and expose it with ngrok:

node server.js
# In another terminal:
ngrok http 3000

Copy the ngrok HTTPS URL and paste it into your Meta app's webhook configuration:

  • Callback URL: https://your-ngrok-url/webhook
  • Verify Token: scamverify_webhook_secret

Click Verify and Save. Meta will send a GET request to verify the endpoint.

Now send a message to your WhatsApp test number:

(555) 867-5309

The bot should reply with a formatted risk assessment within a few seconds.

Try sending help to see the instructions message, and try sending hello world to see the invalid input response.

Deploy to production

For production, you need a permanent access token and a verified WhatsApp Business number.

  1. Permanent token: Create a System User in Meta Business Manager, generate a token with whatsapp_business_messaging permission
  2. Business verification: Complete Meta's business verification process
  3. Phone number: Register a real phone number (instead of the test number)

Deploy the server to Railway, Render, or any Node.js hosting:

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

Update the webhook URL in Meta to point to your production domain.

WhatsApp Business API pricing: Meta charges per conversation (approximately $0.005 to $0.08 depending on region and conversation type). Budget for this alongside your ScamVerify™ API costs.

Next Steps

  • Add URL checking by detecting http:// and https:// in messages
  • Support bulk checks by accepting comma-separated phone numbers
  • Add message templates for proactive scam alerts
  • Track usage with a simple SQLite database to monitor popular queries

On this page