Developer GuideEmails with Attachments

Sending Emails with Attachments

This is FormaMail’s superpower: send transactional emails with dynamically generated PDF or Excel attachments in a single API call. No Puppeteer. No S3. No glue code.

Quick Example

// Send order confirmation with PDF invoice attached
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({
    templateId: "order-confirmation",
    to: [{ email: "customer@example.com", name: "John Doe" }],
    variables: {
      orderId: "12345",
      customerName: "John Doe",
      items: [
        { name: "Widget", qty: 2, price: 29.99 },
        { name: "Gadget", qty: 1, price: 49.99 }
      ],
      total: 109.97
    },
    attachments: [{
      attachmentTemplateId: "invoice-pdf"
    }]
  })
});
 
const result = await response.json();
console.log('Email sent:', result.emailId);
// Customer receives email with professionally formatted PDF invoice attached

Result: Customer receives an email with a dynamically generated PDF invoice attached, using the same order data for both email and PDF.

How It Works

  1. You call the API with email template ID + attachment template ID + variables
  2. FormaMail renders the email using your email variables
  3. FormaMail generates the PDF/Excel using attachment variables (provided separately or inherited)
  4. FormaMail attaches the file to the email
  5. FormaMail sends the email via AWS SES
  6. You receive a response with the email ID

All in one API call. No intermediate services. No temporary storage.

Providing Variables

FormaMail supports variable inheritance to reduce payload size and simplify API calls:

  • Email variables are provided in the variables field
  • Attachment variables inherit from email variables automatically
  • Attachment-specific variables (in attachments[].variables) can override or extend inherited values

Variable Priority (lowest to highest)

  1. Email-level variables - Base variables from variables field
  2. Attachment-specific variables - Override/extend from attachments[].variables

Pro Tip: You don’t need to duplicate variables! If your email and attachment use the same data (like orderId, customerName, items), just provide them once at the email level.

Your email template might have variables like:

Hi {{customerName}},
 
Your order #{{orderId}} is confirmed.
Total: ${{total}}
 
See the attached invoice for details.

Your PDF invoice template has its own variables:

INVOICE #{{orderId}}
Bill To: {{customerName}}
 
