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 attachedResult: Customer receives an email with a dynamically generated PDF invoice attached, using the same order data for both email and PDF.
How It Works
- You call the API with email template ID + attachment template ID + variables
- FormaMail renders the email using your email variables
- FormaMail generates the PDF/Excel using attachment variables (provided separately or inherited)
- FormaMail attaches the file to the email
- FormaMail sends the email via AWS SES
- 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
variablesfield - Attachment variables inherit from email variables automatically
- Attachment-specific variables (in
attachments[].variables) can override or extend inherited values
Variable Priority (lowest to highest)
- Email-level variables - Base variables from
variablesfield - 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
1. Generate from Template (Recommended)
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"]
}]
}| Field | Required | Description |
|---|---|---|
attachmentTemplateId | Yes | Template ID, shortId, or slug |
filename | No | Custom filename (default: template name) |
outputFormats | No | Array: ["pdf"], ["excel"], or ["pdf", "excel"] |
variables | No | Override 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.pdfreport-november.xlsx
4. Variable Inheritance (Recommended)
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.
7. Attach from Asset Gallery
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
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
| Limit | Value |
|---|---|
| Attachments per email | 10 |
| Total attachment size | 25 MB |
| Single attachment | 10 MB |
| PDF pages | 50 |
| Excel rows per sheet | 100,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.