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 (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
Sandbox modeNew accounts are sandboxedNo sandbox

The biggest friction with SES is setup overhead: sandbox mode, IAM permissions, region selection, SNS topic wiring, and the AWS SDK dependency. On Anypost you verify your domain, create an API key, and start sending, with no AWS account required.

Five steps to switch

SES can stay active while you test Anypost; use a separate sending domain or subdomain during the transition and verify delivery 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 DNS records for SPF, DKIM, and tracking. Unlike SES, there's no production-access request and no sandbox waiting period. 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.

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.bouncedrenameHard bounce; suppress the address
Bounce (Transient)email.bouncedrenameSES fires this only after its 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":    suppressAddress(data.recipient); break;
      case "email.complained": unsubscribe(data.recipient); break;
      case "email.opened":
      case "email.clicked":    recordEngagement(data); break;
    }
  }
  res.sendStatus(200);
});

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_TLS=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_TLS=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?

Create your account, verify your domain, and send your first email in minutes.