Developer GuideSending Emails

Sending Emails

Send transactional and bulk emails via the FormaMail API using templates.

Overview

The FormaMail API provides two endpoints for sending emails with API key authentication:

  • Single Email: Send one email to one or more recipients using a template
  • Bulk Personalized: Send personalized emails to many recipients with individual variables

Important: All emails must use a template. Templates are created via the dashboard.


API Endpoints

Send Single Email

POST /api/emails/send

Send an email using a template to one or more recipients.

Send Bulk Personalized Emails

POST /api/emails/send/bulk

Send personalized emails to multiple recipients, each with their own variables.


Authentication

All requests require an API key in the Authorization header:

Authorization: Bearer fm_sk_abc123xyz456...

Get your API key from the dashboard: Settings → API Keys


Send Single Email

Request Format

curl -X POST 'http://localhost:3000/api/emails/send' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer fm_sk_abc123...' \
  -d '{
    "templateId": "tmpl_abc123",
    "to": [
      { "email": "user@example.com", "name": "John Doe" }
    ],
    "variables": {
      "firstName": "John",
      "orderNumber": "12345",
      "orderTotal": 99.99
    }
  }'

Request Body

FieldTypeRequiredDescription
templateIdstringYesTemplate ID from dashboard
toarrayYesRecipients (max 50)
to[].emailstringYesRecipient email address
to[].namestringNoRecipient name
variablesobjectNoTemplate variables
ccarrayNoCC recipients
bccarrayNoBCC recipients
replyTostringNoReply-to email address
attachmentsarrayNoAttachments (see below)
tagsstring[]NoTags for categorization
prioritystringNohigh, normal, or low
scheduledAtstringNoISO 8601 datetime for scheduling

Example: Simple Email

const axios = require('axios');
 
const API_KEY = 'fm_sk_abc123...';
const API_ENDPOINT = 'http://localhost:3000/api';
 
