SendboundSendbound

Migrate from AWS SES

Step-by-step guide to moving from Amazon Simple Email Service (SES) to Sendbound. SDK replacement, IAM cleanup, SNS notification migration, and SMTP relay removal.

This guide covers everything specific to migrating from Amazon Simple Email Service (SES).

Before starting, read the general migration guide for the parallel-run strategy and DNS playbook.


What's different

AWS SESSendbound
AuthIAM credentials (access key + secret) or SMTP passwordBearer token (sk_live_xxx)
SDK@aws-sdk/client-ses or SMTP relayPlain fetch — no SDK
Region configRequired (us-east-1, etc.)Not required
TemplatesSES Email Templates (limited) or app-side renderingFull template editor with live preview
Event notificationsSNS topics → SQS / LambdaHTTPS webhook endpoint
SuppressionAccount-level suppression listPer-project suppression list
Sandbox modeSES sandbox restricts recipientsNo sandbox — verify domain and send freely

If you're on SES sandbox, you're restricted to verified email addresses only. With Sendbound you can send to any address as soon as your domain is verified — no sandbox to escape.


Export from AWS SES

Document verified identities

aws ses list-identities --identity-type Domain
aws ses list-identities --identity-type EmailAddress

Note which domains and addresses you send from — you'll verify the same domains in Sendbound.

Export SES templates

# List all templates
aws ses list-templates

# Export each template
aws ses get-template --template-name YOUR_TEMPLATE_NAME

The response includes HtmlPart and TextPart — save these as HTML files to import into Sendbound's template editor.

Export the account-level suppression list

aws sesv2 list-suppressed-destinations \
  --query 'SuppressedDestinationSummaries[*].EmailAddress' \
  --output text \
  > suppressions.txt

Document SNS notification configuration

For each verified identity:

aws ses get-identity-notification-attributes \
  --identities yourdomain.com

Note the SNS topic ARNs for bounces, complaints, and deliveries. You'll replace these with Sendbound webhooks.


API migration

Sending a transactional email

import { SESClient, SendEmailCommand } from '@aws-sdk/client-ses';

const ses = new SESClient({ region: 'us-east-1' });

await ses.send(new SendEmailCommand({
  Source: 'hello@yourdomain.com',
  Destination: {
    ToAddresses: ['user@example.com'],
  },
  Message: {
    Subject: { Data: 'Welcome', Charset: 'UTF-8' },
    Body: {
      Html: { Data: '<p>Welcome aboard!</p>', Charset: 'UTF-8' },
      Text: { Data: 'Welcome aboard!', Charset: 'UTF-8' },
    },
  },
}));
import nodemailer from 'nodemailer';

const transporter = nodemailer.createTransport({
  host: 'email-smtp.us-east-1.amazonaws.com',
  port: 587,
  auth: {
    user: process.env.SES_SMTP_USERNAME,
    pass: process.env.SES_SMTP_PASSWORD,
  },
});

await transporter.sendMail({
  from: 'hello@yourdomain.com',
  to: 'user@example.com',
  subject: 'Welcome',
  html: '<p>Welcome aboard!</p>',
});
// No SDK, no SMTP config, no region
await fetch('https://api.sendbound.com/v1/send', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.SENDBOUND_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    to: 'user@example.com',
    from: 'hello@yourdomain.com',
    subject: 'Welcome',
    html: '<p>Welcome aboard!</p>',
    text: 'Welcome aboard!',
  }),
});

Using a stored template

import { SendTemplatedEmailCommand } from '@aws-sdk/client-ses';

await ses.send(new SendTemplatedEmailCommand({
  Source: 'hello@yourdomain.com',
  Destination: { ToAddresses: ['user@example.com'] },
  Template: 'WelcomeEmail',
  TemplateData: JSON.stringify({
    name: 'Jane',
    confirm_url: 'https://app.example.com/confirm',
  }),
}));
await fetch('https://api.sendbound.com/v1/send', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.SENDBOUND_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    to: 'user@example.com',
    from: 'hello@yourdomain.com',
    templateId: 'tmpl_your_id',
    variables: {
      name: 'Jane',
      confirm_url: 'https://app.example.com/confirm',
    },
  }),
});

SNS notification migration

SES uses SNS topics to deliver bounce, complaint, and delivery notifications to your application. Sendbound replaces this entire stack with a single HTTPS webhook.

What you're replacing

SES → SNS Topic → SQS Queue → Lambda/Worker → Your handler

What you get with Sendbound

Sendbound → HTTPS POST → Your endpoint

Update your bounce/complaint handler

// Lambda function triggered by SNS → SQS
export async function handler(event: SQSEvent) {
  for (const record of event.Records) {
    const snsMessage = JSON.parse(record.body);
    const sesNotification = JSON.parse(snsMessage.Message);

    if (sesNotification.notificationType === 'Bounce') {
      const emails = sesNotification.bounce.bouncedRecipients.map(r => r.emailAddress);
      await markAsBounced(emails);
    }

    if (sesNotification.notificationType === 'Complaint') {
      const emails = sesNotification.complaint.complainedRecipients.map(r => r.emailAddress);
      await markAsComplained(emails);
    }
  }
}
// Express handler at your webhook endpoint
import crypto from 'crypto';

app.post('/webhooks/sendbound', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-sendbound-signature'] as string;
  const expected = crypto.createHmac('sha256', process.env.SENDBOUND_WEBHOOK_SECRET!)
    .update(req.body).digest('hex');

  if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    return res.status(401).send('Unauthorized');
  }

  const event = JSON.parse(req.body.toString());

  if (event.type === 'email.bounced') {
    await markAsBounced([event.data.to]);
  }

  if (event.type === 'email.complained') {
    await markAsComplained([event.data.to]);
  }

  res.status(200).send('ok');
});

IAM cleanup

After cutover, clean up the SES-specific AWS resources:

# Remove SES send permissions from the IAM role/user
aws iam detach-user-policy \
  --user-name your-ses-user \
  --policy-arn arn:aws:iam::aws:policy/AmazonSESFullAccess

# Delete the SMTP credentials (IAM user with ses:SendRawEmail)
aws iam delete-access-key \
  --user-name your-ses-smtp-user \
  --access-key-id AKIAIOSFODNN7EXAMPLE

# Delete SNS topics used for SES notifications
aws sns delete-topic --topic-arn arn:aws:sns:us-east-1:123456789:ses-bounces
aws sns delete-topic --topic-arn arn:aws:sns:us-east-1:123456789:ses-complaints

Checklist

  • Documented all verified SES identities/domains
  • Exported SES templates via AWS CLI
  • Exported account-level suppression list
  • Documented SNS topic ARNs + Lambda handlers
  • Added and verified sending domain in Sendbound
  • Imported suppression list
  • Imported templates into Sendbound template editor
  • Replaced AWS SDK / SMTP with fetch to Sendbound API
  • Set SENDBOUND_API_KEY in environment
  • Added Sendbound webhook endpoint and updated handler (replacing SNS/SQS/Lambda)
  • Ran parallel test (SES + Sendbound simultaneously)
  • Cut over to Sendbound only
  • Deleted SNS topics and SQS queues
  • Removed IAM permissions and SMTP credentials
  • Uninstalled @aws-sdk/client-ses from package.json