Best Practices
Security, performance, and optimization tips for using the FormaMail API.
Overview
Follow these best practices to build secure, reliable, and performant email integrations with FormaMail. These guidelines cover API key security, error handling, performance optimization, and email delivery best practices.
Important: These best practices apply specifically to API usage. For template design and dashboard features, see the User Guide.
API Key Security
1. Never Hardcode API Keys
❌ Bad:
const apiKey = 'fm_sk_abc123xyz456...'; // Hardcoded - dangerous!
fetch('https://api.formamail.com/api/emails/send', {
headers: { 'Authorization': `Bearer ${apiKey}` }
});✅ Good:
const apiKey = process.env.FORMAMAIL_API_KEY; // From environment variable
fetch('https://api.formamail.com/api/emails/send', {
headers: { 'Authorization': `Bearer ${apiKey}` }
});2. Use Environment Variables
Store API keys in environment variables, never in code:
.env file:
FORMAMAIL_API_KEY=fm_sk_abc123xyz456....gitignore:
.env
.env.local
.env.productionNode.js:
require('dotenv').config();
const apiKey = process.env.FORMAMAIL_API_KEY;Python:
import os
api_key = os.environ.get('FORMAMAIL_API_KEY')PHP:
$apiKey = getenv('FORMAMAIL_API_KEY');3. Use Separate Keys for Different Environments
Create separate API keys for development, staging, and production:
DEV_FORMAMAIL_API_KEY=fm_sk_dev123...
STAGING_FORMAMAIL_API_KEY=fm_sk_staging456...
PROD_FORMAMAIL_API_KEY=fm_sk_prod789...Benefits:
- Isolate test traffic from production
- Revoke dev keys without affecting production
- Monitor usage separately
4. Rotate Keys Regularly
Rotate API keys every 90 days:
// Store key creation date in your database
const KEY_AGE_LIMIT_DAYS = 90;
function checkKeyAge(createdAt) {
const ageInDays = (Date.now() - createdAt) / (1000 * 60 * 60 * 24);
if (ageInDays > KEY_AGE_LIMIT_DAYS) {
console.warn('API key is old! Rotate immediately.');
// Alert admin, create new key in dashboard
}
}5. Never Log API Keys
❌ Bad:
console.log('Sending with key:', apiKey); // Logs sensitive data!
logger.debug({ apiKey, templateId }); // Exposes key in logs!✅ Good:
console.log('Sending email...'); // No sensitive data
logger.debug({ templateId, recipientCount }); // Safe information only
// If you must log for debugging, redact the key
const redactedKey = apiKey.substring(0, 10) + '***';
console.log('Using key:', redactedKey); // Safe6. Restrict API Key Permissions
When creating API keys in the dashboard:
- ✅ Only grant necessary permissions (email sending only)
- ✅ Set expiration dates
- ✅ Name keys descriptively (“Production Server”, “Staging API”)
- ❌ Don’t create overly permissive keys
Error Handling
1. Always Check Response Status
❌ Bad:
const response = await fetch(url, options);
const data = await response.json(); // Might fail if status is 4xx/5xx✅ Good:
const response = await fetch(url, options);
if (!response.ok) {
const error = await response.json();
throw new Error(`API Error: ${error.code} - ${error.message}`);
}
const data = await response.json();2. Implement Retry Logic with Exponential Backoff
✅ Production-ready retry logic:
async function sendEmailWithRetry(emailData, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch('https://api.formamail.com/api/emails/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.FORMAMAIL_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(emailData)
});
// Handle rate limiting
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After')) || 60;
console.log(`Rate limited. Retrying in ${retryAfter}s...`);
await sleep(retryAfter * 1000);
continue;
}
// Handle server errors (5xx) - retry
if (response.status >= 500) {
const delay = Math.min(2 ** attempt * 1000, 30000); // Max 30s
console.log(`Server error. Retrying in ${delay}ms...`);
await sleep(delay);
continue;
}
// Handle client errors (4xx) - don't retry
if (response.status >= 400) {
const error = await response.json();
throw new Error(`${error.code}: ${error.message}`);
}
// Success
return await response.json();
} catch (error) {
// Last attempt - throw error
if (attempt === maxRetries - 1) {
throw error;
}
// Network error - retry with exponential backoff
const delay = Math.min(2 ** attempt * 1000, 30000);
console.log(`Attempt ${attempt + 1} failed. Retrying in ${delay}ms...`);
await sleep(delay);
}
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}3. Handle Specific Error Codes
async function handleApiError(error) {
switch (error.code) {
case 'ERR_AUTH_001':
// Invalid API key - check key, don't retry
console.error('Invalid API key. Check FORMAMAIL_API_KEY environment variable.');
alertAdmin('API key invalid or expired');
break;
case 'ERR_TMPL_002':
// Template not found - check template ID, don't retry
console.error(`Template not found: ${error.relatedInfo.templateId}`);
break;
case 'ERR_QUOTA_001':
// Email quota exceeded - wait until next billing period
console.error('Monthly email quota exceeded');
alertAdmin('Upgrade plan or wait for quota reset');
break;
case 'ERR_QUOTA_003':
// Rate limit - retry after specified time
const retryAfter = error.relatedInfo.retryAfterSeconds;
console.log(`Rate limited. Retry after ${retryAfter}s`);
await sleep(retryAfter * 1000);
return sendEmail(emailData); // Retry once
break;
default:
// Unknown error - log and alert
console.error('Unexpected error:', error);
alertAdmin(`API error: ${error.code}`);
}
}4. Log Errors Properly
✅ Structured logging:
function logApiError(error, context) {
logger.error({
timestamp: new Date().toISOString(),
errorCode: error.code,
errorMessage: error.message,
statusCode: error.statusCode,
context: {
templateId: context.templateId,
recipientCount: context.recipientCount,
// Don't log recipient emails (privacy)
},
stack: error.stack
});
}Performance Optimization
1. Use Bulk Endpoints
❌ Bad - 100 individual requests:
for (const recipient of recipients) {
await sendEmail({
templateId: 'tmpl_welcome',
to: [{ email: recipient.email }],
variables: recipient.variables
});
}
// 100 API calls, 100x the time, 100x rate limit usage✅ Good - 1 bulk request:
await sendBulkEmails({
templateId: 'tmpl_welcome',
recipients: recipients.map(r => ({
email: r.email,
variables: r.variables
}))
});
// 1 API call, much faster, efficient rate limit usage2. Implement Request Queuing
For high-volume applications, queue requests to avoid rate limits:
class EmailQueue {
constructor(rateLimitPerMinute = 30) {
this.queue = [];
this.processing = false;
this.rateLimitPerMinute = rateLimitPerMinute;
this.requestTimes = [];
}
enqueue(emailData) {
return new Promise((resolve, reject) => {
this.queue.push({ emailData, resolve, reject });
this.process();
});
}
async process() {
if (this.processing || this.queue.length === 0) return;
this.processing = true;
while (this.queue.length > 0) {
// Clean up old request times (>1 minute ago)
const now = Date.now();
this.requestTimes = this.requestTimes.filter(t => now - t < 60000);
// Wait if at rate limit
if (this.requestTimes.length >= this.rateLimitPerMinute) {
const oldestRequest = this.requestTimes[0];
const waitTime = 60000 - (now - oldestRequest);
await sleep(waitTime);
continue;
}
// Process next email
const { emailData, resolve, reject } = this.queue.shift();
this.requestTimes.push(now);
try {
const result = await sendEmail(emailData);
resolve(result);
} catch (error) {
reject(error);
}
}
this.processing = false;
}
}
// Usage
const queue = new EmailQueue(30);
const result = await queue.enqueue(emailData);3. Validate Before Sending
Validate data before making API calls to avoid wasted requests:
function validateEmailData(data) {
// Check required fields
if (!data.templateId) {
throw new Error('Template ID required');
}
if (!data.to || data.to.length === 0) {
throw new Error('At least one recipient required');
}
// Validate email addresses
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
for (const recipient of data.to) {
if (!emailRegex.test(recipient.email)) {
throw new Error(`Invalid email: ${recipient.email}`);
}
}
// Validate required variables
if (!data.variables) {
throw new Error('Variables object required');
}
return true;
}
// Use it
try {
validateEmailData(emailData); // Validate first
const result = await sendEmail(emailData); // Then send
} catch (error) {
console.error('Validation error:', error.message);
}4. Cache Template IDs
Store template IDs in your application configuration instead of fetching them repeatedly:
// config/email-templates.js
const EMAIL_TEMPLATES = {
WELCOME: process.env.TEMPLATE_ID_WELCOME || 'tmpl_welcome_abc123',
PASSWORD_RESET: process.env.TEMPLATE_ID_PASSWORD_RESET || 'tmpl_reset_xyz456',
INVOICE: process.env.TEMPLATE_ID_INVOICE || 'tmpl_invoice_def789',
NEWSLETTER: process.env.TEMPLATE_ID_NEWSLETTER || 'tmpl_newsletter_ghi012',
};
// Usage
await sendEmail({
templateId: EMAIL_TEMPLATES.WELCOME,
to: [{ email: user.email }],
variables: { ... }
});Note: Templates are managed in the FormaMail dashboard. Store template IDs in environment variables for easy configuration across environments.
Email Delivery Best Practices
1. Always Use Templates
FormaMail requires templates for all emails:
✅ Templates ensure:
- Consistent branding
- Variable validation
- Reusability
- Easy updates without code changes
Create templates in dashboard, reference via API:
{
templateId: 'tmpl_welcome',
to: [{ email: 'user@example.com' }],
variables: {
firstName: 'John',
companyName: 'Acme Corp'
}
}2. Use Descriptive Subject Lines
// ❌ Bad
{ subject: 'Update' }
// ✅ Good
{ subject: 'Your Acme Corp invoice is ready' }3. Provide Value in Variables
// ❌ Bad - minimal personalization
{
variables: {
name: 'User'
}
}
// ✅ Good - rich personalization
{
variables: {
firstName: 'John',
lastName: 'Doe',
companyName: 'Acme Corp',
accountTier: 'Pro',
dashboardUrl: 'https://app.acme.com/dashboard',
supportEmail: 'support@acme.com'
}
}4. Bounce and Complaint Handling
FormaMail automatically tracks email delivery events and manages suppression lists:
What’s Automated:
- ✅ Hard bounces tracked and suppressed automatically
- ✅ Spam complaints tracked and suppressed immediately
- ✅ Suppressed emails filtered out before sending
- ✅ Delivery, bounce, and complaint tracking via dashboard
Your Responsibility:
- Maintain clean email lists in your database
- Remove invalid emails from your mailing lists
- Monitor bounce/complaint rates in the dashboard
- Follow email best practices (permission-based sending, unsubscribe links)
Healthy Metrics (view in dashboard):
- Bounce rate: < 5%
- Complaint rate: < 0.1%
Automatic Protection: FormaMail prevents you from sending to bounced or complained addresses. If all recipients are suppressed, the API returns an error with details. If some are suppressed, they’re automatically filtered out and the email sends to valid recipients only.
Testing
1. Use Test API Keys in Development
const apiKey = process.env.NODE_ENV === 'production'
? process.env.FORMAMAIL_API_KEY_PROD
: process.env.FORMAMAIL_API_KEY_TEST;Benefits:
- No cost during development
- Isolated from production metrics
- Can delete/reset without risk
2. Test with Real Data
✅ Production-like test data:
const testEmail = {
templateId: 'tmpl_welcome',
to: [{ email: 'test@yourdomain.com', name: 'Test User' }],
variables: {
firstName: 'John',
lastName: 'Doe',
companyName: 'Test Corp',
accountUrl: 'https://staging.app.com/account/test-123',
supportEmail: 'support@yourdomain.com'
}
};3. Test Error Scenarios
// Test with invalid template ID
await expect(sendEmail({ templateId: 'invalid' }))
.rejects.toThrow('ERR_TMPL_002');
// Test with missing required variable
await expect(sendEmail({
templateId: 'tmpl_welcome',
variables: {} // Missing required variables
})).rejects.toThrow('ERR_TMPL_004');
// Test rate limiting
const promises = Array(35).fill().map(() => sendEmail(testEmail));
const results = await Promise.allSettled(promises);
const rateLimited = results.filter(r => r.reason?.code === 'ERR_QUOTA_003');
expect(rateLimited.length).toBeGreaterThan(0);4. Test Email Rendering
Send test emails to yourself before production:
if (process.env.NODE_ENV !== 'production') {
// Override recipient in dev/staging
emailData.to = [{ email: 'developer@yourdomain.com' }];
}Monitoring and Logging
1. Log All Email Sends
async function sendEmail(emailData) {
const startTime = Date.now();
try {
const result = await fetch(url, options);
// Log success
logger.info({
event: 'email_sent',
emailId: result.id,
templateId: emailData.templateId,
recipientCount: emailData.to.length,
duration: Date.now() - startTime,
timestamp: new Date().toISOString()
});
return result;
} catch (error) {
// Log failure
logger.error({
event: 'email_failed',
templateId: emailData.templateId,
errorCode: error.code,
errorMessage: error.message,
duration: Date.now() - startTime,
timestamp: new Date().toISOString()
});
throw error;
}
}2. Monitor API Health
async function checkApiHealth() {
try {
const response = await fetch('https://api.formamail.com/api/health');
if (!response.ok) {
alertAdmin('FormaMail API is down!');
}
} catch (error) {
alertAdmin('Cannot reach FormaMail API');
}
}
// Check every 5 minutes
setInterval(checkApiHealth, 300000);3. Track Success Rates
const metrics = {
sent: 0,
failed: 0,
rateLimited: 0
};
async function sendEmailWithMetrics(emailData) {
try {
const result = await sendEmail(emailData);
metrics.sent++;
return result;
} catch (error) {
if (error.code === 'ERR_QUOTA_003') {
metrics.rateLimited++;
} else {
metrics.failed++;
}
throw error;
}
}
// Report metrics every hour
setInterval(() => {
logger.info({
event: 'email_metrics',
sent: metrics.sent,
failed: metrics.failed,
rateLimited: metrics.rateLimited,
successRate: (metrics.sent / (metrics.sent + metrics.failed)) * 100
});
// Reset counters
metrics.sent = 0;
metrics.failed = 0;
metrics.rateLimited = 0;
}, 3600000);4. Alert on Critical Errors
function alertAdmin(message, severity = 'warning') {
// Send to monitoring service (e.g., Sentry, DataDog, PagerDuty)
if (severity === 'critical') {
// Page on-call engineer
sendSlackAlert(`🚨 CRITICAL: ${message}`);
sendPagerDutyAlert(message);
} else if (severity === 'warning') {
// Log and notify
sendSlackAlert(`⚠️ WARNING: ${message}`);
}
logger.log(severity, message);
}Security Considerations
1. Sanitize User Input
Always sanitize data before including in emails:
function sanitizeInput(input) {
if (typeof input !== 'string') return input;
// Remove potentially dangerous content
return input
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/on\w+\s*=\s*["'][^"']*["']/gi, '')
.replace(/javascript:/gi, '');
}
// Use it
const emailData = {
templateId: 'tmpl_welcome',
to: [{ email: userEmail }],
variables: {
firstName: sanitizeInput(user.firstName),
companyName: sanitizeInput(user.companyName),
// ... sanitize all user-provided data
}
};2. Validate Email Addresses
function isValidEmail(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!regex.test(email)) return false;
// Additional checks
if (email.length > 254) return false; // RFC 5321
if (email.split('@')[0].length > 64) return false; // Local part max
return true;
}
// Use it
if (!isValidEmail(userEmail)) {
throw new Error('Invalid email address');
}3. Implement Rate Limiting on Your End
Even though FormaMail has rate limits, add your own to prevent abuse:
const userRequestCounts = new Map();
function checkUserRateLimit(userId, maxPerHour = 100) {
const now = Date.now();
const oneHourAgo = now - 3600000;
// Get user's request history
let requests = userRequestCounts.get(userId) || [];
// Remove requests older than 1 hour
requests = requests.filter(time => time > oneHourAgo);
// Check limit
if (requests.length >= maxPerHour) {
throw new Error('User rate limit exceeded');
}
// Record request
requests.push(now);
userRequestCounts.set(userId, requests);
}Production Checklist
Before deploying to production, verify:
API Configuration
- ✅ API key stored in environment variable (not hardcoded)
- ✅ Using production API key (
fm_sk_...) - ✅ API key has appropriate permissions
- ✅ API key expiration date set (if applicable)
Error Handling
- ✅ Retry logic implemented with exponential backoff
- ✅ All error codes handled appropriately
- ✅ Errors logged with structured logging
- ✅ Critical errors trigger alerts
Performance
- ✅ Using bulk endpoints for multiple emails
- ✅ Request queuing implemented for high volume
- ✅ Rate limit headers monitored
- ✅ Input validation before API calls
Security
- ✅ User input sanitized
- ✅ Email addresses validated
- ✅ API keys not logged
- ✅ HTTPS enforced for all API calls
Monitoring
- ✅ All email sends logged
- ✅ Success/failure metrics tracked
- ✅ Health checks configured
- ✅ Alerts set up for critical errors
Testing
- ✅ Tested with production-like data
- ✅ Error scenarios tested
- ✅ Rate limiting tested
- ✅ Email rendering verified
Common Pitfalls
1. Not Handling Rate Limits
❌ Problem:
for (const user of users) {
await sendEmail({ ... }); // Will hit rate limit quickly
}✅ Solution: Use bulk endpoints or implement queuing (see Performance Optimization).
2. No Retry Logic
❌ Problem: Single network error causes email to fail permanently.
✅ Solution: Implement retry with exponential backoff (see Error Handling).
3. Hardcoded Template IDs
❌ Problem:
const TEMPLATE_ID = 'tmpl_abc123'; // Hardcoded✅ Solution:
const TEMPLATE_ID = process.env.WELCOME_EMAIL_TEMPLATE_ID;4. Not Validating Before Sending
❌ Problem: Wasting API calls on invalid data.
✅ Solution: Validate email addresses, required fields, and variable types before API calls.
Next Steps
- Sending Emails: Review API details in Sending Emails
- Error Handling: See all error codes in Error Handling
- Rate Limiting: Understand limits in Rate Limiting
Related: Sending Emails | Error Handling | Rate Limiting | Authentication