Integrate Email Insights into Your Node.js Application: A Developer's Guide

10 min readOpportify Team

This guide shows how to integrate Email Insights into a Node.js application using the official SDK. You will learn how to run single validations, launch batch jobs with polling, handle errors gracefully, and build production-ready integrations with proper configuration and security.

If you are new to the product, start with the overview at /products/email-insights and the bulk features at /email-insights/bulk-email-validation. For security and compliance details, see /security and /legal/gdpr-notice.

SDK Resources:

Requirements

  • Node.js 16.x or later
  • npm or yarn
  • A valid API key (see below for instructions)

Get Your API Key

  1. Log into your Opportify dashboard
  2. Navigate to Access Control → API Keys
  3. Generate a new API key or copy your existing key
  4. Store it securely (environment variable, secrets manager—never commit to source control)

Install the SDK

npm install @opportify/sdk-nodejs

Quick start: single email analysis

import { EmailInsights } from '@opportify/sdk-nodejs';

const emailInsights = new EmailInsights({
  version: '1.0',
  apiKey: process.env.OPPORTIFY_API_KEY // Load securely per your policy
});

async function analyzeEmail() {
  try {
    const result = await emailInsights.analyze({
      email: "user@example.com",
      enableAutoCorrection: true,
      enableAI: true
    });

    console.log({
      isDeliverable: result.isDeliverable,    // "yes" | "no" | "unknown"
      isCatchAll: result.isCatchAll,
      isMailboxFull: result.isMailboxFull,
      isReachable: result.isReachable,
      normalizedScore: result.normalizedScore,
      riskLevel: result.riskLevel || result.riskReport?.level,
      provider: result.provider,
      domain: result.domain
    });
  } catch (error) {
    console.error('Email analysis failed:', error.message);
    throw error;
  }
}

analyzeEmail();

When to enable AI and autocorrection

  • enableAutoCorrection: true helps correct common user typos in form inputs (e.g., "gmial.com" → "gmail.com").
  • enableAI: true adds domain enrichment and risk signals that improve decisions for gating, segmentation, and fraud protection.

Batch analysis with async/await

Use batch jobs for large lists. A job returns a jobId that you can poll until results are ready.

import { EmailInsights } from '@opportify/sdk-nodejs';

const emailInsights = new EmailInsights({
  version: '1.0',
  apiKey: process.env.OPPORTIFY_API_KEY
});

async function processBatch() {
  const params = {
    emails: [
      "user1@company.com",
      "user2@domain.org",
      "test@example.com"
    ],
    name: "User Signup Validation",
    enableAutoCorrection: true,
    enableAI: true
  };

  try {
    // Start batch job
    const batch = await emailInsights.batchAnalyze(params);
    console.log('Batch job started:', batch.jobId);

    // Poll for completion
    const result = await waitForCompletion(batch.jobId);

    console.log('Download URLs:');
    console.log('CSV:', result.downloadUrls?.csv);
    console.log('JSON:', result.downloadUrls?.json);
  } catch (error) {
    console.error('Batch processing failed:', error.message);
    throw error;
  }
}

