{}</>
Developer11 min readJanuary 27, 2026

Building Email Validation: The Developer's Guide

A comprehensive tutorial covering BigShield SDK installation, basic validation, score interpretation, edge case handling, and webhook integration with full TypeScript code examples.

What We Are Building

By the end of this guide, you will have a fully integrated email validation system that scores signups in real time, handles edge cases gracefully, and can receive asynchronous enrichment data via webhooks. We will use the BigShield TypeScript SDK, but the concepts apply regardless of your stack.

Prerequisites: Node.js 18+, a BigShield API key (the free tier works for everything here), and a basic Express or Next.js app to integrate with.

Step 1: Installation and Setup

Install the SDK:

npm install @bigshield/sdk

Initialize the client:

import { BigShield } from '@bigshield/sdk';

const bigshield = new BigShield({
  apiKey: process.env.BIGSHIELD_API_KEY!,
  // Optional: set timeout (default 5000ms)
  timeout: 3000,
  // Optional: enable debug logging
  debug: process.env.NODE_ENV === 'development',
});

The SDK automatically handles retries (up to 2, with exponential backoff), connection pooling, and response caching. You should create a single instance and reuse it across your application.

Step 2: Basic Email Validation

The simplest integration is a single validation call:

import { BigShield } from '@bigshield/sdk';

const bigshield = new BigShield({
  apiKey: process.env.BIGSHIELD_API_KEY!,
});

async function validateSignup(email: string) {
  const result = await bigshield.validate({
    email,
  });

  console.log(result);
  // {
  //   score: 82,
  //   verdict: 'pass',
  //   signals: [...],
  //   requestId: 'req_abc123',
  //   latencyMs: 47,
  // }

  return result;
}

The response includes a score (0-100, where 100 is most trustworthy), a verdict ('pass', 'warn', or 'fail'), and an array of signals that contributed to the score.

Step 3: Understanding the Score

BigShield scores range from 0 to 100. Here is how to interpret them:

  • 85-100: High trust. Legitimate email with strong positive signals. Let them through.
  • 60-84: Moderate trust. Probably legitimate but some minor flags. Let them through, maybe monitor.
  • 30-59: Low trust. Multiple suspicious signals. Consider additional verification (CAPTCHA, phone verification).
  • 0-29: Very low trust. Strong indicators of fraud. Block or require manual review.

A common mistake is treating the score as binary. Do not simply block everything below 50. Instead, build tiered responses:

import type { ValidationResult } from '@bigshield/sdk';

type SignupAction =
  | { type: 'allow' }
  | { type: 'allow_with_monitoring' }
  | { type: 'challenge'; method: 'captcha' | 'phone' }
  | { type: 'block'; reason: string };

function decideAction(result: ValidationResult): SignupAction {
  const { score, verdict, signals } = result;

  // Hard blocks: known disposable domains, blacklisted IPs
  const hasHardBlock = signals.some(
    (s) => s.name === 'disposable_domain' && s.confidence > 0.9
  );
  if (hasHardBlock) {
    return { type: 'block', reason: 'Disposable email detected' };
  }

  // Score-based tiers
  if (score >= 85) {
    return { type: 'allow' };
  }

  if (score >= 60) {
    return { type: 'allow_with_monitoring' };
  }

  if (score >= 30) {
    // Check which signals fired to pick the right challenge
    const hasIpFlag = signals.some(
      (s) => s.category === 'ip' && s.score_impact < -10
    );
    return {
      type: 'challenge',
      method: hasIpFlag ? 'phone' : 'captcha',
    };
  }

  return { type: 'block', reason: 'Email failed validation' };
}

Step 4: Enriching Validation with Context

The more context you provide, the more accurate the score. Beyond the email address, you can pass IP address, user agent, and custom metadata:

async function validateWithContext(
  email: string,
  req: Request
) {
  const result = await bigshield.validate({
    email,
    ip: req.headers.get('x-forwarded-for')?.split(',')[0]
      ?? req.headers.get('x-real-ip')
      ?? undefined,
    userAgent: req.headers.get('user-agent') ?? undefined,
    metadata: {
      signupSource: 'landing-page',
      plan: 'free',
    },
  });

  return result;
}

