Skip to content

Templates

Create reusable email templates with placeholders. Templates are synced to AWS SES for optimized bulk sending.

All examples assume you've set the API key in the constructor. For multi-tenant usage, pass { apiKey } as the last argument to any method. See Configuration.

Create Template

ts
const { template } = await outbound.templates.create({
  name: 'welcome-email',
  subject: 'Welcome {{firstName}}!',
  htmlBody: `
    <h1>Hello {{firstName}}</h1>
    <p>Welcome to {{company}}. We're glad to have you.</p>
    <p>Click <a href="{{dashboardUrl}}">here</a> to get started.</p>
  `,
  textBody: 'Hello {{firstName}}, Welcome to {{company}}.',
  variables: ['firstName', 'company', 'dashboardUrl'],
  metadata: { category: 'onboarding', version: '2' },
});

console.log(template.id);               // 'uuid'
console.log(template.ses_template_name); // 'outbound-{uuid}' (auto-synced to SES)

Parameters

FieldTypeRequiredDescription
namestringYesUnique name within your tenant
subjectstringYesSubject line — supports
htmlBodystringYesHTML body — supports
textBodystringNoPlain text alternative
variablesstring[]NoList of variable names used in the template
metadataobjectNoCustom key-value pairs

Response

ts
{
  message: 'Template created',
  template: {
    id: '550e8400-e29b-41d4-a716-446655440000',
    tenant_id: 'uuid',
    name: 'welcome-email',
    subject: 'Welcome {{firstName}}!',
    html_body: '<h1>Hello {{firstName}}</h1>...',
    text_body: 'Hello {{firstName}}...',
    variables: ['firstName', 'company', 'dashboardUrl'],
    use_count: 0,
    last_sent_at: null,
    ses_template_name: 'outbound-550e8400...',
    status: 'active',
    metadata: { category: 'onboarding', version: '2' },
    created_at: '2026-03-08T10:00:00.000Z',
    updated_at: '2026-03-08T10:00:00.000Z'
  }
}

Possible Errors

ErrorStatusCause
BadRequestError400Missing name, subject, or htmlBody
ConflictError409Template with this name already exists for your tenant
ForbiddenError403Template quota exceeded (check outbound.templates.stats())

List Templates

ts
const result = await outbound.templates.list({
  status: 'active',
  search: 'welcome',
  sortBy: 'use_count',
  sortOrder: 'DESC',
  page: 1,
  limit: 20,
});

console.log(result.total);           // 45
console.log(result.quota.remaining); // 955 templates remaining

Query Parameters

FieldTypeDefaultDescription
status'active' | 'archived'AllFilter by template status
searchstringSearch by template name (case-insensitive)
sortBystringcreatedAtSort by: name, createdAt, use_count, last_sent_at
sortOrder'ASC' | 'DESC'DESCSort direction
pagenumber1Page number
limitnumber20Items per page (max 100)

Response

ts
{
  templates: [ /* array of Template objects */ ],
  total: 45,
  page: 1,
  limit: 20,
  quota: {
    used: 45,
    allocated: 1000,
    remaining: 955
  }
}

Auto-Pagination

Iterate through all templates without managing pages manually:

ts
for await (const template of outbound.templates.listAll({ status: 'active' })) {
  console.log(`${template.name} — used ${template.use_count} times`);
}

Get Single Template

ts
const { template } = await outbound.templates.get('template-uuid');

Possible Errors

ErrorStatusCause
NotFoundError404Template ID doesn't exist or belongs to another tenant

Update Template

Only pass the fields you want to change:

ts
const { template } = await outbound.templates.update('template-uuid', {
  subject: 'Updated: Welcome {{firstName}}!',
  htmlBody: '<h1>New design</h1><p>Hello {{firstName}}</p>',
  variables: ['firstName'],
});

Parameters

