TutorialsSend Bulk Personalized Emails

Send Bulk Personalized Emails

Send thousands of personalized emails efficiently with dynamic content and attachments for each recipient.


Overview

FormaMail’s bulk email feature allows you to send personalized emails to up to 1,000 recipients in a single API call. Each recipient can have unique:

  • Personalized content (variables)
  • Custom attachments (including dynamically generated PDFs)
  • Individual metadata for tracking

This tutorial covers:

  • Sending bulk emails with personalized variables
  • Using base variables shared across all recipients
  • Attaching personalized PDFs to each email
  • Monitoring batch progress
  • Handling failures and retries

Prerequisites

Before starting, make sure you have:

  • An active FormaMail account
  • An API key with bulk send permissions
  • An email template created and published
  • Basic knowledge of JavaScript/Python or API calls

How Bulk Sending Works

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                       Bulk Send Request                          β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Template ID: "monthly-report"                                   β”‚
β”‚  Base Variables: { companyName: "Acme Corp", month: "November" } β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Recipients:                                                     β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚
β”‚  β”‚ 1. john@example.com                                         β”‚β”‚
β”‚  β”‚    Variables: { firstName: "John", sales: 50000 }           β”‚β”‚
β”‚  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”‚
β”‚  β”‚ 2. jane@example.com                                         β”‚β”‚
β”‚  β”‚    Variables: { firstName: "Jane", sales: 75000 }           β”‚β”‚
β”‚  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”‚
β”‚  β”‚ 3. bob@example.com                                          β”‚β”‚
β”‚  β”‚    Variables: { firstName: "Bob", sales: 45000 }            β”‚β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
                              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     Personalized Emails                          β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Email 1: "Hi John, your November sales: $50,000 - Acme Corp"   β”‚
β”‚  Email 2: "Hi Jane, your November sales: $75,000 - Acme Corp"   β”‚
β”‚  Email 3: "Hi Bob, your November sales: $45,000 - Acme Corp"    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key concepts:

  • Base Variables: Shared across all recipients (e.g., company name, date)
  • Recipient Variables: Unique to each recipient (e.g., name, personalized data)
  • Variable Merging: Recipient variables override base variables if same key

Tutorial: Send Personalized Monthly Reports

Step 1: Prepare Your Template

First, ensure you have an email template with variables for personalization:

  1. Go to Templates β†’ Create Template β†’ Email Template

  2. Design a template with variables like:

    • {{firstName}} - Recipient’s name
    • {{month}} - Report month
    • {{salesAmount}} - Individual sales
    • {{topProduct}} - Best selling product
    • {{companyName}} - Your company name
  3. Save and Publish the template

  4. Copy the Template ID

Step 2: Prepare Your Recipient Data

Structure your data for bulk sending:

const recipients = [
  {
    email: 'john.doe@example.com',
    name: 'John Doe',
    variables: {
      firstName: 'John',
      salesAmount: '$50,000',
      topProduct: 'Product A',
      performanceRating: 'Excellent'
    }
  },
  {
    email: 'jane.smith@example.com',
    name: 'Jane Smith',
    variables: {
      firstName: 'Jane',
      salesAmount: '$75,000',
      topProduct: 'Product B',
      performanceRating: 'Outstanding'
    }
  },
  {
    email: 'bob.wilson@example.com',
    name: 'Bob Wilson',
    variables: {
      firstName: 'Bob',
      salesAmount: '$45,000',
      topProduct: 'Product C',
      performanceRating: 'Good'
    }
  }
  // ... up to 1,000 recipients
];

Step 3: Send Bulk Emails

const axios = require('axios');
 
const API_KEY = process.env.FORMAMAIL_API_KEY;
const API_URL = 'https://api.formamail.com/api';
 
