Migrate/Amazon SES
AWS to standalone API

Switch from Amazon SES to Anypost

SES requires AWS credentials, region-specific endpoints, deeply nested JSON, and SNS for webhooks. Anypost replaces all of that with a single API key and a flat REST API.

~60 mintypical migration time
4structural changes
8official SDK languages

What's the same, what changed

SES is AWS-native: you authenticate with IAM, call region-specific endpoints, and route events through SNS. Anypost is a standalone email API, with one key, one URL, and direct HTTP webhooks.

ConceptAmazon SESAnypost
AuthAWS Signature v4 (AWS SDK / IAM)Authorization: Bearer ap_…
CredentialsAccess key ID + secret access keyA single ap_… API key
Base URLemail.{region}.amazonaws.comapi.anypost.com/v1 (global)
Send endpointPOST /v2/email/outbound-emailsPOST /v1/email
From addressFromEmailAddress (top-level)from (top-level)
To addressesDestination.ToAddresses: [...]to: [...]
SubjectContent.Simple.Subject.Datasubject (top-level)
HTML bodyContent.Simple.Body.Html.Datahtml (top-level)
Text bodyContent.Simple.Body.Text.Datatext (top-level)
TagsEmailTags: [{Name, Value}]tags: ["string"]
Response{MessageId: "…"}202 Accepted, {id, created_at}
WebhooksAmazon SNS to HTTP subscriptionDirect HTTP POST to your URL
Open / click trackingEvent Publishing to Kinesis / CloudWatchBuilt-in webhook events
SMTP hostemail-smtp.{region}.amazonaws.comsmtp.anypost.com (global)
SMTP usernameIAM-generated SMTP usernameanypost
SMTP passwordDerived from your AWS secret keyYour ap_… API key
Before productionSandbox, then a production-access requestEvaluation, then onboarding with our team

The biggest friction with SES is setup overhead: IAM permissions, region selection, SNS topic wiring, and the AWS SDK dependency. On Anypost you verify your domain, create one API key, and build against the real API, with no AWS account required.

Seven steps to switch

SES can stay active while you test Anypost; the two sets of DNS records don't conflict, so verify delivery one email type at a time before cutting over completely.

Create an Anypost account and verify your domain

Sign up at anypost.com and add your sending domain (the same one you use with SES). You'll publish three CNAME records (one for verification, two for DKIM), then click verify; Anypost hosts the SPF and DKIM records they point to, and on Cloudflare one click creates them for you. During your evaluation, your account sends to your own verified domains, so you can build and test everything first. See Domains.

Create an API key

In the Anypost dashboard, create a key (it starts with ap_). This one key replaces your entire AWS IAM credential setup: no access keys, secret keys, regions, or IAM policies to manage. Set it as ANYPOST_API_KEY. See Authentication.

Replace the AWS SDK with the Anypost SDK

Uninstall @aws-sdk/client-sesv2 (or boto3, or the relevant AWS SDK) and install anypost. The Anypost request body is flat, so you'll delete a lot of nesting.

Before (SES)
import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2";
 
const client = new SESv2Client({ region: "us-east-1" });
 
await client.send(new SendEmailCommand({
  FromEmailAddress: "[email protected]",
  Destination: { ToAddresses: ["[email protected]"] },
  Content: {
    Simple: {
      Subject: { Data: "Welcome", Charset: "UTF-8" },
      Body: {
        Html: { Data: "<p>Hi.</p>", Charset: "UTF-8" },
      },
    },
  },
}));
After (Anypost)
import { Anypost } from "anypost";
 
const client = new Anypost("ap_…");
 
await client.email.send({
  from: "[email protected]",
  to: ["[email protected]"],
  subject: "Welcome",
  html: "<p>Hi.</p>",
});

Replace SNS notifications with direct webhooks

SES delivers events through Amazon SNS, which wraps the real notification in an outer envelope. Anypost posts directly to your HTTPS endpoint with a plain JSON body, with no SNS topics, subscriptions, or confirmation handshakes. See the Webhooks section below.

Update your SMTP config (if applicable)

SES SMTP credentials are IAM-generated and region-specific. Anypost uses a single global endpoint with your ap_ key as the password. If you run SES in multiple regions, you can consolidate to one Anypost config. See Sending over SMTP.

Bring your suppression list

Export SES's account-level suppression list (ListSuppressedDestinations) and add each address with POST /v1/suppressions, so your first Anypost sends skip addresses that have already hard-bounced, complained, or opted out. Give each unsubscribe the topic it opted out of, such as marketing, so receipts and password resets still reach that person. See Suppressions.

for (const { email, topic } of exported) {
  await anypost.suppressions.create({ email, topic });
}

Move your volume over with us

Our deliverability experts plan your warm-up from what you told us about your sending. Move one email type at a time, starting with the mail your recipients expect most, while we raise your volume at a pace mailbox providers accept, so they see a steady sender, not a sudden new one. Questions about timing? Talk to us.

The same change in every SDK

The pattern is the same everywhere: remove the AWS SDK, remove the nested content wrappers, and call the Anypost SDK with a flat body. Pick your language.

Amazon SES
# SES requires AWS Signature v4, which is not practical
# to sign by hand. Use the AWS CLI instead:
 
aws sesv2 send-email \
  --region us-east-1 \
  --from-email-address [email protected] \
  --destination [email protected] \
  --content 'Simple={
    Subject={Data=Welcome,Charset=UTF-8},
    Body={Html={Data=<p>Hi.</p>,Charset=UTF-8}}
  }'
