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:
-
Go to Templates β Create Template β Email Template
-
Design a template with variables like:
{{firstName}}- Recipientβs name{{month}}- Report month{{salesAmount}}- Individual sales{{topProduct}}- Best selling product{{companyName}}- Your company name
-
Save and Publish the template
-
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
- Always dry run first - Validate data and check quota
- Verify email addresses - Remove invalid/bounced emails
- Check template variables - Ensure all required variables are provided
- Test with small batch - Send to 5-10 test addresses first
- Check suppression list - Donβt send to unsubscribed users
During Sending
- Monitor progress - Watch the batch status
- Handle failures gracefully - Log and retry if needed
- Respect rate limits - Donβt overwhelm your quota
- Use meaningful batch names - For easy tracking
After Sending
- Check delivery stats - Review sent vs delivered vs failed
- Retry failed emails - Investigate and retry failures
- Monitor bounces - Add hard bounces to suppression list
- Track engagement - Review open and click rates
Troubleshooting
Batch Stuck in βProcessingβ
Solutions:
- β Wait for completion - large batches take time
- β Check batch status API for progress
- β Contact support if stuck for over 30 minutes
High Failure Rate
Solutions:
- β Check email addresses for validity
- β Verify domain is not blocklisted
- β Check suppression list for recipients
- β Review individual failure reasons in email logs
Rate Limit Errors
Solutions:
- β Reduce batch size
- β Add delays between batches
- β Upgrade plan for higher limits
- β Use scheduling to spread sends over time
Missing Variables
Solutions:
- β Ensure all recipients have required variables
- β Use base variables for common data
- β Check variable names match template exactly
- β Run dry run to catch missing variables
Next Steps
Now that you can send bulk emails:
- Send Emails with Charts - Add data visualizations
- Generate PDF Invoices - Attach personalized documents
- Webhooks - Get delivery notifications
- Analytics - Track campaign performance