Guide · AWS SES v2

MCP Server AWS SES Configuration Sets — event destinations, dedicated IPs

A configuration set is the control plane for every email sent from an MCP server tool: it determines where delivery event notifications go, which IP pool sends the mail, whether the account suppression list is enforced, and what tracking features are enabled. Every production SES send should reference a configuration set via the ConfigurationSetName parameter of SendEmailCommand. Without one, you are sending blind — no feedback on delivery, opens, or clicks, and no way to route bounce/complaint events to your processing Lambda. The two most critical event destinations to wire are SNS (for real-time bounce and complaint feedback) and EventBridge (for structured delivery analytics). The dedicated IP pool decision determines your sending reputation isolation — transactional and marketing mail must never share an IP pool.

TL;DR

Create a configuration set with CreateConfigurationSetCommand. Add event destinations with CreateConfigurationSetEventDestinationCommand — use SNS for bounce/complaint/delivery events (Lambda processor), EventBridge for structured analytics. Attach the set to sends via ConfigurationSetName in SendEmailCommand. Isolate transactional from marketing sends via separate sets + dedicated IP pools (DedicatedIpPoolName). Override suppression list enforcement per-set with SuppressionOptions.SuppressedReasons — but only do this for transactional (never marketing).

Creating a configuration set

import {
  SESv2Client,
  CreateConfigurationSetCommand,
  CreateConfigurationSetEventDestinationCommand,
  GetConfigurationSetCommand,
  ListConfigurationSetsCommand,
  DeleteConfigurationSetCommand,
  EventType,
} from '@aws-sdk/client-sesv2';

const ses = new SESv2Client({ region: process.env.AWS_REGION ?? 'us-east-1' });

// Create a configuration set for transactional email
await ses.send(new CreateConfigurationSetCommand({
  ConfigurationSetName: 'transactional-alerts',
  TrackingOptions: {
    // CustomRedirectDomain for custom click/open tracking domain
    // e.g. 'click.alivemcp.com' — requires CNAME in DNS
    // CustomRedirectDomain: 'click.alivemcp.com',
  },
  DeliveryOptions: {
    TlsPolicy: 'REQUIRE', // require TLS for outbound SMTP; default is 'OPTIONAL'
    // DedicatedIpPoolName: 'transactional-pool', // dedicated IP pool for this set
  },
  ReputationOptions: {
    ReputationMetricsEnabled: true, // exposes BounceRate/ComplaintRate in CloudWatch
    LastFreshStart: new Date(), // reset the reputation window (use sparingly)
  },
  SendingOptions: {
    SendingEnabled: true, // set to false to pause sending without deleting the set
  },
  SuppressionOptions: {
    SuppressedReasons: [
      'BOUNCE',     // SuppressionListReason.BOUNCE — suppress addresses that hard-bounce
      'COMPLAINT',  // SuppressionListReason.COMPLAINT — suppress addresses that complain
    ],
    // To disable suppression for critical transactional (password reset, etc.):
    // SuppressedReasons: [] — but then you must manage suppression entirely yourself
  },
}));

Configuration set names must be unique per region per account and are case-sensitive. Use a naming convention that separates stream types: transactional-alerts, transactional-auth, marketing-weekly-digest. Never send marketing email through a transactional configuration set — a complaint from a marketing send will suppress addresses that might have wanted your password-reset email.

Adding SNS event destination for bounce and complaint processing

An SNS event destination publishes SES events to an SNS topic as JSON. Subscribe a Lambda to the topic to process events in real time. This is the standard architecture for bounce and complaint processing — see the suppression and bounce guide for the Lambda handler code.

// First create the SNS topic (or use an existing one):
// aws sns create-topic --name ses-feedback-transactional

const SNS_TOPIC_ARN = 'arn:aws:sns:us-east-1:123456789012:ses-feedback-transactional';

await ses.send(new CreateConfigurationSetEventDestinationCommand({
  ConfigurationSetName: 'transactional-alerts',
  EventDestinationName: 'sns-feedback-bounce-complaint',
  EventDestination: {
    Enabled: true,
    MatchingEventTypes: [
      EventType.BOUNCE,
      EventType.COMPLAINT,
      EventType.DELIVERY,
      // Other available types:
      // EventType.SEND         — message accepted by SES (before delivery attempt)
      // EventType.REJECT       — rejected before send (SES-level, not SMTP bounce)
      // EventType.OPEN         — recipient opened HTML email (requires pixel tracking)
      // EventType.CLICK        — recipient clicked a tracked link (requires tracking domain)
      // EventType.RENDERING_FAILURE — template variable substitution failed
      // EventType.DELIVERY_DELAY    — delivery delayed (grey-listing, server retry)
      // EventType.SUBSCRIPTION      — recipient updated their subscription preferences
    ],
    SnsDestination: {
      TopicArn: SNS_TOPIC_ARN,
    },
  },
}));