Anypost
curl https://api.anypost.com/v1/email \
  -H "Authorization: Bearer ap_…" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "[email protected]",
    "to": ["[email protected]"],
    "subject": "Welcome",
    "html": "<p>Hi.</p>"
  }'

No request signing. SES requires AWS Signature v4, so a raw curl call needs Authorization, X-Amz-Date, and related headers computed from your credentials. Anypost's Bearer token works with any HTTP client, no signing step.

Webhooks

SES doesn't post directly to your server. It publishes to an Amazon SNS topic, which delivers an outer envelope whose Message field is a JSON-encoded string you have to parse a second time. Anypost posts its events directly to your URL in a batched events array.

SES notificationTypeAnypost eventNotes
Deliveryemail.deliveredrenameAccepted by the recipient's mail server
Bounce (Permanent)email.bouncedrenamebounce.type: "permanent"; Anypost suppresses the address
Bounce (Transient)email.bouncedrenamebounce.type: "transient", after the final retry
Complaintemail.complainedrenameFeedback-loop spam report
Open (Event Publishing)email.openedrenameNo Kinesis/CloudWatch; a direct webhook
Click (Event Publishing)email.clickedrenameNo Kinesis/CloudWatch; a direct webhook

Anypost also emits email.delayed while a delivery is failing temporarily and still being retried, which SES surfaces only as a terminal Transient bounce. You don't need to handle it to migrate.

SES via SNS (double-encoded)
{
  "Type": "Notification",
  "MessageId": "uuid",
  "Message": "{\"notificationType\":\"Bounce\",\"mail\":{\"messageId\":\"…\",\"destination\":[\"[email protected]\"]},\"bounce\":{\"bounceType\":\"Permanent\",\"bouncedRecipients\":[{\"emailAddress\":\"[email protected]\"}]}}"
}
Anypost (direct)
{
  "batch_id": "9c1f2a7b…",
  "events": [
    {
      "type": "email.bounced",
      "occurred_at": "2026-01-15T10:30:00Z",
      "data": {
        "email_id": "email_018f4f3e-7b2c-7c80-8e21-1a3a4f5b6c7d",
        "recipient": "[email protected]"
      }
    }
  ]
}
Before (SES via SNS)
// SES via SNS: unwrap the envelope, parse the inner JSON string
app.post("/webhook/email", async (req, res) => {
  const sns = req.body;
  if (sns.Type === "SubscriptionConfirmation") {
    await fetch(sns.SubscribeURL); // confirm the SNS subscription
    return res.sendStatus(200);
  }
  const event = JSON.parse(sns.Message); // double-encoded
  switch (event.notificationType) {
    case "Bounce":
      suppressAddress(event.bounce.bouncedRecipients[0].emailAddress);
      break;
    case "Complaint":
      unsubscribe(event.complaint.complainedRecipients[0].emailAddress);
      break;
    case "Delivery":
      markDelivered(event.mail.messageId);
      break;
  }
  res.sendStatus(200);
});
After (Anypost)
// Anypost: a batched events array, standard names, no unwrapping
app.post("/webhook/email", (req, res) => {
  for (const { type, data } of req.body.events) {
    switch (type) {
      case "email.delivered":  markDelivered(data.email_id); break;
      case "email.bounced":
        if (data.bounce.type === "permanent") suppressAddress(data.recipient);
        break;
      case "email.complained": unsubscribe(data.recipient); break;
      case "email.opened":
      case "email.clicked":    recordEngagement(data); break;
    }
  }
  res.sendStatus(200);
});

Suppress on permanent bounces only. email.bounced also reports temporary failures and messages that expired after 72 hours of retries. Anypost already suppresses permanent bounces for you, so if you mirror the list, check data.bounce.type first. See Webhooks.

Signature verification differs. SES messages are signed by SNS (an x509 certificate you fetch and verify). Anypost signs with an Anypost-Signature header (t=<unix>,v1=<hmac>) over the timestamp and raw body. You can drop the SNS subscription-confirmation handler entirely. See Webhooks.

Register your HTTPS URL once in the Anypost dashboard and select which events to receive. No SNS topics, subscriptions, or confirmation handshake.

Using SMTP instead of the API?

SES SMTP credentials are IAM-generated and region-specific; the SMTP password is derived from your AWS secret key by a special algorithm, not the secret itself. Anypost uses your ap_ key directly as the password.

Amazon SES SMTP
# Region-specific host, different per AWS region
SMTP_HOST=email-smtp.us-east-1.amazonaws.com
SMTP_PORT=587
# IAM-generated SMTP username (not your AWS access key)
SMTP_USER=AKIAIOSFODNN7EXAMPLE
# Derived SMTP password (not your AWS secret key)
SMTP_PASS=BXXhFg5Xb9/0sJtqo...
SMTP_STARTTLS=true
Anypost SMTP
# One global host, no region configuration
SMTP_HOST=smtp.anypost.com
SMTP_PORT=587
SMTP_USER=anypost
# Your API key is the password, no derivation step
SMTP_PASS=ap_…
SMTP_STARTTLS=true

If you run SES across multiple regions, each has its own SMTP hostname and its own IAM-generated credentials. Anypost uses one global endpoint wherever your servers run. Supported ports are 587 (recommended), 2587, and 25; STARTTLS is required on every port, and there's no implicit-TLS port, so move off 465 to 587. See Sending over SMTP.

Ready to switch?

Start with a free evaluation and build against the real API, then bring your volume over while our deliverability experts warm up your sending.