What changes, what doesn't
This is the biggest jump of the four migration guides, because SES isn't a conventional API-key product. It's an AWS service authenticated with IAM and wired into SNS for events, not webhooks. What survives: the SMTP path, if that's how you send today, changes the least. What doesn't: request signing, event delivery, and (usually) scheduling all work differently enough that "find and replace the endpoint" won't get you there. The honest comparison, including where SES remains the better choice (raw per-email cost, if you already accept AWS as your data processor), is at euromail vs Amazon SES.
Verify your domain on EuroMail first, run both in parallel, then cut over.
API mapping
| Amazon SES (v2) | EuroMail | |
|---|---|---|
| Endpoint | POST https://email.{region}.amazonaws.com/v2/email/outbound-emails | POST https://api.euromail.dev/v1/emails, no region in the path |
| Auth | AWS Signature Version 4. IAM credentials, not a bearer token | X-EuroMail-Api-Key: em_live_... header |
| Sender/recipient | FromEmailAddress, Destination.ToAddresses/CcAddresses/BccAddresses (arrays) | from, to/cc/bcc |
| HTML body | Content.Simple.Body.Html.Data | html_body |
| Text body | Content.Simple.Body.Text.Data | text_body |
| Attachments | Content.Simple.Attachments[].RawContent (added to the Simple content type in 2024; raw MIME was previously required for attachments) | attachments: [{ filename, content, content_type }] (base64) |
| Grouping | EmailTags (name/value pairs) | tags (array of strings) + metadata (object) |
| Scheduling | No native delivery-time parameter. Build it yourself (EventBridge Scheduler, SQS delay queue, or a cron that calls SendEmail at the right time) | send_at (RFC 3339). Built in, no extra infrastructure |
| Idempotency | None at the SES API level | idempotency_key body field. Same key always returns the original result |
| Success response | 200 with { "MessageId": "..." } | 202 Accepted with { "id", "status": "queued", ... } |
Auth is the real migration work here. SES requests are signed with AWS
SigV4 (normally handled by an AWS SDK, not written by hand) while EuroMail
is a single static header. If your code currently calls SESv2Client from an
AWS SDK, replace the client entirely rather than trying to preserve the
signing logic.
Code before and after
TypeScript (SigV4 signing makes a raw curl comparison misleading. The SDK swap is the realistic before/after):
// Before: Amazon SES (AWS SDK v3)
import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2";
const ses = new SESv2Client({ region: "eu-west-1" });
await ses.send(new SendEmailCommand({
FromEmailAddress: "[email protected]",
Destination: { ToAddresses: ["[email protected]"] },
Content: {
Simple: {
Subject: { Data: "Welcome aboard" },
Body: { Html: { Data: "<h1>Welcome!</h1>" } },
},
},
}));
// After: EuroMail
import { EuroMail } from "@euromail/sdk";
const client = new EuroMail({ apiKey: process.env.EUROMAIL_API_KEY! });
await client.emails.send({
from: "[email protected]",
to: "[email protected]",
subject: "Welcome aboard",
html_body: "<h1>Welcome!</h1>",
});
No IAM role, no region config, no SDK-specific client. An API key and one HTTP call. Python, Rust, and Go SDKs follow the same pattern.
SMTP cutover
If you send through SES's SMTP interface rather than the API, this is the smallest-effort path. SES SMTP is already a username/password credential, not IAM-signed:
| Setting | Amazon SES | EuroMail |
|---|---|---|
| Host | email-smtp.{region}.amazonaws.com (region-specific) | smtp.euromail.dev (single global endpoint) |
| Port | 587, 465, or 25 | 587 (STARTTLS) or 465 (implicit TLS) |
| Username | SES-specific SMTP username (generated in the console, derived from IAM, not your access key directly) | apikey (literal string) |
| Password | SES-specific SMTP password (also console-generated, one per AWS Region) | your EuroMail API key (em_live_...) |
Two simplifications: EuroMail has no per-region endpoint (one host, everywhere) and no separate credential-generation step (the API key you already have is the password). Full walkthroughs in Send Email via SMTP.
DNS: verify your domain before switching
- Add your domain in the EuroMail dashboard and publish its DKIM, SPF, and DMARC records. Domain Verification.
- Keep SES's records in place during the dual-run. Different DKIM
selectors and stacked SPF
include:s let both providers verify at once. - Remove SES's records after cutover, on your own schedule.
Webhook event mapping (SES uses SNS, not webhooks)
SES doesn't have webhooks in the conventional sense. It publishes JSON event records to an SNS topic, which you then subscribe to (commonly via an HTTP/S endpoint, which behaves like a webhook once wired up, or via SQS). EuroMail delivers events as direct HTTP webhooks with no message-bus configuration required.
| SES SNS event type | EuroMail event |
|---|---|
Delivery | delivered |
Bounce (bounceType: "Permanent") | bounced |
Bounce (bounceType: "Transient") | deferred |
Complaint | complained |
Open | opened |
Click | clicked |
Send | No equivalent. The 202 API response already confirms acceptance |
Reject, Rendering Failure | No single event. Malformed requests are rejected synchronously with a 4xx response |
DeliveryDelay | Reflected as a deferred retry, not a separate event |
Subscription | Handled via suppression lists, not a webhook event |
Setup changes from configuring an SNS topic, subscription, and (usually) an
SQS queue or Lambda to just registering a URL. Swap your event handler for the
one in Webhooks. HMAC-SHA256 over
X-EuroMail-Signature, verifiable without any AWS SDK.
Bring your suppression list
SES maintains an account-level suppression list; export it via the
ListSuppressedDestinations API or console, then replay entries:
curl -X POST https://api.euromail.dev/v1/suppressions \
-H "X-EuroMail-Api-Key: em_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"email_address": "[email protected]",
"reason": "migrated from Amazon SES: hard bounce"
}'
Details in Suppression Lists.
Honest gaps
- No infrastructure-as-code parity. If your SES setup is Terraform/CDK (identities, configuration sets, SNS topics, IAM policies), that infrastructure has no EuroMail equivalent to import. Domain verification and webhook registration happen once, through the dashboard or API, and stay there.
- Sending quotas work differently. SES enforces per-account sending rate and daily quota limits you request increases for; EuroMail's plan tiers define quota instead, with no separate rate-limit negotiation. See Billing & Plans.
- Cross-account sending authorization (SES's identity policies allowing one AWS account to send as another's verified identity) has no direct analog. Use sub-accounts for the equivalent multi-tenant pattern.
- Full comparison at euromail vs Amazon SES.
Cutover checklist
- Create a EuroMail account and an API key with the
emails:sendscope (API Keys). - Verify your sending domain; confirm DKIM/SPF/DMARC show green.
- Register your webhook endpoint, no SNS topic or subscription needed.
- Import suppressions from your SES export.
- Replace the AWS SDK SES client with the EuroMail SDK (or the two SMTP
settings), accepting
202as success. - Send a staging email end-to-end; confirm the
deliveredwebhook arrives. - Cut production traffic over; watch the first hour in Analytics and Deliverability Insights.
- Keep SES identities verified for a rollback window, then clean up IAM policies and SNS subscriptions.