Including the IP address enables several additional signals: datacenter detection, VPN/proxy identification, geographic anomaly detection, and IP reputation scoring. This alone can improve detection accuracy by 15-20%.

Step 5: Handling Edge Cases

Production systems need to handle failures gracefully. Here are the edge cases you should plan for:

Network Timeouts

import { BigShieldError, TimeoutError } from '@bigshield/sdk';

async function safeValidate(email: string) {
  try {
    const result = await bigshield.validate({ email });
    return { success: true, result } as const;
  } catch (err) {
    if (err instanceof TimeoutError) {
      // BigShield took too long. Fail open or fail closed
      // depending on your risk tolerance.
      console.warn('Validation timeout, failing open');
      return {
        success: false,
        fallback: 'allow_with_monitoring',
      } as const;
    }

    if (err instanceof BigShieldError) {
      // API error (rate limit, auth, etc.)
      console.error('BigShield API error:', err.code, err.message);
      return {
        success: false,
        fallback: 'allow_with_monitoring',
      } as const;
    }

    throw err; // Unknown error, rethrow
  }
}

Rate Limiting

The SDK automatically handles rate limits with backoff, but you should still plan for burst scenarios:

// For high-volume signups, use the batch endpoint
async function validateBatch(emails: string[]) {
  // Batch validates up to 100 emails in a single request
  const results = await bigshield.validateBatch(
    emails.map((email) => ({ email }))
  );

  return results;
  // Returns: Array<ValidationResult> in the same order
}

The "Plus Addressing" Edge Case

Gmail and some other providers support plus addressing (user+tag@gmail.com). Fraudsters use this to create seemingly unique emails from a single account:

// BigShield automatically normalizes plus addresses
// and dot variations for Gmail, but you can also
// handle this yourself:
function normalizeEmail(email: string): string {
  const [localPart, domain] = email.toLowerCase().split('@');

  if (['gmail.com', 'googlemail.com'].includes(domain)) {
    const withoutPlus = localPart.split('+')[0];
    const withoutDots = withoutPlus.replace(/\./g, '');
    return `${withoutDots}@gmail.com`;
  }

  return `${localPart}@${domain}`;
}

BigShield handles this normalization server-side, but it is useful to also normalize on your end for deduplication in your own database.

Step 6: Express.js Integration

Here is a complete middleware implementation for Express:

import express from 'express';
import { BigShield } from '@bigshield/sdk';

const app = express();
const bigshield = new BigShield({
  apiKey: process.env.BIGSHIELD_API_KEY!,
});

app.use(express.json());

app.post('/api/signup', async (req, res) => {
  const { email, password, name } = req.body;

  // 1. Validate email with BigShield
  let validation;
  try {
    validation = await bigshield.validate({
      email,
      ip: req.ip,
      userAgent: req.get('user-agent'),
    });
  } catch {
    // Fail open: allow signup but flag for review
    validation = null;
  }

  // 2. Decide what to do
  if (validation && validation.score < 30) {
    return res.status(422).json({
      error: 'Unable to verify this email address.',
      code: 'EMAIL_VALIDATION_FAILED',
    });
  }

  if (validation && validation.score < 60) {
    // Create account but require email verification
    const user = await createUser({
      email, name, password,
      requiresVerification: true,
      riskScore: validation.score,
    });

    return res.status(201).json({
      user: { id: user.id, email: user.email },
      requiresVerification: true,
    });
  }

  // 3. Score is good, create account normally
  const user = await createUser({
    email, name, password,
    requiresVerification: false,
    riskScore: validation?.score ?? null,
  });

  res.status(201).json({
    user: { id: user.id, email: user.email },
    requiresVerification: false,
  });
});

function createUser(data: {
  email: string;
  name: string;
  password: string;
  requiresVerification: boolean;
  riskScore: number | null;
}) {
  // Your user creation logic here
  return { id: 'usr_123', email: data.email };
}

app.listen(3000);

Step 7: Next.js App Router Integration

If you are using Next.js with the App Router, here is the equivalent as a Route Handler:

// app/api/signup/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { BigShield } from '@bigshield/sdk';

const bigshield = new BigShield({
  apiKey: process.env.BIGSHIELD_API_KEY!,
});