async function sendBulkReports() {
  try {
    const response = await axios.post(
      `${API_URL}/emails/send/bulk`,
      {
        templateId: 'monthly-sales-report',
        // Variables shared by all recipients
        baseVariables: {
          companyName: 'Acme Corp',
          month: 'November 2025',
          reportDate: new Date().toLocaleDateString(),
          supportEmail: 'support@acme.com'
        },
        // Individual recipients with personalized data
        recipients: [
          {
            email: 'john.doe@example.com',
            name: 'John Doe',
            variables: {
              firstName: 'John',
              salesAmount: '$50,000',
              topProduct: 'Widget Pro',
              percentChange: '+15%'
            }
          },
          {
            email: 'jane.smith@example.com',
            name: 'Jane Smith',
            variables: {
              firstName: 'Jane',
              salesAmount: '$75,000',
              topProduct: 'Gadget Plus',
              percentChange: '+23%'
            }
          },
          {
            email: 'bob.wilson@example.com',
            name: 'Bob Wilson',
            variables: {
              firstName: 'Bob',
              salesAmount: '$45,000',
              topProduct: 'Device Max',
              percentChange: '+8%'
            }
          }
        ],
        // Optional settings
        batchName: 'November 2025 Sales Reports',
        tags: ['monthly-report', 'sales', 'automated'],
        priority: 'normal'
      },
      {
        headers: {
          'Authorization': `Bearer ${API_KEY}`,
          'Content-Type': 'application/json'
        }
      }
    );
 
    console.log('Bulk send initiated!');
    console.log('Batch ID:', response.data.batchId);
    console.log('Total emails:', response.data.totalEmails);
    console.log('Status:', response.data.status);
 
    return response.data;
  } catch (error) {
    console.error('Error:', error.response?.data || error.message);
    throw error;
  }
}
 
sendBulkReports();

Step 4: Check the Response

A successful bulk send returns:

{
  "batchId": "880e8400-e29b-41d4-a716-446655440000",
  "totalEmails": 3,
  "status": "queued",
  "message": "Successfully queued 3 emails for sending",
  "createdAt": "2025-11-19T12:00:00.000Z",
  "estimatedCompletionAt": "2025-11-19T12:01:00.000Z"
}

Step 5: Monitor Batch Progress

Track the status of your bulk send:

async function checkBatchStatus(batchId) {
  const response = await axios.get(
    `${API_URL}/emails/batch/${batchId}/status`,
    {
      headers: {
        'Authorization': `Bearer ${API_KEY}`
      }
    }
  );
 
  console.log('Batch Status:', response.data.status);
  console.log('Progress:', response.data.progress + '%');
  console.log('Sent:', response.data.sent);
  console.log('Delivered:', response.data.delivered);
  console.log('Failed:', response.data.failed);
 
  return response.data;
}
 
// Poll for status
const batchId = '880e8400-e29b-41d4-a716-446655440000';
const status = await checkBatchStatus(batchId);

Response:

{
  "batchId": "880e8400-e29b-41d4-a716-446655440000",
  "batchName": "November 2025 Sales Reports",
  "status": "completed",
  "totalEmails": 3,
  "sent": 3,
  "delivered": 3,
  "failed": 0,
  "queued": 0,
  "processing": 0,
  "progress": 100,
  "queuedAt": "2025-11-19T12:00:00.000Z",
  "startedAt": "2025-11-19T12:00:05.000Z",
  "completedAt": "2025-11-19T12:00:45.000Z"
}

Advanced Examples

Bulk Send with PDF Attachments

Send personalized invoices to each customer:

async function sendBulkInvoices(customers) {
  const response = await axios.post(
    `${API_URL}/emails/send/bulk`,
    {
      templateId: 'invoice-notification',
      baseVariables: {
        companyName: 'Acme Corp',
        supportEmail: 'billing@acme.com',
        paymentUrl: 'https://pay.acme.com'
      },
      // Attachment configuration
      attachments: [{
        filename: 'invoice-{{invoiceNumber}}.pdf',
        attachmentTemplateId: 'invoice-template',
        // Base variables for all PDFs
        baseVariables: {
          companyName: 'Acme Corp',
          companyAddress: '123 Business St',
          companyLogo: 'https://acme.com/logo.png'
        },
        // These fields will be pulled from each recipient's variables
        recipientVariableFields: [
          'invoiceNumber',
          'customerName',
          'customerAddress',
          'items',
          'subtotal',
          'tax',
          'total',
          'dueDate'
        ],
        outputFormats: ['pdf'],
        required: true
      }],
      recipients: customers.map(customer => ({
        email: customer.email,
        name: customer.name,
        variables: {
          firstName: customer.name.split(' ')[0],
          invoiceNumber: customer.invoiceNumber,
          customerName: customer.name,
          customerAddress: formatAddress(customer.address),
          items: customer.items,
          subtotal: formatCurrency(customer.subtotal),
          tax: formatCurrency(customer.tax),
          total: formatCurrency(customer.total),
          dueDate: formatDate(customer.dueDate)
        }
      })),
      batchName: 'Monthly Invoices - November 2025',
      tags: ['invoices', 'billing', 'automated']
    },
    {
      headers: {
        'Authorization': `Bearer ${API_KEY}`,
        'Content-Type': 'application/json'
      }
    }
  );
 
  return response.data;
}
 
