ScamVerify™
Tutorials

Build a Phone Verification Flow

Step-by-step tutorial for building a complete phone verification flow with Node.js and Express using the ScamVerify™ API.

This tutorial walks you through building a complete phone verification flow in Node.js with Express. By the end, you will have a working API endpoint that accepts a phone number, calls the ScamVerify™ API, interprets the verdict, and returns a clear result to your user.

Prerequisites

What You Will Build

A /verify endpoint that:

  1. Accepts a phone number from a form or API call
  2. Validates the input format
  3. Calls the ScamVerify™ phone lookup API
  4. Interprets the risk score and verdict
  5. Returns a structured result to the user
  6. Handles errors gracefully

Set up the project

Create a new directory and initialize the project.

mkdir phone-verifier && cd phone-verifier
npm init -y
npm install express dotenv

Create a .env file with your API key:

SCAMVERIFY_API_KEY=sv_live_your_key_here
PORT=3000

Create the ScamVerify™ client

Create a reusable client module that handles API communication, retries, and error classification.

// scamverify.js
class ScamVerifyClient {
  constructor(apiKey) {
    if (!apiKey || !apiKey.startsWith('sv_')) {
      throw new Error('Invalid ScamVerify 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 === 401) {
      throw new Error('Invalid or revoked API key');
    }
    if (response.status === 402) {
      throw new Error('Quota exhausted. Upgrade your plan or wait for the monthly reset.');
    }
    if (response.status === 429) {
      const retryAfter = response.headers.get('Retry-After') || '60';
      throw new Error(`Rate limited. Retry after ${retryAfter} seconds.`);
    }
    if (!response.ok) {
      throw new Error(`ScamVerify API error: ${response.status}`);
    }

    return response.json();
  }
}

module.exports = { ScamVerifyClient };

Build the verification endpoint

Create the Express server with input validation, API call, and result interpretation.

// server.js
require('dotenv').config();
const express = require('express');
const { ScamVerifyClient } = require('./scamverify');

const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

const client = new ScamVerifyClient(process.env.SCAMVERIFY_API_KEY);

// Phone number format validation
function isValidPhoneFormat(phone) {
  // Accept E.164, (xxx) xxx-xxxx, xxx-xxx-xxxx, and 10-digit formats
  const cleaned = phone.replace(/[\s\-\(\)\.]/g, '');
  if (cleaned.startsWith('+1') && cleaned.length === 12) return true;
  if (cleaned.startsWith('1') && cleaned.length === 11) return true;
  if (cleaned.length === 10 && /^\d+$/.test(cleaned)) return true;
  return false;
}

// Normalize to E.164 format
function normalizePhone(phone) {
  const digits = phone.replace(/\D/g, '');
  if (digits.length === 10) return `+1${digits}`;
  if (digits.length === 11 && digits.startsWith('1')) return `+${digits}`;
  return phone; // Already in E.164 or let the API handle it
}

// Interpret the verdict into a user-friendly result
function interpretResult(apiResponse) {
  const { risk_score, verdict, explanation, signals, cached } = apiResponse;

  const levels = {
    safe: { label: 'Safe', color: 'green', action: 'This number appears legitimate.' },
    low_risk: { label: 'Low Risk', color: 'blue', action: 'This number is probably safe, but exercise normal caution.' },
    medium_risk: { label: 'Medium Risk', color: 'yellow', action: 'This number has some risk factors. Proceed with caution.' },
    high_risk: { label: 'High Risk', color: 'orange', action: 'This number has significant risk indicators. We recommend avoiding it.' },
    critical: { label: 'Dangerous', color: 'red', action: 'This number is associated with known scam activity. Do not engage.' },
  };

  const level = levels[verdict] || levels.medium_risk;

  return {
    riskScore: risk_score,
    verdict: level.label,
    color: level.color,
    recommendation: level.action,
    explanation,
    details: {
      carrier: signals.carrier || 'Unknown',
      lineType: signals.line_type || 'Unknown',
      ftcComplaints: signals.ftc_complaints || 0,
      fccComplaints: signals.fcc_complaints || 0,
      robocallDetected: signals.robocall_detected || false,
      communityReports: signals.community_reports || 0,
    },
    cached,
  };
}

// POST /verify - Main verification endpoint
app.post('/verify', async (req, res) => {
  const { phone_number } = req.body;

  // Validate input
  if (!phone_number) {
    return res.status(400).json({
      error: 'Missing phone_number field',
      hint: 'Send a JSON body with {"phone_number": "+12025551234"}',
    });
  }

  if (!isValidPhoneFormat(phone_number)) {
    return res.status(400).json({
      error: 'Invalid phone number format',
      hint: 'Use E.164 format (+12025551234), (202) 555-1234, or 2025551234',
    });
  }

  try {
    const normalized = normalizePhone(phone_number);
    const apiResponse = await client.lookupPhone(normalized);
    const result = interpretResult(apiResponse);

    return res.json({
      success: true,
      phoneNumber: normalized,
      ...result,
    });
  } catch (error) {
    console.error('Verification failed:', error.message);

    if (error.message.includes('Quota exhausted')) {
      return res.status(503).json({
        error: 'Service temporarily unavailable',
        message: 'Verification quota has been reached. Please try again later.',
      });
    }

    if (error.message.includes('Rate limited')) {
      return res.status(429).json({
        error: 'Too many requests',
        message: 'Please wait a moment before trying again.',
      });
    }

    return res.status(500).json({
      error: 'Verification failed',
      message: 'Unable to verify this phone number. Please try again.',
    });
  }
});

// GET /health - Health check
app.get('/health', (req, res) => {
  res.json({ status: 'ok', apiKeyConfigured: !!process.env.SCAMVERIFY_API_KEY });
});

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

Test the endpoint

Start the server and test with cURL.

node server.js

In another terminal:

# Test with E.164 format
curl -X POST http://localhost:3000/verify \
  -H "Content-Type: application/json" \
  -d '{"phone_number": "+12025551234"}'

# Test with standard US format
curl -X POST http://localhost:3000/verify \
  -H "Content-Type: application/json" \
  -d '{"phone_number": "(202) 555-1234"}'

# Test with missing number (should return 400)
curl -X POST http://localhost:3000/verify \
  -H "Content-Type: application/json" \
  -d '{}'

Handle the response in your frontend

Here is how you might call the /verify endpoint from a web form.

<form id="verifyForm">
  <input type="tel" id="phoneInput" placeholder="(555) 123-4567" required />
  <button type="submit">Verify Number</button>
</form>
<div id="result"></div>

<script>
document.getElementById('verifyForm').addEventListener('submit', async (e) => {
  e.preventDefault();
  const phone = document.getElementById('phoneInput').value;
  const resultDiv = document.getElementById('result');

  resultDiv.textContent = 'Checking...';

  try {
    const response = await fetch('/verify', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ phone_number: phone }),
    });