FieldTypeDescription
namestringRename the template
subjectstringNew subject line
htmlBodystringNew HTML body
textBodystringNew text body
variablesstring[]Updated variable list
metadataobjectUpdated metadata
status'active' | 'archived'Archive or reactivate

Archive a Template

ts
await outbound.templates.update('template-uuid', { status: 'archived' });

Archived templates cannot be used for sending. Reactivate with { status: 'active' }.

SES Sync

When you update subject, htmlBody, or textBody, the template is automatically re-synced to AWS SES. No manual action needed.


Delete Template

ts
const result = await outbound.templates.delete('template-uuid');
// { message: 'Template deleted', id: 'template-uuid' }

This also deletes the template from AWS SES. This action is irreversible.

Possible Errors

ErrorStatusCause
NotFoundError404Template doesn't exist

Duplicate Template

Create a copy of an existing template:

ts
// With auto-generated name ("Copy of welcome-email")
const { template } = await outbound.templates.duplicate('template-uuid');

// With custom name
const { template } = await outbound.templates.duplicate('template-uuid', {
  name: 'welcome-email-v2',
});

The duplicate is a completely independent template with its own ID, use_count: 0, and a new SES template. It counts against your template quota.

Possible Errors

ErrorStatusCause
NotFoundError404Source template doesn't exist
ForbiddenError403Template quota exceeded
ConflictError409Name already exists

Preview Template

Render a template with sample variables without sending — useful for testing in your UI:

ts
const preview = await outbound.templates.preview('template-uuid', {
  variables: { firstName: 'John', company: 'Acme' },
});

console.log(preview.subject);            // "Welcome John!"
console.log(preview.htmlBody);           // "<h1>Hello John</h1><p>Welcome to Acme...</p>"
console.log(preview.missingVariables);   // [] — or ['dashboardUrl'] if not provided

Parameters

FieldTypeRequiredDescription
variablesRecord<string, string>NoVariable values to substitute

Response

ts
{
  subject: 'Welcome John!',
  htmlBody: '<h1>Hello John</h1><p>Welcome to Acme.</p>',
  textBody: 'Hello John, Welcome to Acme.',
  missingVariables: ['dashboardUrl']   // variables in template but not provided
}

TIP

Use missingVariables to validate that your code is passing all required variables before sending.


Send Email with Template

Single Recipient

ts
const { jobId, messageId } = await outbound.templates.send({
  templateId: 'template-uuid',
  toEmail: 'user@example.com',
  fromEmail: 'noreply@yourcompany.com',
  senderName: 'YourCompany',
  replyTo: 'support@yourcompany.com',
  variables: {
    firstName: 'John',
    company: 'Acme',
    dashboardUrl: 'https://app.acme.com/dashboard',
  },
  priority: 'high',
  idempotencyKey: 'welcome-john-123',
  metadata: { userId: 'u_123' },
  tracking: { opens: true, clicks: true },
});

Parameters

FieldTypeRequiredDescription
templateIdstringYesTemplate ID to use
toEmailstringYesRecipient email
fromEmailstringYesSender email (verified domain)
variablesRecord<string, string>NoTemplate variable values
senderNamestringNoDisplay name
replyTostringNoReply-to address
ccEmailstringNoCC recipient
bccEmailstringNoBCC recipient
priority'low' | 'normal' | 'high'NoDefault: normal
idempotencyKeystringNoPrevents duplicate sends — expires after 24 hours
campaignIdstringNoTag this send as part of a campaign — enables bulk cancellation via email.cancel()
metadataobjectNoCustom metadata
headersobjectNoCustom email headers
trackingobjectNoOpen/click tracking config

Response

ts
{
  message: 'Email queued successfully',
  jobId: 'uuid',
  messageId: 'uuid'
}

Possible Errors