// Example usage
const customers = [
  {
    email: 'customer1@example.com',
    name: 'John Doe',
    invoiceNumber: 'INV-2025-001',
    address: { street: '123 Main St', city: 'NYC', state: 'NY', zip: '10001' },
    items: [
      { description: 'Service A', quantity: 1, price: 500 },
      { description: 'Service B', quantity: 2, price: 250 }
    ],
    subtotal: 1000,
    tax: 85,
    total: 1085,
    dueDate: new Date('2025-12-15')
  },
  // ... more customers
];
 
sendBulkInvoices(customers);

Dry Run for Validation

Test your bulk send without actually sending emails:

async function validateBulkSend(templateId, recipients) {
  const response = await axios.post(
    `${API_URL}/emails/send/bulk`,
    {
      templateId,
      recipients,
      dryRun: true  // Validate without sending
    },
    {
      headers: {
        'Authorization': `Bearer ${API_KEY}`,
        'Content-Type': 'application/json'
      }
    }
  );
 
  console.log('Validation Result:');
  console.log('Valid:', response.data.valid);
  console.log('Total Emails:', response.data.totalEmails);
  console.log('Quota Cost:', response.data.totalQuotaCost);
  console.log('Valid Emails:', response.data.recipientValidation.validEmails);
  console.log('Invalid Emails:', response.data.recipientValidation.invalidEmails);
 
  if (response.data.warnings?.length > 0) {
    console.log('Warnings:', response.data.warnings);
  }
 
  return response.data;
}

Dry run response:

{
  "valid": true,
  "totalEmails": 100,
  "totalQuotaCost": 150,
  "costBreakdown": {
    "emails": 100,
    "attachments": 50
  },
  "quotaStatus": {
    "emailQuota": {
      "used": 850,
      "limit": 10000,
      "remaining": 9150
    }
  },
  "recipientValidation": {
    "totalRecipients": 100,
    "validEmails": 98,
    "invalidEmails": 2
  },
  "templateValidation": {
    "templateFound": true,
    "templateType": "email",
    "requiredVariables": ["firstName", "reportMonth"],
    "missingVariables": []
  },
  "warnings": [
    "2 recipients have invalid email addresses and will be skipped"
  ]
}

Always Dry Run First: For large batches (100+ emails), always run a dry run first to validate your data and check quota before sending.

Schedule Bulk Send for Later

Schedule bulk emails for a specific time:

const response = await axios.post(
  `${API_URL}/emails/send/bulk`,
  {
    templateId: 'weekly-newsletter',
    baseVariables: {
      weekOf: 'December 1-7, 2025',
      editionNumber: 47
    },
    recipients: subscribers,
    scheduledAt: '2025-12-01T09:00:00Z',  // Send at 9 AM UTC
    batchName: 'Weekly Newsletter #47'
  },
  { headers: { 'Authorization': `Bearer ${API_KEY}` } }
);
 
console.log('Scheduled for:', response.data.scheduledAt);

Cancel a Batch

Cancel a queued or processing batch:

async function cancelBatch(batchId) {
  const response = await axios.post(
    `${API_URL}/emails/batch/${batchId}/cancel`,
    {},
    {
      headers: {
        'Authorization': `Bearer ${API_KEY}`
      }
    }
  );
 
  console.log('Batch cancelled');
  console.log('Sent before cancel:', response.data.sent);
  console.log('Cancelled:', response.data.cancelled);
 
  return response.data;
}

Complete Example: Newsletter System

Here’s a production-ready newsletter system:

const axios = require('axios');
 
