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.
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.
| Concept | Amazon SES | Anypost |
|---|---|---|
| Auth | AWS Signature v4 (AWS SDK / IAM) | Authorization: Bearer ap_… |
| Credentials | Access key ID + secret access key | A single ap_… API key |
| Base URL | email.{region}.amazonaws.com | api.anypost.com (global) |
| Send endpoint | POST /v2/email/outbound-emails | POST /v1/email |
| From address | FromEmailAddress (top-level) | from (top-level) |
| To addresses | Destination.ToAddresses: [...] | to: [...] |
| Subject | Content.Simple.Subject.Data | subject (top-level) |
| HTML body | Content.Simple.Body.Html.Data | html (top-level) |
| Text body | Content.Simple.Body.Text.Data | text (top-level) |
| Tags | EmailTags: [{Name, Value}] | tags: ["string"] |
| Response | {MessageId: "…"} | 202 Accepted, {id, created_at} |
| Webhooks | Amazon SNS to HTTP subscription | Direct HTTP POST to your URL |
| Open / click tracking | Event Publishing to Kinesis / CloudWatch | Built-in webhook events |
| SMTP host | email-smtp.{region}.amazonaws.com | smtp.anypost.com (global) |
| SMTP username | IAM-generated SMTP username | anypost |
| SMTP password | Derived from your AWS secret key | Your ap_… API key |
| Sandbox mode | New accounts are sandboxed | No 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.
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" },
},
},
},
}));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.
# 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}}
}'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 notificationType | Anypost event | Notes | |
|---|---|---|---|
Delivery | email.delivered | rename | Accepted by the recipient's mail server |
Bounce (Permanent) | email.bounced | rename | Hard bounce; suppress the address |
Bounce (Transient) | email.bounced | rename | SES fires this only after its final retry |
Complaint | email.complained | rename | Feedback-loop spam report |
| Open (Event Publishing) | email.opened | rename | No Kinesis/CloudWatch; a direct webhook |
| Click (Event Publishing) | email.clicked | rename | No 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.
{
"Type": "Notification",
"MessageId": "uuid",
"Message": "{\"notificationType\":\"Bounce\",\"mail\":{\"messageId\":\"…\",\"destination\":[\"[email protected]\"]},\"bounce\":{\"bounceType\":\"Permanent\",\"bouncedRecipients\":[{\"emailAddress\":\"[email protected]\"}]}}"
}{
"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]"
}
}
]
}// 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);
});// 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.
# 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# 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=trueIf 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.