async function sendEmail() {
  try {
    const response = await axios.post(`${API_ENDPOINT}/emails/send`, {
      templateId: 'tmpl_welcome',
      to: [
        { email: 'user@example.com', name: 'John Doe' }
      ],
      variables: {
        firstName: 'John',
        companyName: 'Acme Corp'
      }
    }, {
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${API_KEY}`
      }
    });
 
    console.log('Email sent successfully:', response.data);
    return response.data;
  } catch (error) {
    console.error('Error sending email:', error.response?.data || error.message);
    throw error;
  }
}
 
sendEmail();

Example: Email with CC/BCC

{
  "templateId": "tmpl_notification",
  "to": [
    { "email": "primary@example.com", "name": "Primary User" }
  ],
  "cc": [
    { "email": "manager@example.com", "name": "Manager" }
  ],
  "bcc": [
    { "email": "archive@example.com" }
  ],
  "replyTo": "support@example.com",
  "variables": {
    "subject": "Project Update",
    "message": "The project has been completed."
  },
  "tags": ["notification", "project"]
}

Example: Scheduled Email

{
  "templateId": "tmpl_reminder",
  "to": [
    { "email": "user@example.com", "name": "John" }
  ],
  "variables": {
    "eventName": "Team Meeting",
    "eventTime": "2025-11-08 14:00"
  },
  "scheduledAt": "2025-11-08T09:00:00Z",
  "priority": "high"
}

Response

Success (200 OK):

{
  "id": "email_abc123xyz456",
  "status": "queued",
  "templateId": "tmpl_welcome",
  "to": [
    { "email": "user@example.com", "name": "John Doe" }
  ],
  "queuedAt": "2025-11-07T10:30:00Z"
}

Email Statuses:

  • pending: Email created but not yet queued
  • queued: Email queued for sending
  • processing: Email is being processed
  • scheduled: Email scheduled for future delivery
  • sent: Email sent to provider (AWS SES)
  • delivered: Email delivered to recipient’s inbox
  • failed: Email sending failed permanently
  • bounced: Email bounced (delivery failed)
  • rejected: Email rejected by recipient server

Send Bulk Personalized Emails

Send personalized emails to multiple recipients, each with their own variables.

Request Format

curl -X POST 'http://localhost:3000/api/emails/send/bulk' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer fm_sk_abc123...' \
  -d '{
    "templateId": "tmpl_order_confirmation",
    "recipients": [
      {
        "email": "user1@example.com",
        "name": "John Doe",
        "variables": {
          "orderNumber": "12345",
          "orderTotal": 99.99
        }
      },
      {
        "email": "user2@example.com",
        "name": "Jane Smith",
        "variables": {
          "orderNumber": "12346",
          "orderTotal": 149.99
        }
      }
    ],
    "baseVariables": {
      "companyName": "Acme Corp",
      "supportEmail": "support@acme.com"
    },
    "batchName": "Daily Order Confirmations"
  }'

Request Body

FieldTypeRequiredDescription
templateIdstringYesTemplate ID from dashboard
recipientsarrayYesArray of recipients (max 1000)
recipients[].emailstringYesRecipient email
recipients[].namestringNoRecipient name
recipients[].variablesobjectYesPer-recipient variables
baseVariablesobjectNoShared variables for all recipients
batchNamestringNoName for this batch
tagsstring[]NoTags for all emails
prioritystringNoPriority for all emails

Example: JavaScript

const axios = require('axios');
 
const API_KEY = 'fm_sk_abc123...';
const API_ENDPOINT = 'http://localhost:3000/api';
 
async function sendBulkEmails() {
  try {
    const response = await axios.post(`${API_ENDPOINT}/emails/send/bulk`, {
      templateId: 'tmpl_order_confirmation',
      recipients: [
        {
          email: 'user1@example.com',
          name: 'John Doe',
          variables: {
            orderNumber: '12345',
            orderTotal: 99.99,
            items: [
              { name: 'Product A', price: 49.99 },
              { name: 'Product B', price: 49.99 }
            ]
          }
        },
        {
          email: 'user2@example.com',
          name: 'Jane Smith',
          variables: {
            orderNumber: '12346',
            orderTotal: 149.99,
            items: [
              { name: 'Product C', price: 149.99 }
            ]
          }
        }
      ],
      baseVariables: {
        companyName: 'Acme Corp',
        supportEmail: 'support@acme.com'
      },
      batchName: 'Daily Order Confirmations',
      tags: ['order', 'confirmation']
    }, {
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${API_KEY}`
      }
    });
 
    console.log('Bulk emails queued:', response.data);
    return response.data;
  } catch (error) {
    console.error('Error sending bulk emails:', error.response?.data || error.message);
    throw error;
  }
}
 
sendBulkEmails();

Example: Python

import requests
 
API_KEY = 'fm_sk_abc123...'
API_ENDPOINT = 'http://localhost:3000/api'
 
def send_bulk_emails():
    url = f'{API_ENDPOINT}/emails/send/bulk'
    headers = {
        'Content-Type': 'application/json',
        'Authorization': f'Bearer {API_KEY}'
    }
 
    payload = {
        'templateId': 'tmpl_order_confirmation',
        'recipients': [
            {
                'email': 'user1@example.com',
                'name': 'John Doe',
                'variables': {
                    'orderNumber': '12345',
                    'orderTotal': 99.99
                }
            },
            {
                'email': 'user2@example.com',
                'name': 'Jane Smith',
                'variables': {
                    'orderNumber': '12346',
                    'orderTotal': 149.99
                }
            }
        ],
        'baseVariables': {
            'companyName': 'Acme Corp'
        },
        'batchName': 'Daily Orders'
    }
 
    try:
        response = requests.post(url, headers=headers, json=payload)
        response.raise_for_status()
 
        print('Bulk emails queued:', response.json())
        return response.json()
    except requests.exceptions.RequestException as error:
        print('Error sending bulk emails:', error)
        if hasattr(error, 'response') and error.response is not None:
            print('Response:', error.response.text)
        raise
 