SES requires the SNS topic to be in the same region as the configuration set. SES must have SNS:Publish permission on the topic — add a topic resource policy granting ses.amazonaws.com publish rights, or attach the topic via the SES console which sets up the policy automatically.

// SNS topic resource policy required for SES to publish:
{
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "ses.amazonaws.com" },
    "Action": "SNS:Publish",
    "Resource": "arn:aws:sns:us-east-1:123456789012:ses-feedback-transactional",
    "Condition": {
      "StringEquals": {
        "AWS:SourceAccount": "123456789012"
      },
      "ArnLike": {
        "AWS:SourceArn": "arn:aws:ses:us-east-1:123456789012:configuration-set/transactional-alerts"
      }
    }
  }]
}

Adding EventBridge event destination for analytics

An EventBridge event destination publishes SES events to an EventBridge event bus as structured JSON. Use this for delivery funnel analytics — events arrive as detail-type: "SES Message Events" on the default event bus or a custom bus. EventBridge rules can then route events to CloudWatch Metrics, Firehose for S3 archival, or a Lambda for real-time dashboarding.

await ses.send(new CreateConfigurationSetEventDestinationCommand({
  ConfigurationSetName: 'transactional-alerts',
  EventDestinationName: 'eventbridge-analytics',
  EventDestination: {
    Enabled: true,
    MatchingEventTypes: [
      EventType.SEND,
      EventType.DELIVERY,
      EventType.BOUNCE,
      EventType.COMPLAINT,
      EventType.OPEN,
      EventType.CLICK,
      EventType.DELIVERY_DELAY,
    ],
    EventBridgeDestination: {
      EventBusArn: 'arn:aws:events:us-east-1:123456789012:event-bus/default',
      // Or a custom event bus:
      // EventBusArn: 'arn:aws:events:us-east-1:123456789012:event-bus/ses-analytics',
    },
  },
}));

// EventBridge rule to capture all SES events:
// {
//   "source": ["aws.ses"],
//   "detail-type": ["SES Message Events"]
// }
//
// Event detail structure includes:
// {
//   "eventType": "Delivery",
//   "mail": { "messageId": "...", "destination": ["..."], "tags": {...} },
//   "delivery": { "timestamp": "...", "processingTimeMillis": 450, "smtpResponse": "250 ok" }
// }

Dedicated IP pools: isolation for transactional vs marketing

SES uses shared IP addresses by default. Any other SES customer who sends bulk marketing mail on shared IPs can damage the pool's reputation, causing your transactional emails to land in spam. Dedicated IPs eliminate this — you own the IP, its reputation is determined solely by your sending behavior.

Dedicated IPs require a minimum commitment and cost $24.95/month per IP. Use dedicated IP pools to group IPs by send stream. Create separate pools for transactional and marketing mail, then associate each pool with a configuration set via DeliveryOptions.DedicatedIpPoolName.

import {
  CreateDedicatedIpPoolCommand,
  PutDedicatedIpInPoolCommand,
  GetDedicatedIpPoolCommand,
  ListDedicatedIpPoolsCommand,
  ScalingMode,
} from '@aws-sdk/client-sesv2';

// Create a dedicated IP pool for transactional mail
await ses.send(new CreateDedicatedIpPoolCommand({
  PoolName: 'transactional-pool',
  ScalingMode: ScalingMode.STANDARD, // STANDARD (manual IP management) or MANAGED (auto-scale)
  Tags: [{ Key: 'stream', Value: 'transactional' }],
}));

// MANAGED pools automatically add/remove IPs based on volume — preferred for variable workloads
await ses.send(new CreateDedicatedIpPoolCommand({
  PoolName: 'marketing-pool',
  ScalingMode: ScalingMode.MANAGED,
}));