export async function POST(req: NextRequest) {
  const { email, password, name } = await req.json();

  const ip = req.headers.get('x-forwarded-for')?.split(',')[0]
    ?? req.headers.get('x-real-ip')
    ?? req.ip;

  const validation = await bigshield.validate({
    email,
    ip,
    userAgent: req.headers.get('user-agent') ?? undefined,
  });

  if (validation.score < 30) {
    return NextResponse.json(
      { error: 'Unable to verify this email address.' },
      { status: 422 }
    );
  }

  // Continue with user creation...
  return NextResponse.json(
    { success: true, requiresVerification: validation.score < 60 },
    { status: 201 }
  );
}

Step 8: Webhook Integration for Async Signals

Some signals (Tier 2) run asynchronously and complete after the initial response. BigShield sends webhook events when these signals resolve, allowing you to update your risk assessment:

// app/api/webhooks/bigshield/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { BigShield } from '@bigshield/sdk';
import crypto from 'crypto';

const bigshield = new BigShield({
  apiKey: process.env.BIGSHIELD_API_KEY!,
});

export async function POST(req: NextRequest) {
  const body = await req.text();
  const signature = req.headers.get('x-bigshield-signature');

  // 1. Verify the webhook signature
  const isValid = bigshield.webhooks.verify({
    payload: body,
    signature: signature ?? '',
    secret: process.env.BIGSHIELD_WEBHOOK_SECRET!,
  });

  if (!isValid) {
    return NextResponse.json(
      { error: 'Invalid signature' },
      { status: 401 }
    );
  }

  // 2. Parse the event
  const event = JSON.parse(body);

  switch (event.type) {
    case 'validation.enriched': {
      // Tier 2 signals have completed
      const { requestId, updatedScore, newSignals } = event.data;

      // 3. Update your user record
      await updateUserRiskScore(requestId, updatedScore);

      // 4. Take action if score dropped significantly
      if (updatedScore < 30) {
        await flagUserForReview(requestId);
      }
      break;
    }

    case 'validation.campaign_detected': {
      // This signup was linked to a fraud campaign
      const { requestId, campaignId, campaignSize } = event.data;
      await flagUserForReview(requestId);
      break;
    }
  }

  return NextResponse.json({ received: true });
}

async function updateUserRiskScore(
  requestId: string,
  score: number
) {
  // Look up user by BigShield requestId and update
}

async function flagUserForReview(requestId: string) {
  // Flag the user account for manual review
}

Step 9: Testing with Test Mode

BigShield API keys prefixed with ev_test_ operate in test mode. In test mode, certain email patterns trigger predictable responses:

// These emails return predictable scores in test mode:
// test-pass@example.com     -> score: 95, verdict: 'pass'
// test-warn@example.com     -> score: 55, verdict: 'warn'
// test-fail@example.com     -> score: 15, verdict: 'fail'
// test-timeout@example.com  -> simulates a timeout
// test-error@example.com    -> simulates a 500 error

// Use these in your integration tests:
import { describe, it, expect } from 'vitest';

describe('signup validation', () => {
  it('blocks low-score emails', async () => {
    const res = await fetch('/api/signup', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        email: 'test-fail@example.com',
        password: 'testpass123',
        name: 'Test User',
      }),
    });

    expect(res.status).toBe(422);
  });

  it('allows high-score emails', async () => {
    const res = await fetch('/api/signup', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        email: 'test-pass@example.com',
        password: 'testpass123',
        name: 'Test User',
      }),
    });

    expect(res.status).toBe(201);
  });
});

Step 10: Monitoring and Observability

In production, you will want to track validation performance:

// The SDK emits events you can hook into
bigshield.on('request', (event) => {
  metrics.histogram('bigshield.latency', event.latencyMs);
  metrics.increment('bigshield.requests', {
    verdict: event.result.verdict,
  });
});

bigshield.on('error', (event) => {
  metrics.increment('bigshield.errors', {
    code: event.error.code,
  });
});

What is Next

This guide covered the core integration. For more advanced topics, check out:

BigShield's free tier includes 1,000 validations per month, which is enough to build and test a complete integration. Grab an API key at bigshield.app and start protecting your signups today.

Ready to stop fake signups?

BigShield validates emails with 20+ signals in under 200ms. Start for free, no credit card required.

Get Started Free

Related Articles