    const data = await response.json();

    if (data.success) {
      resultDiv.innerHTML = `
        <strong>${data.verdict}</strong> (Score: ${data.riskScore}/100)<br>
        ${data.recommendation}<br>
        <small>Carrier: ${data.details.carrier} | Type: ${data.details.lineType}</small>
      `;
    } else {
      resultDiv.textContent = data.message || data.error;
    }
  } catch (err) {
    resultDiv.textContent = 'Failed to verify. Please try again.';
  }
});
</script>

Add error handling best practices

For production use, add retry logic for transient failures.

async function lookupWithRetry(client, phoneNumber, maxRetries = 2) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await client.lookupPhone(phoneNumber);
    } catch (error) {
      // Do not retry auth or quota errors
      if (error.message.includes('Invalid') || error.message.includes('Quota')) {
        throw error;
      }

      if (attempt === maxRetries) {
        throw error;
      }

      // Wait before retrying (exponential backoff)
      const delay = Math.pow(2, attempt) * 1000;
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

Complete Project Structure

phone-verifier/
  .env                  # API key and port
  scamverify.js         # API client module
  server.js             # Express server with /verify endpoint
  package.json

Use test keys for development. Replace sv_live_ with sv_test_ in your .env file during development. Test keys return realistic mock data without consuming your quota.

Next Steps

On this page