// Associate pool with a configuration set:
await ses.send(new CreateConfigurationSetCommand({
  ConfigurationSetName: 'marketing-weekly',
  DeliveryOptions: {
    TlsPolicy: 'REQUIRE',
    DedicatedIpPoolName: 'marketing-pool',
  },
  SuppressionOptions: {
    SuppressedReasons: ['BOUNCE', 'COMPLAINT'],
  },
}));

// List dedicated IPs in a pool
import { ListDedicatedIpsCommand } from '@aws-sdk/client-sesv2';
const ips = await ses.send(new ListDedicatedIpsCommand({ PoolName: 'transactional-pool' }));
for (const ip of ips.DedicatedIps ?? []) {
  console.log({
    ip: ip.Ip,
    warmupStatus: ip.WarmupStatus, // 'IN_PROGRESS' | 'DONE'
    warmupPercentage: ip.WarmupPercentage,
  });
  // New dedicated IPs must warm up — sending high volume on cold IPs triggers spam filters
  // SES auto-warms new IPs by routing a small % of traffic through them initially
}

Suppression override for critical transactional email

The default suppression behavior blocks sends to hard-bounce and complaint addresses. For most mail this is correct. The exception is security-critical transactional mail — if a user changes their password and the new address generates a hard bounce (they mistyped it), you still want them to receive the confirmation email to discover the error. Set SuppressedReasons: [] on the configuration set for this stream.

// Configuration set for security-critical transactional (password reset, 2FA, account alerts)
// No suppression — always attempt delivery so the user learns their address is wrong
await ses.send(new CreateConfigurationSetCommand({
  ConfigurationSetName: 'security-critical',
  DeliveryOptions: {
    TlsPolicy: 'REQUIRE',
    DedicatedIpPoolName: 'transactional-pool',
  },
  SuppressionOptions: {
    SuppressedReasons: [],
    // Empty array = suppression list NOT enforced for this configuration set
    // WARNING: Use only for security-critical transactional — never for marketing sends
  },
}));

// When sending a password-reset email, use the security-critical set:
await ses.send(new SendEmailCommand({
  FromEmailAddress: 'security@alivemcp.com',
  Destination: { ToAddresses: [userEmail] },
  Content: { Template: { TemplateName: 'password-reset-v2', TemplateData: JSON.stringify(data) } },
  ConfigurationSetName: 'security-critical', // bypasses suppression list
}));

Event type reference

EventTypeTriggerKey destination
SENDSES accepted the message; delivery not yet attemptedEventBridge (volume tracking)
REJECTSES rejected message before delivery (content policy)SNS (alert engineering)
DELIVERYSMTP server confirmed delivery (250 response)EventBridge (delivery funnel)
BOUNCEHard or soft bounce from receiving serverSNS (Lambda → suppression list + DB)
COMPLAINTRecipient marked as spam via feedback loopSNS (Lambda → suppression list + DB)
OPENRecipient opened HTML email (1×1 tracking pixel loaded)EventBridge (engagement analytics)
CLICKRecipient clicked a tracked link (requires CustomRedirectDomain)EventBridge (engagement analytics)
RENDERING_FAILURETemplate variable substitution failed (missing required var)SNS (alert engineering)
DELIVERY_DELAYDelivery attempted but delayed (server busy, greylisting)EventBridge (SLA monitoring)
SUBSCRIPTIONRecipient updated subscription preferences via SES list managementLambda (sync subscription state)

Reading configuration set status

// Get a configuration set and all its event destinations
const config = await ses.send(new GetConfigurationSetCommand({
  ConfigurationSetName: 'transactional-alerts',
}));

console.log({
  name: config.ConfigurationSetName,
  sendingEnabled: config.SendingOptions?.SendingEnabled,
  tlsPolicy: config.DeliveryOptions?.TlsPolicy,
  dedicatedPool: config.DeliveryOptions?.DedicatedIpPoolName,
  suppressedReasons: config.SuppressionOptions?.SuppressedReasons,
});

// List event destinations for the set
import { GetConfigurationSetEventDestinationsCommand } from '@aws-sdk/client-sesv2';

const destinations = await ses.send(new GetConfigurationSetEventDestinationsCommand({
  ConfigurationSetName: 'transactional-alerts',
}));

for (const dest of destinations.EventDestinations ?? []) {
  console.log({
    name: dest.Name,
    enabled: dest.Enabled,
    matchingEventTypes: dest.MatchingEventTypes,
    snsTopicArn: dest.SnsDestination?.TopicArn,
    eventBusArn: dest.EventBridgeDestination?.EventBusArn,
  });
}