{{#each items}}
{{name}} - Qty: {{qty}} - ${{price}}
{{/each}}
 
Total: ${{total}}

Define your data once. Use it everywhere.

Attachment Options

The most common approach - generate PDF/Excel from a FormaMail template:

{
  "templateId": "order-email",
  "to": [{"email": "customer@example.com"}],
  "variables": { "orderId": "12345" },
  "attachments": [{
    "attachmentTemplateId": "invoice-template",
    "filename": "invoice-12345.pdf",
    "outputFormats": ["pdf"]
  }]
}
FieldRequiredDescription
attachmentTemplateIdYesTemplate ID, shortId, or slug
filenameNoCustom filename (default: template name)
outputFormatsNoArray: ["pdf"], ["excel"], or ["pdf", "excel"]
variablesNoOverride variables (merged with email variables)

2. Multiple Attachments

Attach multiple files to a single email:

{
  "attachments": [
    {
      "attachmentTemplateId": "invoice-pdf",
      "filename": "invoice.pdf"
    },
    {
      "attachmentTemplateId": "receipt-excel",
      "outputFormats": ["excel"],
      "filename": "receipt.xlsx"
    }
  ]
}

3. Both PDF and Excel from Same Template

Generate both formats from one template:

{
  "attachments": [{
    "attachmentTemplateId": "monthly-report",
    "outputFormats": ["pdf", "excel"],
    "filename": "report-november"
  }]
}

Results in two attachments:

  • report-november.pdf
  • report-november.xlsx

Attachments automatically inherit email variables. Only specify attachment-specific overrides:

{
  "templateId": "shipping-notification",
  "variables": {
    "customerName": "John",
    "trackingNumber": "1Z999AA10123456784",
    "orderItems": [...]
  },
  "attachments": [{
    "attachmentTemplateId": "packing-slip",
    "variables": {
      "includeBarcode": true,
      "warehouseNotes": "Fragile - Handle with care"
    }
  }]
}

What the attachment receives:

{
  // Inherited from email variables
  customerName: "John",
  trackingNumber: "1Z999AA10123456784",
  orderItems: [...],
  // Plus attachment-specific variables
  includeBarcode: true,
  warehouseNotes: "Fragile - Handle with care"
}

Variable Priority: If the same variable name exists in both email and attachment variables, the attachment-specific value takes precedence.

5. Attach Static Files (Base64)

Attach a file you already have:

{
  "attachments": [{
    "filename": "contract.pdf",
    "content": "JVBERi0xLjQK...",  // Base64 encoded
    "contentType": "application/pdf"
  }]
}

6. Attach from URL

Attach a file from a URL:

{
  "attachments": [{
    "filename": "report.pdf",
    "url": "https://example.com/reports/monthly.pdf"
  }]
}
⚠️

URL must be publicly accessible. FormaMail will fetch the file at send time. Maximum file size: 10 MB.

Attach a file from your uploaded assets:

{
  "attachments": [{
    "assetId": "asset-uuid-here"
  }]
}

Template Identification

You can reference templates by UUID, shortId, or slug:

// UUID
"attachmentTemplateId": "550e8400-e29b-41d4-a716-446655440000"
 
// Short ID (prefixed)
"attachmentTemplateId": "atpl_a1b2c3d4"
 
// Slug (human-readable)
"attachmentTemplateId": "monthly-invoice"

We recommend using slugs for readability. Create them when setting up templates in the dashboard.

Output Formats

PDF

Best for:

  • Invoices and receipts
  • Reports and statements
  • Contracts and agreements
  • Any document intended for print
"outputFormats": ["pdf"]

Excel

Best for:

  • Data exports
  • Spreadsheets with formulas
  • Reports users will edit
  • Data for further analysis
"outputFormats": ["excel"]

Both

Generate both formats:

"outputFormats": ["pdf", "excel"]

Full API Schema

interface SendEmailRequest {
  // Email template
  templateId: string;
 
  // Recipients (up to 50)
  to: Array<{
    email: string;
    name?: string;
  }>;
 
  // Optional CC/BCC
  cc?: Array<{ email: string; name?: string }>;
  bcc?: Array<{ email: string; name?: string }>;
 
  // Template variables
  variables: Record<string, any>;
 
  // Attachments (up to 10)
  attachments?: Array<
    | TemplateAttachment
    | Base64Attachment
    | UrlAttachment
    | AssetAttachment
  >;
 
  // Optional overrides
  subject?: string;      // Override template subject
  replyTo?: string;      // Override reply-to address
  metadata?: Record<string, string>;  // Custom tracking data
}
 
interface TemplateAttachment {
  attachmentTemplateId: string;
  filename?: string;
  outputFormats?: Array<'pdf' | 'excel'>;
  variables?: Record<string, any>;
}
 
interface Base64Attachment {
  filename: string;
  content: string;  // Base64 encoded
  contentType: string;
}
 
interface UrlAttachment {
  filename: string;
  url: string;
}
 
interface AssetAttachment {
  assetId: string;
}

Code Examples

Node.js / Express

const express = require('express');
const app = express();
 
app.post('/send-invoice', async (req, res) => {
  const { order, customer } = req.body;
 
  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({
        templateId: 'order-confirmation',
        to: [{ email: customer.email, name: customer.name }],
        variables: {
          orderId: order.id,
          orderDate: new Date().toLocaleDateString(),
          customerName: customer.name,
          items: order.items,
          subtotal: order.subtotal,
          tax: order.tax,
          total: order.total
        },
        attachments: [{
          attachmentTemplateId: 'invoice-pdf',
          filename: `invoice-${order.id}.pdf`
        }]
      })
    });
 
    const result = await response.json();
 
    if (!response.ok) {
      throw new Error(result.message);
    }
 
    res.json({ success: true, emailId: result.emailId });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

Python

import requests
import os
 
def send_invoice_email(order, customer):
    response = requests.post(
        'https://api.formamail.com/api/emails/send',
        headers={
            'Authorization': f"Bearer {os.environ['FORMAMAIL_API_KEY']}",
            'Content-Type': 'application/json'
        },
        json={
            'templateId': 'order-confirmation',
            'to': [{'email': customer['email'], 'name': customer['name']}],
            'variables': {
                'orderId': order['id'],
                'customerName': customer['name'],
                'items': order['items'],
                'total': order['total']
            },
            'attachments': [{
                'attachmentTemplateId': 'invoice-pdf',
                'filename': f"invoice-{order['id']}.pdf"
            }]
        }
    )
 
    response.raise_for_status()
    return response.json()

cURL

curl -X POST https://api.formamail.com/api/emails/send \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "templateId": "order-confirmation",
    "to": [{"email": "customer@example.com", "name": "John Doe"}],
    "variables": {
      "orderId": "12345",
      "customerName": "John Doe",
      "items": [{"name": "Widget", "qty": 2, "price": 29.99}],
      "total": 59.98
    },
    "attachments": [{
      "attachmentTemplateId": "invoice-pdf",
      "filename": "invoice-12345.pdf"
    }]
  }'

Limits

LimitValue
Attachments per email10
Total attachment size25 MB
Single attachment10 MB
PDF pages50
Excel rows per sheet100,000

See Limits & Quotas for the complete list.

Common Patterns

Invoice on Order Completion

// In your order completion handler
async function onOrderComplete(order) {
  await sendEmail({
    templateId: 'order-confirmation',
    to: [{ email: order.customerEmail }],
    variables: formatOrderVariables(order),
    attachments: [{
      attachmentTemplateId: 'invoice-pdf',
      filename: `invoice-${order.id}.pdf`
    }]
  });
}

Weekly Report with Excel

// Scheduled job (cron)
async function sendWeeklyReport() {
  const data = await getWeeklyAnalytics();
 
  await sendEmail({
    templateId: 'weekly-report',
    to: [
      { email: 'team@company.com' },
      { email: 'leadership@company.com' }
    ],
    variables: {
      weekOf: getWeekRange(),
      metrics: data.summary,
      dailyBreakdown: data.daily
    },
    attachments: [{
      attachmentTemplateId: 'analytics-report',
      outputFormats: ['pdf', 'excel'],
      filename: `report-week-${getWeekNumber()}`
    }]
  });
}

Contract with Signature

// Send contract with company signature already embedded
await sendEmail({
  templateId: 'contract-email',
  to: [{ email: client.email }],
  variables: {
    clientName: client.name,
    contractTerms: contractDetails,
    signatureDate: new Date().toLocaleDateString()
  },
  attachments: [{
    attachmentTemplateId: 'service-agreement',
    filename: `contract-${client.id}.pdf`
  }]
});

Error Handling

const response = await fetch('https://api.formamail.com/api/emails/send', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(emailData)
});
 