async function waitForCompletion(jobId, maxWaitMs = 600000) {
  const startTime = Date.now();
  let delay = 2000; // Start with 2 second delay

  while (true) {
    const status = await emailInsights.getBatchStatus(jobId);
    const state = status.status;
    const progress = status.progress || 0;

    console.log(`Status: ${state} - Progress: ${progress}%`);

    if (state === 'COMPLETED') {
      return status;
    }

    if (state === 'ERROR') {
      const description = status.statusDescription || 'Unknown error';
      throw new Error(`Batch job failed: ${description}`);
    }

    if (Date.now() - startTime > maxWaitMs) {
      throw new Error('Batch job timeout - did not complete within time limit');
    }

    // Exponential backoff with cap at 30 seconds
    await sleep(Math.min(delay, 30000));
    delay = Math.min(delay * 1.6, 30000);
  }
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

processBatch();

Batch with file upload

import { EmailInsights } from '@opportify/sdk-nodejs';
import fs from 'fs';

const emailInsights = new EmailInsights({
  version: '1.0',
  apiKey: process.env.OPPORTIFY_API_KEY
});

async function uploadAndProcess() {
  try {
    const batch = await emailInsights.batchAnalyzeFile('./emails.csv', {
      name: 'Monthly Email List Cleanup',
      enableAI: true,
      enableAutoCorrection: true
    });

    console.log('File uploaded, job ID:', batch.jobId);

    // Poll for results
    const result = await waitForCompletion(batch.jobId);
    console.log('Processing complete!');
  } catch (error) {
    console.error('File upload failed:', error.message);
    throw error;
  }
}

uploadAndProcess();

Practical tips for batches

  • Split large uploads into smaller jobs (5,000-10,000 emails per job) for faster completion and easier retries.
  • Persist jobId in your database to track results even if your application restarts.
  • Use exponential backoff when polling to reduce unnecessary API calls.
  • Consider implementing webhook listeners for batch completion notifications (if available).

Handling errors safely

Always wrap API calls in try-catch blocks to handle exceptions cleanly.

import { EmailInsights } from '@opportify/sdk-nodejs';

const emailInsights = new EmailInsights({
  version: '1.0',
  apiKey: process.env.OPPORTIFY_API_KEY
});

async function safeAnalyze(email) {
  try {
    return await emailInsights.analyze({
      email,
      enableAI: true,
      enableAutoCorrection: true
    });
  } catch (error) {
    if (error.response) {
      // API returned an error response
      console.error('API error:', {
        status: error.response.status,
        message: error.response.data?.message || error.message
      });

      // Handle specific error codes
      if (error.response.status === 403) {
        throw new Error('API key invalid or feature not available on your plan');
      }
      if (error.response.status === 429) {
        throw new Error('Rate limit exceeded - implement backoff and retry');
      }
    } else if (error.request) {
      // Request was made but no response received
      console.error('Network error - no response from server');
      throw new Error('Connection to validation service failed');
    } else {
      // Something else happened
      console.error('Request setup error:', error.message);
    }
    throw error;
  }
}

Interpreting results and taking action

Deliverability is tri-state isDeliverable returns a string: yes, no, or unknown. Combine it with isMailboxFull, isCatchAll, and isReachable to make informed decisions.

Risk scoring model Email Insights provides a normalizedScore on a 200 to 1000 scale with corresponding risk levels:

Risk Level Score Range Guidance
Lowest ≤ 300 Safe to include in primary campaigns
Low 300-400 Generally safe; monitor engagement
Medium 400-600 Consider revalidation or secondary flows
High 600-800 Exclude from high-value campaigns; verify
Highest > 800 Reject; investigate for fraud or traps

Decision helper (tri-state aware)

function classifyContact(result) {
  const deliverable = (result.isDeliverable || '').toLowerCase();
  const riskLevel = (result.riskLevel || result.riskReport?.level || '').toLowerCase();
  const isCatchAll = Boolean(result.isCatchAll);
  const isMailboxFull = Boolean(result.isMailboxFull);
  const isReachable = Boolean(result.isReachable);
  const signals = result.signals || {};
  const isDisposable = Boolean(signals.isDisposableDomain);

  // Immediate rejection criteria
  if (isDisposable || riskLevel === 'highest') {
    return 'reject';
  }

  // Handle non-deliverable emails
  if (deliverable === 'no') {
    return isMailboxFull ? 'revalidate' : 'reject';
  }

  // Handle unknown deliverability
  if (deliverable === 'unknown') {
    if (riskLevel === 'high') return 'review';
    if (isCatchAll || !isReachable) return 'revalidate';
    return 'review';
  }

  // Handle deliverable emails (yes)
  if (riskLevel === 'high') return 'review';
  if (riskLevel === 'medium' || isCatchAll) return 'revalidate';

  return 'accept';
}

// Usage example
const result = await emailInsights.analyze({ email: 'user@example.com' });
const action = classifyContact(result);
console.log(`Action for ${result.emailAddress}: ${action}`);

Production hardening

Configuration and secrets management

  • Store your OPPORTIFY_API_KEY securely using environment variables or your preferred secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.).
  • Never hard-code the API key or commit it to version control.
  • Avoid logging the full API key or payloads containing personal data.
  • Use .env files locally with proper .gitignore configuration.

Access Control Lists (ACLs) Email Insights supports API Key Access Control Lists that restrict usage by IP address or range. Configure these in your account dashboard to ensure only approved systems can access the API.

Caching for performance

Implement caching to reduce API calls and improve response times:

import { EmailInsights } from '@opportify/sdk-nodejs';

class CachedEmailValidator {
  constructor(apiKey) {
    this.client = new EmailInsights({
      version: '1.0',
      apiKey
    });
    this.cache = new Map();
    this.cacheDuration = 3600000; // 1 hour in ms
    this.maxCacheSize = 10000; // Limit cache size to prevent unbounded growth
  }

  async validate(email) {
    // Check cache first
    const cached = this.getFromCache(email);
    if (cached) return cached;

    // Fetch from API
    const result = await this.client.analyze({
      email,
      enableAI: true,
      enableAutoCorrection: true
    });

    // Save to cache
    this.saveToCache(email, result);
    return result;
  }

  getFromCache(email) {
    const cached = this.cache.get(email);
    if (!cached) return null;

    const age = Date.now() - cached.timestamp;
    if (age > this.cacheDuration) {
      this.cache.delete(email);
      return null;
    }

    return cached.result;
  }