ErrorStatusCause
BadRequestError400Missing templateId, toEmail, or fromEmail
NotFoundError404Template doesn't exist
ForbiddenError403Template is archived, domain not verified
RateLimitError429Per-second rate limit exceeded, or the global AWS SES daily sending quota is exhausted ("AWS SES daily sending quota exhausted (X/Y). Try again later.")

Idempotency for Template Sends

Idempotency works identically for template and non-template sends — keys expire after 48 hours, in-flight duplicates return 409, and completed duplicates return the exact cached response.

ts
import { createHash } from 'crypto';

// Single template send: combine template + purpose + recipient
const key = createHash('sha256')
  .update(`welcome-email:${userId}:${userEmail}`)
  .digest('hex');

await outbound.templates.send({
  templateId: 'template-uuid',
  toEmail: userEmail,
  fromEmail: 'noreply@company.com',
  variables: { firstName: 'Alice' },
  idempotencyKey: key,
});

// Bulk template send: one key for the entire batch
const bulkKey = createHash('sha256')
  .update(`march-newsletter:batch-1`)
  .digest('hex');

await outbound.templates.bulkSend({
  templateId: 'newsletter-template',
  fromEmail: 'news@company.com',
  idempotencyKey: bulkKey,
  recipients: [
    { toEmail: 'alice@example.com', variables: { firstName: 'Alice' } },
    { toEmail: 'bob@example.com', variables: { firstName: 'Bob' } },
  ],
});

TIP

There is no difference in how idempotency keys work between template and non-template sends. Keys expire after 24 hours. The same rules apply — see the Email Idempotency guide for full details.


Bulk Send with Template

Send the same template to up to 500 recipients with per-recipient variables. This uses AWS SES SendBulkEmailCommand for maximum throughput.

ts
const result = await outbound.templates.bulkSend({
  templateId: 'template-uuid',
  fromEmail: 'noreply@yourcompany.com',
  senderName: 'YourCompany',
  replyTo: 'support@yourcompany.com',
  idempotencyKey: 'march-newsletter-2026',
  recipients: [
    {
      toEmail: 'alice@example.com',
      variables: { firstName: 'Alice', company: 'Acme' },
    },
    {
      toEmail: 'bob@example.com',
      variables: { firstName: 'Bob', company: 'Acme' },
      ccEmail: 'bob-assistant@example.com',
    },
    {
      toEmail: 'charlie@example.com',
      variables: { firstName: 'Charlie', company: 'Acme' },
      metadata: { segment: 'enterprise' },
    },
  ],
});

Parameters

FieldTypeRequiredDescription
templateIdstringYesTemplate ID
fromEmailstringYesSender email (verified domain)
recipientsTemplateBulkRecipient[]YesArray of recipients (max 500)
senderNamestringNoDisplay name
replyTostringNoReply-to address
idempotencyKeystringNoPrevents duplicate bulk sends — expires after 24 hours
campaignIdstringNoTag this batch as part of a campaign — enables bulk cancellation via email.cancel()

Recipient Object

FieldTypeRequiredDescription
toEmailstringYesRecipient email address
variablesRecord<string, string>NoPer-recipient variable values
ccEmailstringNoCC for this recipient
bccEmailstringNoBCC for this recipient
metadataobjectNoPer-recipient metadata

Response

ts
{
  message: 'Bulk email queued successfully',
  jobId: 'uuid',
  recipientCount: 487,
  suppressedCount: 10,
  suppressedEmails: ['bounced@example.com', 'spam@example.com'],  // absent on duplicate responses
  duplicatesRemoved: 3,
  usingSesTemplate: true   // confirms SES bulk template optimization is active
}

suppressedEmails on duplicate responses

When an idempotencyKey is replayed, the response returns duplicate: true but omits suppressedEmails — only the count is cached. Always default the field:

ts
const { suppressedEmails = [], suppressedCount } = await outbound.templates.bulkSend({ ... });

Possible Errors

