Skip to content

Best Practices

Follow these best practices to maximize the effectiveness of your email validation integration.

Validation results are stable—cache them to avoid redundant API calls.

const cache = new Map<string, EmailValidationResult>();
const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
async function validateWithCache(email: string) {
const key = email.toLowerCase().trim();
const cached = cache.get(key);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.result;
}
const result = await client.validate(email);
cache.set(key, { result, timestamp: Date.now() });
return result;
}

For form validation where speed matters, use quick mode:

// Real-time form validation (< 100ms)
const quick = await client.validate(email, { quickMode: true });
// Background verification (full checks)
const full = await client.validate(email);

Quick mode skips:

  • SMTP verification
  • Catch-all detection
  • Some reputation checks

Remove duplicates before batch validation to save quota:

function deduplicateEmails(emails: string[]): string[] {
const seen = new Set<string>();
return emails.filter(email => {
const normalized = email.toLowerCase().trim();
if (seen.has(normalized)) return false;
seen.add(normalized);
return true;
});
}
const unique = deduplicateEmails(rawEmails);
const results = await client.validateBatch(unique);
async function processLargeList(emails: string[]) {
const BATCH_SIZE = 100;
const results = [];
for (let i = 0; i < emails.length; i += BATCH_SIZE) {
const batch = emails.slice(i, i + BATCH_SIZE);
const response = await client.validateBatch(batch);
results.push(...response.results);
}
return results;
}

import { RateLimitError } from '@spamidate/sdk';
async function validateWithRetry(email: string) {
try {
return await client.validate(email);
} catch (error) {
if (error instanceof RateLimitError) {
await sleep(error.retryAfter * 1000);
return client.validate(email);
}
throw error;
}
}

For transient errors, use exponential backoff:

async function validateWithBackoff(email: string, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await client.validate(email);
} catch (error) {
if (error.isRetryable && attempt < maxRetries - 1) {
const delay = Math.pow(2, attempt) * 1000;
await sleep(delay);
continue;
}
throw error;
}
}
}

Check quota before large operations:

async function validateListSafely(emails: string[]) {
const usage = await client.getUsage();
if (emails.length > usage.quota.remaining) {
throw new Error(`Insufficient quota: need ${emails.length}, have ${usage.quota.remaining}`);
}
if (usage.quota.percentage > 80) {
console.warn(`Warning: ${usage.quota.percentage}% quota used`);
}
return client.validateBatch(emails);
}

// WRONG - API key in client-side code
const client = new Spamidate({ apiKey: 'spm_live_...' });
// RIGHT - API key in server-side environment variable
const client = new Spamidate({ apiKey: process.env.SPAMIDATE_API_KEY });

Create a server-side endpoint:

// pages/api/validate-email.ts (Next.js example)
export async function POST(req: Request) {
const { email } = await req.json();
const client = new Spamidate({ apiKey: process.env.SPAMIDATE_API_KEY });
const result = await client.validate(email);
// Return only necessary fields
return Response.json({
isValid: result.isValid,
score: result.score,
suggestion: result.checks.typoSuggestion?.metadata?.suggestion,
});
}
  1. Generate a new API key in the dashboard
  2. Update your environment variables
  3. Deploy the changes
  4. Disable the old key after confirming the new one works

// React example with debouncing
function EmailInput({ onValidated }) {
const [email, setEmail] = useState('');
const [status, setStatus] = useState<'idle' | 'validating' | 'valid' | 'invalid'>('idle');
const validateEmail = useMemo(
() => debounce(async (value: string) => {
if (!value) return;
setStatus('validating');
const response = await fetch('/api/validate-email', {
method: 'POST',
body: JSON.stringify({ email: value }),
});
const result = await response.json();
setStatus(result.isValid ? 'valid' : 'invalid');
onValidated(result);
}, 500),
[]
);
return (
<input
type="email"
value={email}
onChange={(e) => {
setEmail(e.target.value);
validateEmail(e.target.value);
}}
/>
);
}
if (result.checks.typoSuggestion?.metadata?.suggestion) {
return (
<div>
Did you mean{' '}
<button onClick={() => setEmail(result.checks.typoSuggestion.metadata.suggestion)}>
{result.checks.typoSuggestion.metadata.suggestion}
</button>
?
</div>
);
}
function getValidationMessage(result: EmailValidationResult) {
switch (result.severity) {
case 'valid':
return { type: 'success', message: 'Email verified' };
case 'warning':
return { type: 'warning', message: result.recommendations[0] };
case 'invalid':
return { type: 'error', message: result.recommendations[0] || 'Invalid email' };
}
}