  saveToCache(email, result) {
    // Prevent unbounded cache growth - use LRU-like behavior
    if (this.cache.size >= this.maxCacheSize) {
      // Remove oldest entries (first 20% of cache)
      const entriesToRemove = Math.floor(this.maxCacheSize * 0.2);
      const iterator = this.cache.keys();
      for (let i = 0; i < entriesToRemove; i++) {
        const key = iterator.next().value;
        this.cache.delete(key);
      }
    }

    this.cache.set(email, {
      result,
      timestamp: Date.now()
    });
  }

  isAcceptable(result, options = {}) {
    const {
      maxRiskScore = 600,
      allowDisposable = false,
      allowRole = true,
      requireAuth = false
    } = options;

    if (result.isDeliverable !== 'yes') return false;
    if (result.riskReport?.score > maxRiskScore) return false;
    if (!allowDisposable && result.emailType === 'disposable') return false;
    if (!allowRole && result.addressSignals?.isRoleAddress) return false;
    if (requireAuth && (!result.emailDNS?.spfValid || !result.emailDNS?.dkimConfigured)) return false;

    return true;
  }
}

// Usage in Express application
import express from 'express';

const app = express();

// Configure body parsing middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

const validator = new CachedEmailValidator(process.env.OPPORTIFY_API_KEY);

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

  try {
    const validation = await validator.validate(email);

    if (!validator.isAcceptable(validation, {
      maxRiskScore: 600,
      allowDisposable: false,
      allowRole: false
    })) {
      return res.status(400).json({
        error: 'Invalid email address',
        details: {
          riskScore: validation.riskReport?.score,
          deliverable: validation.isDeliverable,
          emailType: validation.emailType
        }
      });
    }

    // Proceed with registration
    const user = await createUser({ email, name });
    res.json({ success: true, userId: user.id });

  } catch (error) {
    console.error('Validation error:', error);

    // Graceful degradation - don't block registration on validation failure
    res.json({
      success: true,
      warning: 'Email validation temporarily unavailable'
    });
  }
});

Timeouts and retries

async function analyzeWithRetry(email, maxRetries = 3) {
  let lastError;

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await emailInsights.analyze({
        email,
        enableAI: true,
        enableAutoCorrection: true
      });
    } catch (error) {
      lastError = error;

      // Don't retry on client errors (4xx)
      if (error.response?.status >= 400 && error.response?.status < 500) {
        throw error;
      }

      // Exponential backoff for retries
      if (attempt < maxRetries) {
        const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
        await sleep(delay);
      }
    }
  }

  throw lastError;
}

Observability and monitoring

import { EmailInsights } from '@opportify/sdk-nodejs';

class MonitoredEmailValidator {
  constructor(apiKey, logger) {
    this.client = new EmailInsights({
      version: '1.0',
      apiKey
    });
    this.logger = logger;
    this.metrics = {
      totalCalls: 0,
      errors: 0,
      cacheHits: 0
    };
  }

  async validate(email) {
    const startTime = Date.now();
    this.metrics.totalCalls++;

    try {
      const result = await this.client.analyze({
        email,
        enableAI: true,
        enableAutoCorrection: true
      });

      const duration = Date.now() - startTime;
      this.logger.info('Email validation successful', {
        email: email.replace(/(?<=.{2}).(?=.*@)/g, '*'), // Mask email
        duration,
        riskLevel: result.riskReport?.level,
        isDeliverable: result.isDeliverable
      });

      return result;
    } catch (error) {
      this.metrics.errors++;
      const duration = Date.now() - startTime;

      this.logger.error('Email validation failed', {
        email: email.replace(/(?<=.{2}).(?=.*@)/g, '*'),
        duration,
        error: error.message,
        statusCode: error.response?.status
      });

      throw error;
    }
  }

  getMetrics() {
    return {
      ...this.metrics,
      errorRate: this.metrics.totalCalls > 0
        ? (this.metrics.errors / this.metrics.totalCalls * 100).toFixed(2) + '%'
        : '0%'
    };
  }
}

Local testing checklist

  • Use synthetic emails covering edge cases: disposable domains, catch-all domains, corporate domains, common typos.
  • Test routing logic for each riskLevel and isDeliverable value combination.
  • Verify graceful handling of invalid API keys, network timeouts, and rate limits.
  • Test batch processing with both small and large datasets.
  • Validate that caching works correctly and respects TTL.

Where to go next

SDK & Documentation:

Product & Features:

Security & Compliance:

Reference: SDK methods

  • new EmailInsights({ version, apiKey })
  • emailInsights.analyze(params)
  • emailInsights.batchAnalyze(params)
  • emailInsights.batchAnalyzeFile(filePath, params)
  • emailInsights.getBatchStatus(jobId)