if __name__ == '__main__':
    send_bulk_emails()

Response

Success (200 OK):

{
  "batchId": "batch_abc123",
  "status": "queued",
  "totalRecipients": 2,
  "queuedAt": "2025-11-07T10:30:00Z"
}

Attachments

You can include attachments in emails using two methods:

1. Template-Generated Attachments

Generate PDF or Excel files from attachment templates:

{
  "templateId": "tmpl_email",
  "to": [{ "email": "user@example.com" }],
  "variables": { "name": "John" },
  "attachments": [
    {
      "filename": "invoice.pdf",
      "attachmentTemplateId": "tmpl_pdf_invoice",
      "variables": {
        "invoiceNumber": "INV-2025-001",
        "amount": 99.99
      },
      "outputFormats": ["pdf"]
    }
  ]
}

Attachment Fields:

  • filename: Name of the file
  • attachmentTemplateId: Template ID for PDF/Excel (created in dashboard)
  • variables: Variables for the attachment template
  • outputFormats: Array of formats (["pdf"] or ["excel"])

2. Pre-Uploaded Files

Attach files by providing base64-encoded content:

{
  "templateId": "tmpl_email",
  "to": [{ "email": "user@example.com" }],
  "attachments": [
    {
      "filename": "document.pdf",
      "content": "JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC...",
      "contentType": "application/pdf"
    }
  ]
}

Note: For large files, use template-generated attachments instead of base64 encoding.


Template Variables

Templates support variables using Handlebars syntax {{variableName}}.

Example Template:

<h1>Hello {{firstName}}!</h1>
<p>Your order #{{orderNumber}} totals ${{orderTotal}}.</p>

Variables:

{
  "firstName": "John",
  "orderNumber": "12345",
  "orderTotal": 99.99
}

Result:

<h1>Hello John!</h1>
<p>Your order #12345 totals $99.99.</p>

Supported Variable Types

  • Strings: "John Doe"
  • Numbers: 99.99
  • Booleans: true / false
  • Arrays: ["item1", "item2"]
  • Objects: { "name": "John", "age": 30 }

Conditionals