class NewsletterService {
  constructor(apiKey) {
    this.client = axios.create({
      baseURL: 'https://api.formamail.com/api',
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Content-Type': 'application/json'
      }
    });
  }
 
  async sendNewsletter(newsletter, subscribers) {
    // Validate first
    const validation = await this.validateBatch(newsletter, subscribers);
 
    if (!validation.valid) {
      throw new Error(`Validation failed: ${validation.warnings.join(', ')}`);
    }
 
    console.log(`Sending to ${validation.recipientValidation.validEmails} subscribers...`);
 
    // Send the batch
    const result = await this.client.post('/emails/send/bulk', {
      templateId: newsletter.templateId,
      baseVariables: {
        newsletterTitle: newsletter.title,
        editionNumber: newsletter.edition,
        publishDate: new Date().toLocaleDateString('en-US', {
          year: 'numeric',
          month: 'long',
          day: 'numeric'
        }),
        unsubscribeBaseUrl: 'https://yoursite.com/unsubscribe'
      },
      recipients: subscribers.map(sub => ({
        email: sub.email,
        name: sub.name || sub.email,
        variables: {
          firstName: sub.firstName || 'Subscriber',
          subscriberId: sub.id,
          unsubscribeUrl: `https://yoursite.com/unsubscribe?id=${sub.id}&token=${sub.unsubscribeToken}`,
          // Personalized content based on preferences
          showTechNews: sub.preferences?.includes('tech'),
          showBusinessNews: sub.preferences?.includes('business'),
          showLifestyleNews: sub.preferences?.includes('lifestyle')
        }
      })),
      batchName: `Newsletter #${newsletter.edition} - ${newsletter.title}`,
      tags: ['newsletter', `edition-${newsletter.edition}`],
      priority: 'normal'
    });
 
    console.log(`Batch ${result.data.batchId} queued with ${result.data.totalEmails} emails`);
 
    // Monitor progress
    return this.monitorBatch(result.data.batchId);
  }
 
  async validateBatch(newsletter, subscribers) {
    const response = await this.client.post('/emails/send/bulk', {
      templateId: newsletter.templateId,
      baseVariables: { newsletterTitle: newsletter.title },
      recipients: subscribers.map(sub => ({
        email: sub.email,
        variables: { firstName: sub.firstName || 'Subscriber' }
      })),
      dryRun: true
    });
    return response.data;
  }
 
  async monitorBatch(batchId, intervalMs = 5000) {
    return new Promise((resolve, reject) => {
      const checkStatus = async () => {
        try {
          const response = await this.client.get(`/emails/batch/${batchId}/status`);
          const status = response.data;
 
          console.log(`Progress: ${status.progress}% | Sent: ${status.sent} | Delivered: ${status.delivered} | Failed: ${status.failed}`);
 
          if (status.status === 'completed') {
            console.log('Batch completed!');
            resolve(status);
          } else if (status.status === 'failed' || status.status === 'cancelled') {
            reject(new Error(`Batch ${status.status}`));
          } else {
            setTimeout(checkStatus, intervalMs);
          }
        } catch (error) {
          reject(error);
        }
      };
 
      checkStatus();
    });
  }
 
  async getFailedEmails(batchId) {
    const response = await this.client.get(`/emails`, {
      params: {
        batchId,
        status: 'failed',
        limit: 100
      }
    });
    return response.data.data;
  }
 
  async retryFailedEmails(batchId) {
    const failed = await this.getFailedEmails(batchId);
 
    console.log(`Retrying ${failed.length} failed emails...`);
 
    for (const email of failed) {
      try {
        await this.client.post(`/emails/${email.id}/retry`);
        console.log(`Retried: ${email.id}`);
      } catch (error) {
        console.error(`Failed to retry ${email.id}:`, error.message);
      }
    }
  }
}
 
// Usage
const newsletterService = new NewsletterService(process.env.FORMAMAIL_API_KEY);
 
const newsletter = {
  templateId: 'weekly-newsletter',
  title: 'This Week in Tech',
  edition: 47
};
 
const subscribers = [
  { id: 'sub-1', email: 'user1@example.com', firstName: 'Alice', preferences: ['tech', 'business'] },
  { id: 'sub-2', email: 'user2@example.com', firstName: 'Bob', preferences: ['tech'] },
  { id: 'sub-3', email: 'user3@example.com', firstName: 'Carol', preferences: ['lifestyle'] },
  // ... more subscribers
];
 
newsletterService.sendNewsletter(newsletter, subscribers)
  .then(result => {
    console.log('Newsletter sent successfully!');
    console.log(`Delivered: ${result.delivered}/${result.totalEmails}`);
  })
  .catch(error => {
    console.error('Newsletter failed:', error.message);
  });

Data Preparation Tips

