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/sdkInitialize 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:
- From Zero to Hero: Implementing BigShield for a full production deployment walkthrough
- Architecture of a Fraud Detection Platform for a deep dive into how BigShield scores work under the hood
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.