{{#if isPremium}}
  <p>Thank you for being a premium member!</p>
{{else}}
  <p>Upgrade to premium for exclusive benefits.</p>
{{/if}}

Loops

<ul>
  {{#each items}}
    <li>{{name}} - ${{price}}</li>
  {{/each}}
</ul>

Error Responses

Invalid Template ID

Status Code: 404 Not Found

{
  "statusCode": 404,
  "code": "ERR_TMPL_002",
  "message": "Email template not found",
  "timestamp": "2025-11-07T10:30:00Z",
  "path": "/api/emails/send",
  "relatedInfo": {
    "templateId": "tmpl_invalid"
  }
}

Invalid Email Format

Status Code: 400 Bad Request

{
  "statusCode": 400,
  "code": "ERR_VALID_001",
  "message": "Validation failed",
  "relatedInfo": {
    "errors": [
      {
        "field": "to[0].email",
        "message": "Invalid email format"
      }
    ]
  }
}

Missing Required Variable

Status Code: 400 Bad Request

{
  "statusCode": 400,
  "code": "ERR_TMPL_004",
  "message": "Missing required template variable",
  "relatedInfo": {
    "variable": "firstName",
    "templateId": "tmpl_welcome"
  }
}

Quota Exceeded

Status Code: 429 Too Many Requests

{
  "statusCode": 429,
  "code": "ERR_QUOTA_001",
  "message": "Monthly email quota exceeded",
  "relatedInfo": {
    "limit": 10000,
    "used": 10000,
    "resetsAt": "2025-12-01T00:00:00Z"
  }
}

Sending Emails with Charts

Charts can be included in email templates to visualize data like sales trends, analytics, or performance metrics. In emails, charts are rendered as high-resolution static images via QuickCharts API.

Example: Weekly Sales Report Email

const axios = require('axios');
 
const API_KEY = 'fm_sk_abc123...';
const API_ENDPOINT = 'http://localhost:3000/api';
 
async function sendSalesReportEmail() {
  try {
    const response = await axios.post(`${API_ENDPOINT}/emails/send`, {
      templateId: 'tmpl_weekly_sales_report',  // Template with chart component
      to: [
        { email: 'manager@company.com', name: 'Sales Manager' }
      ],
      variables: {
        // Text variables
        reportWeek: 'Week 47, 2025',
        reportDate: 'November 25, 2025',
 
        // Chart data: array of objects with label and value fields
        weeklySales: [
          { day: 'Monday', revenue: 12500 },
          { day: 'Tuesday', revenue: 15200 },
          { day: 'Wednesday', revenue: 13800 },
          { day: 'Thursday', revenue: 16500 },
          { day: 'Friday', revenue: 18200 },
          { day: 'Saturday', revenue: 21000 },
          { day: 'Sunday', revenue: 14500 }
        ],
 
        // Summary metrics
        totalRevenue: 111700,
        averageDaily: 15957,
        growthPercent: 12.5
      }
    }, {
      headers: {
        'Authorization': `Bearer ${API_KEY}`,
        'Content-Type': 'application/json'
      }
    });
 
    console.log('Sales report sent:', response.data);
    return response.data;
  } catch (error) {
    console.error('Error sending sales report:', error.response?.data || error.message);
    throw error;
  }
}
 
sendSalesReportEmail();

Chart Data Format

The chart component in your template expects data in a specific format:

{
  // For bar/line/area charts (time series or categories)
  chartData: [
    { label: 'January', value: 12500 },    // label: x-axis, value: y-axis
    { label: 'February', value: 15200 },
    { label: 'March', value: 13800 }
  ],
 
  // For pie/doughnut charts (proportions)
  categoryData: [
    { label: 'Product A', value: 45 },     // value: percentage or amount
    { label: 'Product B', value: 30 },
    { label: 'Product C', value: 25 }
  ]
}

Field Mapping: The xAxis and yAxis properties in your chart component (configured in the template) specify which fields to use from your data objects. For example:

  • xAxis: "label" → uses the label field for X-axis
  • yAxis: "value" → uses the value field for Y-axis

Template Configuration (Dashboard)

In your template designer, configure the chart component with these properties:

PropertyValueDescription
chartType"bar"Bar, line, pie, doughnut, area, or scatter
dataSource"{{weeklySales}}"Variable containing array data
xAxis"day"Field name for X-axis (e.g., “day”, “label”, “month”)
yAxis"revenue"Field name for Y-axis (e.g., “revenue”, “value”, “amount”)
title"Weekly Revenue"Chart title
width600Chart width (max 600px for emails)
height400Chart height (300-400px recommended)

Multiple Charts in One Email

{
  templateId: 'tmpl_performance_dashboard',
  to: [{ email: 'executive@company.com' }],
  variables: {
    // Chart 1: Revenue trend
    monthlyRevenue: [
      { month: 'Jan', revenue: 45000 },
      { month: 'Feb', revenue: 52000 },
      { month: 'Mar', revenue: 48000 }
    ],
 
    // Chart 2: Category breakdown
    revenueByCategory: [
      { category: 'Products', amount: 85000 },
      { category: 'Services', amount: 45000 },
      { category: 'Support', amount: 15000 }
    ],
 
    // Chart 3: Regional performance
    regionalSales: [
      { region: 'North', sales: 62000 },
      { region: 'South', sales: 45000 },
      { region: 'East', sales: 38000 }
    ]
  }
}

Chart Best Practices for Emails

  1. Keep data concise: Limit to 7-10 data points for readability
  2. Use appropriate chart types:
    • Bar charts: Comparing categories
    • Line charts: Trends over time
    • Pie/doughnut: Proportions (max 5-6 slices)
  3. Optimize dimensions: Width ≤ 600px to fit email clients
  4. Use clear labels: Short, descriptive axis labels
  5. Test in email clients: Preview in Gmail, Outlook, Apple Mail

Code Examples

PHP

<?php
 
$apiKey = 'fm_sk_abc123...';
$apiEndpoint = 'http://localhost:3000/api';
 
function sendEmail($apiKey, $apiEndpoint) {
    $url = $apiEndpoint . '/emails/send';
 
    $payload = [
        'templateId' => 'tmpl_welcome',
        'to' => [
            ['email' => 'user@example.com', 'name' => 'John Doe']
        ],
        'variables' => [
            'firstName' => 'John',
            'companyName' => 'Acme Corp'
        ]
    ];
 
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_POST, 1);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Content-Type: application/json',
        'Authorization: Bearer ' . $apiKey
    ]);
 
    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $error = curl_error($ch);
    curl_close($ch);
 
    if ($error) {
        echo "cURL Error: " . $error . "\n";
        return false;
    }
 
    $result = json_decode($response, true);
 
    if ($httpCode >= 200 && $httpCode < 300) {
        echo "Email sent successfully:\n";
        print_r($result);
        return $result;
    } else {
        echo "Error sending email (HTTP $httpCode):\n";
        print_r($result);
        return false;
    }
}
 
sendEmail($apiKey, $apiEndpoint);
 
?>

Ruby

require 'net/http'
require 'json'
 
API_KEY = 'fm_sk_abc123...'
API_ENDPOINT = 'http://localhost:3000/api'
 
def send_email
  uri = URI("#{API_ENDPOINT}/emails/send")
 
  payload = {
    templateId: 'tmpl_welcome',
    to: [
      { email: 'user@example.com', name: 'John Doe' }
    ],
    variables: {
      firstName: 'John',
      companyName: 'Acme Corp'
    }
  }
 
  http = Net::HTTP.new(uri.host, uri.port)
 
  request = Net::HTTP::Post.new(uri.path)
  request['Content-Type'] = 'application/json'
  request['Authorization'] = "Bearer #{API_KEY}"
  request.body = payload.to_json
 
  response = http.request(request)
 
  if response.code.to_i >= 200 && response.code.to_i < 300
    puts "Email sent successfully:"
    puts JSON.parse(response.body)
  else
    puts "Error sending email (HTTP #{response.code}):"
    puts response.body
  end
end
 
send_email

Best Practices

1. Use Template Variables

Don’t hardcode dynamic content in templates. Use variables:

âś… Good:

{
  "templateId": "tmpl_welcome",
  "variables": { "firstName": "John" }
}

❌ Bad: Creating a new template for each user.

2. Validate Emails

Always validate email addresses before sending:

function isValidEmail(email) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

3. Handle Errors Gracefully

try {
  const result = await sendEmail(data);
  console.log('Success:', result.id);
} catch (error) {
  if (error.response?.status === 404) {
    console.error('Template not found');
  } else if (error.response?.status === 429) {
    console.error('Quota exceeded');
  } else {
    console.error('Failed:', error.message);
  }
}

4. Use Bulk API for Multiple Recipients

For sending to many recipients, use /emails/send/bulk:

// âś… Good: One API call
await sendBulkEmails({
  templateId: 'tmpl_newsletter',
  recipients: users.map(user => ({
    email: user.email,
    name: user.name,
    variables: { firstName: user.firstName }
  }))
});
 
// ❌ Bad: Many API calls
for (const user of users) {
  await sendEmail({ ... }); // Slow and hits rate limits
}

5. Test Before Production

Before going live:

  • Use a test email address you control
  • Verify template rendering with sample data
  • Check email delivery and formatting

Rate Limits

Email sending is rate limited based on authentication type:

AuthenticationSingle Email SendBulk Email Send
API Key100 requests/min10 requests/min
JWT (Dashboard)100 requests/min10 requests/min
OAuth50 requests/min5 requests/min

Note: Rate limits are per team, not per user. All team members and API keys share the same limit.

See Rate Limiting for details on handling rate limits.


Related: Authentication | Templates (Dashboard) | Error Handling