From CSV/Excel

const Papa = require('papaparse');
const fs = require('fs');
 
function loadRecipientsFromCSV(filePath) {
  const csvData = fs.readFileSync(filePath, 'utf8');
  const { data } = Papa.parse(csvData, { header: true });
 
  return data
    .filter(row => row.email && row.email.includes('@'))
    .map(row => ({
      email: row.email.trim().toLowerCase(),
      name: row.name || row.first_name,
      variables: {
        firstName: row.first_name || row.name?.split(' ')[0] || 'Customer',
        lastName: row.last_name || '',
        company: row.company || '',
        customField1: row.custom_1 || '',
        customField2: row.custom_2 || ''
      }
    }));
}
 
const recipients = loadRecipientsFromCSV('./subscribers.csv');
console.log(`Loaded ${recipients.length} recipients`);

From Database

const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
 
async function loadActiveSubscribers() {
  const subscribers = await prisma.subscriber.findMany({
    where: {
      status: 'active',
      emailVerified: true,
      unsubscribed: false
    },
    include: {
      preferences: true
    }
  });
 
  return subscribers.map(sub => ({
    email: sub.email,
    name: `${sub.firstName} ${sub.lastName}`,
    variables: {
      firstName: sub.firstName,
      lastName: sub.lastName,
      subscribedDate: sub.createdAt.toLocaleDateString(),
      preferredCategories: sub.preferences.map(p => p.category)
    }
  }));
}

Chunking Large Lists

For lists over 1,000 recipients, chunk them:

function chunkArray(array, size) {
  const chunks = [];
  for (let i = 0; i < array.length; i += size) {
    chunks.push(array.slice(i, i + size));
  }
  return chunks;
}
 
async function sendToLargeList(templateId, allRecipients, baseVariables) {
  const chunks = chunkArray(allRecipients, 1000);
  const results = [];
 
  console.log(`Sending to ${allRecipients.length} recipients in ${chunks.length} batches...`);
 
  for (let i = 0; i < chunks.length; i++) {
    console.log(`Sending batch ${i + 1}/${chunks.length}...`);
 
    const response = await axios.post(
      `${API_URL}/emails/send/bulk`,
      {
        templateId,
        baseVariables,
        recipients: chunks[i],
        batchName: `Bulk Send - Batch ${i + 1}/${chunks.length}`
      },
      { headers: { 'Authorization': `Bearer ${API_KEY}` } }
    );
 
    results.push(response.data);
 
    // Wait between batches to avoid rate limits
    if (i < chunks.length - 1) {
      await new Promise(resolve => setTimeout(resolve, 1000));
    }
  }
 
  return results;
}

Best Practices

Before Sending

  1. Always dry run first - Validate data and check quota
  2. Verify email addresses - Remove invalid/bounced emails
  3. Check template variables - Ensure all required variables are provided
  4. Test with small batch - Send to 5-10 test addresses first
  5. Check suppression list - Don’t send to unsubscribed users

During Sending

  1. Monitor progress - Watch the batch status
  2. Handle failures gracefully - Log and retry if needed
  3. Respect rate limits - Don’t overwhelm your quota
  4. Use meaningful batch names - For easy tracking

After Sending

  1. Check delivery stats - Review sent vs delivered vs failed
  2. Retry failed emails - Investigate and retry failures
  3. Monitor bounces - Add hard bounces to suppression list
  4. Track engagement - Review open and click rates

Troubleshooting

Batch Stuck in β€œProcessing”

Solutions:

  1. βœ… Wait for completion - large batches take time
  2. βœ… Check batch status API for progress
  3. βœ… Contact support if stuck for over 30 minutes

High Failure Rate

Solutions:

  1. βœ… Check email addresses for validity
  2. βœ… Verify domain is not blocklisted
  3. βœ… Check suppression list for recipients
  4. βœ… Review individual failure reasons in email logs

Rate Limit Errors

Solutions:

  1. βœ… Reduce batch size
  2. βœ… Add delays between batches
  3. βœ… Upgrade plan for higher limits
  4. βœ… Use scheduling to spread sends over time

Missing Variables

Solutions:

  1. βœ… Ensure all recipients have required variables
  2. βœ… Use base variables for common data
  3. βœ… Check variable names match template exactly
  4. βœ… Run dry run to catch missing variables

Next Steps

Now that you can send bulk emails: