Skip to main content
Technology & EngineeringEmail Services243 lines

AWS Ses

Send email at scale with Amazon SES (Simple Email Service). Use this skill when

Quick Summary26 lines
You are an email operations specialist who integrates Amazon SES into projects. SES
is the most cost-effective option for high-volume sending, offering raw infrastructure
with full control over deliverability, reputation, and compliance.

## Key Points

- Can only send to verified addresses
- Daily limit of 200 emails, 1/second rate
- Must request production access through AWS console
- Always use configuration sets — one for transactional, one for marketing
- Enable account-level suppression list to prevent sending to known bad addresses
- Use dedicated IPs only if sending 100K+ emails/day consistently
- Set up a custom MAIL FROM domain for full authentication alignment
- Publish events to SNS for real-time processing and to Firehose for analytics
- Monitor SES reputation dashboard and set CloudWatch alarms on bounce/complaint rates
- Use SES v2 API — v1 is legacy
- Sending without a configuration set — no tracking or suppression
- Not handling SNS subscription confirmation — events silently fail

## Quick Example

```bash
npm install @aws-sdk/client-sesv2
```
skilldb get email-services-skills/AWS SesFull skill: 243 lines
Paste into your CLAUDE.md or agent config

AWS SES Email Integration

You are an email operations specialist who integrates Amazon SES into projects. SES is the most cost-effective option for high-volume sending, offering raw infrastructure with full control over deliverability, reputation, and compliance.

Core Philosophy

Infrastructure, not abstraction

SES gives you email-sending infrastructure, not a product. You build the abstractions: template management, suppression handling, analytics, and compliance. This is its strength for teams that need full control and its weakness for teams that want a turnkey solution.

Configuration sets are non-negotiable

Every email should be sent through a configuration set. They control tracking, event publishing, suppression, and sending identity. Skipping them means flying blind.

Cost scales linearly

SES charges $0.10 per 1,000 emails. At volume, this is 10-50x cheaper than Resend, SendGrid, or Postmark. But you pay in engineering time for features those services include by default.

Setup

Install

npm install @aws-sdk/client-sesv2

Initialize

import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2';

const ses = new SESv2Client({
  region: process.env.AWS_REGION || 'us-east-1',
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
  },
});

Key Techniques

Simple send

await ses.send(new SendEmailCommand({
  FromEmailAddress: 'App <noreply@yourdomain.com>',
  Destination: {
    ToAddresses: ['user@example.com'],
  },
  Content: {
    Simple: {
      Subject: { Data: 'Your password reset link' },
      Body: {
        Html: { Data: '<p>Click <a href="...">here</a></p>' },
        Text: { Data: 'Reset: https://...' },
      },
    },
  },
  ConfigurationSetName: 'transactional',
  EmailTags: [
    { Name: 'category', Value: 'security' },
    { Name: 'workflow', Value: 'password_reset' },
  ],
}));

Template send

Create templates in SES, then send with template data.

import { CreateEmailTemplateCommand } from '@aws-sdk/client-sesv2';

// Create template (one-time)
await ses.send(new CreateEmailTemplateCommand({
  TemplateName: 'PasswordReset',
  TemplateContent: {
    Subject: 'Reset your password',
    Html: '<p>Hi {{name}}, click <a href="{{resetUrl}}">here</a> to reset.</p>',
    Text: 'Hi {{name}}, reset your password: {{resetUrl}}',
  },
}));

// Send with template
import { SendEmailCommand } from '@aws-sdk/client-sesv2';

await ses.send(new SendEmailCommand({
  FromEmailAddress: 'App <noreply@yourdomain.com>',
  Destination: { ToAddresses: ['user@example.com'] },
  Content: {
    Template: {
      TemplateName: 'PasswordReset',
      TemplateData: JSON.stringify({ name: 'Alice', resetUrl: 'https://...' }),
    },
  },
  ConfigurationSetName: 'transactional',
}));

Bulk template send

Send personalized email to many recipients efficiently.

import { SendBulkEmailCommand } from '@aws-sdk/client-sesv2';

await ses.send(new SendBulkEmailCommand({
  FromEmailAddress: 'App <noreply@yourdomain.com>',
  DefaultContent: {
    Template: {
      TemplateName: 'WeeklySummary',
      TemplateData: JSON.stringify({ defaultGreeting: 'Hello' }),
    },
  },
  BulkEmailEntries: [
    {
      Destination: { ToAddresses: ['user1@example.com'] },
      ReplacementEmailContent: {
        ReplacementTemplate: {
          ReplacementTemplateData: JSON.stringify({ name: 'Alice' }),
        },
      },
    },
    // ... more entries (up to 50 per call)
  ],
  ConfigurationSetName: 'bulk',
}));

Configuration sets

import { CreateConfigurationSetCommand } from '@aws-sdk/client-sesv2';

await ses.send(new CreateConfigurationSetCommand({
  ConfigurationSetName: 'transactional',
  TrackingOptions: { CustomRedirectDomain: 'track.yourdomain.com' },
  DeliveryOptions: { TlsPolicy: 'REQUIRE' },
  ReputationOptions: { ReputationMetricsEnabled: true },
  SendingOptions: { SendingEnabled: true },
  SuppressionOptions: {
    SuppressedReasons: ['BOUNCE', 'COMPLAINT'],
  },
}));

Event destinations (SNS, CloudWatch, Firehose, EventBridge)

import { CreateConfigurationSetEventDestinationCommand } from '@aws-sdk/client-sesv2';

await ses.send(new CreateConfigurationSetEventDestinationCommand({
  ConfigurationSetName: 'transactional',
  EventDestinationName: 'sns-events',
  EventDestination: {
    Enabled: true,
    MatchingEventTypes: ['SEND', 'DELIVERY', 'BOUNCE', 'COMPLAINT', 'REJECT'],
    SnsDestination: { TopicArn: 'arn:aws:sns:us-east-1:123456:ses-events' },
  },
}));

Event Processing

SES publishes events to SNS, CloudWatch, Kinesis Firehose, or EventBridge.

EventAction
DELIVERYMark delivered
BOUNCE (Permanent)Suppress address
BOUNCE (Transient)Log, monitor
COMPLAINTSuppress immediately
REJECTLog — SES rejected before sending
RENDERING_FAILUREFix template — data mismatch

SNS webhook handler

export async function POST(req: Request) {
  const body = await req.json();

  // Handle SNS subscription confirmation
  if (body.Type === 'SubscriptionConfirmation') {
    await fetch(body.SubscribeURL);
    return new Response('OK');
  }

  const message = JSON.parse(body.Message);
  const eventType = message.eventType;
  const recipient = message.mail?.destination?.[0];

  switch (eventType) {
    case 'Bounce':
      if (message.bounce.bounceType === 'Permanent') {
        await suppressAddress(recipient, 'hard_bounce');
      }
      break;
    case 'Complaint':
      await suppressAddress(recipient, 'complaint');
      break;
  }

  return new Response('OK');
}

Sandbox vs Production

New SES accounts start in sandbox mode:

  • Can only send to verified addresses
  • Daily limit of 200 emails, 1/second rate
  • Must request production access through AWS console

Request production access with: sending domain verified, SPF/DKIM configured, bounce/complaint handling implemented, and a clear use case description.

Best Practices

  • Always use configuration sets — one for transactional, one for marketing
  • Enable account-level suppression list to prevent sending to known bad addresses
  • Use dedicated IPs only if sending 100K+ emails/day consistently
  • Set up a custom MAIL FROM domain for full authentication alignment
  • Publish events to SNS for real-time processing and to Firehose for analytics
  • Monitor SES reputation dashboard and set CloudWatch alarms on bounce/complaint rates
  • Use SES v2 API — v1 is legacy

Anti-Patterns

  • Sending without a configuration set — no tracking or suppression
  • Not handling SNS subscription confirmation — events silently fail
  • Using shared IPs for both transactional and marketing at high volume
  • Ignoring the SES sandbox — production access request takes 24-48 hours
  • Not implementing exponential backoff on throttling errors
  • Storing AWS credentials in application code instead of using IAM roles

Install this skill directly: skilldb add email-services-skills

Get CLI access →