ErrorStatusCause
BadRequestError400Empty recipients, exceeds 500 limit
NotFoundError404Template doesn't exist
ForbiddenError403Template archived, domain not verified
ConflictError409Idempotency key is currently being processed by another request
RateLimitError429Per-second rate limit exceeded, or the global AWS SES daily sending quota is exhausted ("AWS SES daily sending quota exhausted (X/Y). Try again later.")

All-suppressed batches still succeed

If every recipient is on the suppression list, the request still returns 202 Accepted with a jobId. recipientCount is 0, and each suppressed address is persisted as a dropped recipient row — query email.status(jobId) to audit them.

SES Template Optimization

When using bulkSend, the platform uses AWS SES's SendBulkEmailCommand which sends up to 50 recipients per API call instead of one-at-a-time. This is significantly faster for large batches. The usingSesTemplate: true field in the response confirms this optimization is active.


Template Statistics

Get an overview of your template usage:

ts
const stats = await outbound.templates.stats();

Response

ts
{
  quota: {
    used: 45,
    allocated: 1000,
    remaining: 955
  },
  counts: {
    total: 45,
    active: 43,
    archived: 2,
    neverUsed: 5
  },
  totalEmailsSentViaTemplates: 150000,
  mostUsed: [
    { id: 'uuid', name: 'welcome-email', use_count: 50000, ... },
    // top 5 by use_count
  ],
  leastUsed: [
    { id: 'uuid', name: 'old-promo', use_count: 2, ... },
    // bottom 5 by use_count
  ],
  recentlyUsed: [
    { id: 'uuid', name: 'march-newsletter', last_sent_at: '2026-03-08T...', ... },
    // 5 most recently sent
  ]
}

Complete Example: Template Lifecycle

ts
import { Outbound, NotFoundError } from '@masters-union/outbound-sdk';

const outbound = new Outbound({ apiKey: 'mu_outbound_...' });

// 1. Create
const { template } = await outbound.templates.create({
  name: 'order-confirmation',
  subject: 'Order #{{orderId}} Confirmed',
  htmlBody: `
    <h1>Thank you, {{customerName}}!</h1>
    <p>Your order <strong>#{{orderId}}</strong> has been confirmed.</p>
    <p>Total: {{orderTotal}}</p>
    <p><a href="{{trackingUrl}}">Track your order</a></p>
  `,
  variables: ['customerName', 'orderId', 'orderTotal', 'trackingUrl'],
});

// 2. Preview before sending
const preview = await outbound.templates.preview(template.id, {
  variables: {
    customerName: 'Jane',
    orderId: '1234',
    orderTotal: '$99.99',
    trackingUrl: 'https://track.example.com/1234',
  },
});
console.log('Preview subject:', preview.subject);
// "Order #1234 Confirmed"

// 3. Send to one customer
await outbound.templates.send({
  templateId: template.id,
  toEmail: 'jane@example.com',
  fromEmail: 'orders@yourcompany.com',
  senderName: 'YourCompany Orders',
  variables: {
    customerName: 'Jane',
    orderId: '1234',
    orderTotal: '$99.99',
    trackingUrl: 'https://track.example.com/1234',
  },
});

// 4. Bulk send to many customers
const bulkResult = await outbound.templates.bulkSend({
  templateId: template.id,
  fromEmail: 'orders@yourcompany.com',
  senderName: 'YourCompany Orders',
  recipients: orders.map(order => ({
    toEmail: order.customerEmail,
    variables: {
      customerName: order.customerName,
      orderId: order.id,
      orderTotal: order.total,
      trackingUrl: `https://track.example.com/${order.id}`,
    },
  })),
});

console.log(`Sent to ${bulkResult.recipientCount} customers`);

// 5. Check stats
const stats = await outbound.templates.stats();
console.log(`Template quota: ${stats.quota.used}/${stats.quota.allocated}`);

// 6. Archive when no longer needed
await outbound.templates.update(template.id, { status: 'archived' });

Released under the MIT License.