if (!response.ok) {
  const error = await response.json();
 
  switch (error.code) {
    case 'ERR_TMPL_002':
      console.error('Template not found:', error.relatedInfo.templateId);
      break;
    case 'ERR_ATTACH_001':
      console.error('Attachment template not found');
      break;
    case 'ERR_ATTACH_002':
      console.error('Attachment too large');
      break;
    case 'ERR_QUOTA_001':
      console.error('Rate limit exceeded, retry after:', error.retryAfter);
      break;
    default:
      console.error('Unknown error:', error);
  }
}

FAQ

How long does attachment generation take?

  • PDF: 2-10 seconds depending on complexity
  • Excel: 1-5 seconds depending on data size

The API responds immediately with a queued status. Actual delivery happens within seconds to minutes depending on queue depth.

Can I track if the attachment was opened?

FormaMail tracks email opens and link clicks, but cannot track if a PDF attachment was opened (this would require desktop software).

What happens if attachment generation fails?

The email will not be sent. You’ll receive an error response with details about the failure. Common causes:

  • Invalid template variables
  • Template references non-existent images
  • Data exceeds limits (too many pages, rows, etc.)

Can I preview the attachment before sending?

Yes! Use the attachment preview endpoint:

GET /api/templates/attachment/:id/preview?variables={...}

Or use the preview feature in the dashboard template editor.

Next Steps