function segmentByQuality(results: EmailValidationResult[]) {
return {
excellent: results.filter(r => r.score >= 90),
good: results.filter(r => r.score >= 70 && r.score < 90),
risky: results.filter(r => r.score >= 40 && r.score < 70),
invalid: results.filter(r => r.score < 40),
};
}
function exportCleanList(results: EmailValidationResult[], minScore = 70) {
return results
.filter(r => r.score >= minScore)
.filter(r => r.checks.disposable?.passed !== false)
.map(r => r.email);
}
function generateReport(results: BatchValidationResponse) {
const checks = {
disposable: results.results.filter(r => r.checks.disposable?.passed === false).length,
roleBased: results.results.filter(r => r.checks.roleBased?.passed === false).length,
typos: results.results.filter(r => r.checks.typoSuggestion?.metadata?.hasTypo).length,
};
return {
total: results.total,
valid: results.valid,
invalid: results.invalid,
warning: results.warning,
validRate: ((results.valid / results.total) * 100).toFixed(1) + '%',
issues: checks,
};
}

Always verify webhook signatures:

function verifyWebhook(payload: string, signature: string, timestamp: string) {
const expected = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(`${timestamp}.${payload}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}

Webhooks may be delivered multiple times:

const processedEvents = new Set<string>();
function handleWebhook(event: WebhookEvent) {
if (processedEvents.has(event.id)) {
return; // Already processed
}
processEvents.add(event.id);
// Process event...
}

Acknowledge webhooks immediately, process asynchronously:

app.post('/webhooks', (req, res) => {
res.status(200).send('OK');
processWebhookAsync(req.body);
});

async function validateAndTrack(email: string) {
const start = Date.now();
const result = await client.validate(email);
const duration = Date.now() - start;
// Log metrics
metrics.track('email_validation', {
duration,
score: result.score,
severity: result.severity,
isDisposable: result.checks.disposable?.passed === false,
});
return result;
}
async function checkQuotaAndAlert() {
const usage = await client.getUsage();
if (usage.quota.percentage >= 80) {
await sendSlackAlert(`Spamidate quota at ${usage.quota.percentage}%`);
}
if (usage.recentErrors.rateLimited > 10) {
await sendSlackAlert('High rate limit errors detected');
}
}
// Run hourly
setInterval(checkQuotaAndAlert, 60 * 60 * 1000);

// WRONG - too many API calls
input.addEventListener('input', () => validate(input.value));
// RIGHT - debounce
input.addEventListener('input', debounce(() => validate(input.value), 500));
// WRONG - no error handling
const result = await client.validate(email);
// RIGHT - handle errors
try {
const result = await client.validate(email);
} catch (error) {
if (error instanceof RateLimitError) {
// Handle rate limit
}
// Handle other errors
}
// WRONG - magic numbers
if (result.score > 65) { ... }
// RIGHT - configurable thresholds
const SCORE_THRESHOLDS = {
signup: 70,
marketing: 80,
transactional: 50,
};
if (result.score >= SCORE_THRESHOLDS[context]) { ... }
// WRONG - only check isValid
if (result.isValid) { proceed(); }
// RIGHT - consider severity and checks
if (result.isValid && result.severity !== 'warning') {
proceed();
} else if (result.checks.typoSuggestion?.metadata?.hasTypo) {
showSuggestion();
}