Migrate from Amazon SES
This page maps the SES v2 SendEmail call, account-level suppression list, and SNS event notifications to Bird. Follow the main migration guide in order, and use these mappings for steps 1, 3, and 4.
Hand this to your agent
Paste this into Claude Code, Cursor, or Codex. The agent works through this page against your own repository, using whichever Bird surface it already has: the MCP server if one is connected, the CLI if it is installed and signed in.
Codebeispiel
I am moving an email integration from Amazon SES to Bird. Route through it with me.
1. Check what you already have before setting anything up. If Bird's MCP server is connected, use its tools. If the Bird CLI is installed and signed in, use that. Either one is enough, and every step below is an action you take with whichever you have. Only if neither is present, follow https://bird.com/docs/ai/set-up-your-agent.md to set one up and sign me in. Every Bird docs page serves Markdown at its own URL with `.md` appended, so fetch that rather than the HTML.
2. Read https://bird.com/docs/guides/email/migrate/ses.md for the payload, suppression and event mapping, and https://bird.com/docs/guides/email/migrate.md for the order the steps go in.
3. Find and list my SES usage in this repository and its infrastructure before you change anything: the SendEmail and SendRawEmail call sites through the AWS SDK or CLI, the configuration sets they name, the SNS topics or EventBridge rules carrying my events, the handler subscribed to them, and every identity I send from. Say which of these live in infrastructure code rather than application code, because those change by a different route.
4. Register each of those sending domains with Bird and give me the DNS records to publish, following https://bird.com/docs/guides/email/sending-domains.md. Leave the SES DKIM CNAMEs exactly as they are: Bird's DKIM record uses its own selector, so the two coexist and both providers authenticate side by side until I switch traffic. Publishing DNS affects mail for the whole domain, so show me the records and let me publish them.
5. Export my account-level suppression list from SES and import it into Bird before any production traffic goes through Bird, so my first sends do not reach addresses that already bounced or complained. Read it from GET /v2/email/suppressed-destinations, paginating with NextToken to the end, and keep both the BOUNCE and COMPLAINT reasons. The Bird import takes one address per request and is idempotent, so a partial re-run is safe. https://bird.com/docs/guides/email/suppressions.md has the reason taxonomy.
6. Port the send call and replace the event plumbing. Bird posts signed webhooks straight to an endpoint, so the SNS topic, the subscription-confirmation handshake, and the message-envelope unwrapping all go away rather than being ported: my handler reads the event body directly and verifies it per Standard Webhooks. See https://bird.com/docs/guides/webhooks.md and https://bird.com/docs/guides/email/events.md. Tell me which SNS or EventBridge resources become unused, but do not delete any of them.
7. Run my whole integration against Bird's mail sandbox before any production traffic, following https://bird.com/docs/guides/email/testing-sandbox.md. Sandbox sends run the real pipeline without reaching an inbox or touching my sending reputation.
8. Stop and ask me wherever a step needs a decision. Do not point production traffic at Bird until I have seen the sandbox results and replied with the words cut over to Bird. Retiring the SES path is a separate step that comes later: ask me again and wait for me to reply with the words retire the SES path. A reply that agrees without naming what it is authorising is not authorisation. Finish by telling me what is left that only a person can do.Map the send call
SES splits a send across Destination, Content, and configuration-set plumbing. Our POST /v1/email/messages is one flat payload:
| What it does | SES (SendEmail v2) | Bird |
|---|---|---|
| Sender | FromEmailAddress | from |
| Recipients | Destination.*Addresses | to / cc / bcc (arrays) |
| Subject | Content.Simple.Subject | subject |
| Body | Content.Simple.Body.Html/Text | html / text (at least one) |
| Reply-to | ReplyToAddresses | reply_to (array) |
| Custom headers | Content.Simple.Headers | headers (string → string object) |
| Filterable labels | EmailTags | tags: {name, value} pairs |
| Round-trip context | (none) | metadata: arbitrary JSON |
| Stored template | Content.Template | template + template.parameters |
| Open/click tracking | configuration set | track_opens / track_clicks (default true) |
| IP pool | dedicated IP pool (config set) | ip_pool_id (ipp_... or ipp_shared) |
| Category | (none) | category: marketing (default) or transactional |
Our field caps and defaults (recipient counts, tag and metadata limits) live in Sending email.
Porting notes:
- Configuration sets dissolve into per-message fields. Tracking, IP pool, and event routing were configuration-set concerns on SES. Here, the first two are payload fields and event routing is a webhook subscription.
- Auth changes from SigV4 to a bearer token. No request signing; a plain Authorization: Bearer bk_... header. Drop the AWS SDK credential chain from this code path.
- SES templates port to stored templates. Content.Template (template name plus TemplateData) maps to our template field with values in template.parameters. See sending with a template.
- Content.Raw (MIME) has no equivalent. We build the message from structured fields. If you assemble raw MIME to attach files, send them as our attachments array instead (base64 content per file, content_id for inline images).
- SES sandbox ≠ Bird sandbox. SES's sandbox restricts who you can send to. Our mail sandbox is a simulator with magic addresses: no allowlisting, and nothing is delivered.
Export suppressions
Export the account-level suppression list and run it through the import loop:
- GET /v2/email/suppressed-destinations (paginate with NextToken, and each entry has BOUNCE or COMPLAINT as the reason)
Translate webhook events
SES publishes events through SNS or EventBridge. We POST signed webhooks directly, so the SNS topic, subscription-confirmation handshake, and message-envelope unwrapping all go away. The event names map like this:
| Outcome | SES | Bird |
|---|---|---|
| Accepted/processed | Send | email.accepted → email.processed |
| Delivered | Delivery | email.delivered |
| Temporary failure | DeliveryDelay | email.deferred |
| Permanent bounce | Bounce | email.bounced / email.out_of_band_bounce |
| Spam complaint | Complaint | email.complained |
| Blocked/suppressed | (none) | email.rejected |
| Open | Open | email.opened |
| Click | Click | email.clicked |
| Unsubscribe | Subscription | email.list_unsubscribed |
email.rejected is new relative to SES: we report suppressed recipients visibly (status rejected, rejection_reason: recipient_suppressed) rather than counting them into the send-and-bounce cycle. Add a handler for it.
In place of SNS message verification, we sign per the Standard Webhooks specification, with HMAC headers on the delivery itself. The verification recipe is in Webhooks & events.
Cut over
Work through domains & DNS and the sandbox smoke test in the main guide. Both are provider-independent. One SES-specific note for the DNS step: SES's DKIM CNAMEs stay in place during the transition. Our DKIM TXT record uses its own selector, so the two coexist.
Next steps
- Sending domains: registration, verification lifecycle, and the DNS records you're re-pointing
- Webhooks & events: endpoint setup and Standard Webhooks verification
- Testing sandbox: smoke-test the new integration before cutover
- Suppressions: confirm your imported list and how we maintain it from here