# Replace Amazon SES without rebuilding delivery
URL: https://samva.dev/guides/replace-amazon-ses
> A practical migration path from Amazon SES and custom glue to a typed email API with templates, conversation threading, and signed webhooks.
- Published: 2026-09-01



Start with [Samva vs Amazon SES](/compare/amazon-ses) if you are deciding whether the switch is
worth making. It compares AWS's published transport rate with the email product you would operate
around it. Return here once the decision is made: this is the implementation path for one reversible
flow, not another pricing comparison.

Amazon SES gives you transport. The application around it is your team's responsibility: request
shapes, templates, domain setup, delivery events, bounce handling, inbound parsing, and the
dashboard that explains what happened. If that surrounding code has become a second product to
maintain, a managed email API can replace the glue without forcing a rewrite of every email flow at
once.

This guide shows a staged migration to Samva. It is for teams that want a typed send path and a
single place for templates, delivery events, inbound email, and conversations. Raw transport can
still be cheaper per thousand emails, so weigh that rate against the engineering and operating work
it leaves you.

## Define the cutover contract [#define-the-cutover-contract]

Pick one transactional flow and write down the evidence required before it moves. A password reset
or order confirmation works well because the trigger, recipient, and expected outcome are clear.

| Checkpoint             | Evidence to retain                                                             | Rollback condition                                                     |
| ---------------------- | ------------------------------------------------------------------------------ | ---------------------------------------------------------------------- |
| Domain                 | A verified Samva sending domain and authenticated From address                 | Verification is incomplete or the sender is not cleared for production |
| Send                   | The application record, idempotency key, and Samva message id agree            | The new path cannot return or persist a message id                     |
| Delivery               | A signed event is stored once for the expected message id                      | Delivery, failure, or bounce handling cannot be reconciled             |
| Reply, when applicable | A `message.received` event carries a conversation id your application can read | A reply cannot be attached to the expected support or product record   |
| Rollback               | The SES path remains deployable for the selected flow                          | Any checkpoint fails during the agreed observation window              |

## Map the stack before you move it [#map-the-stack-before-you-move-it]

Inventory the pieces that currently surround `SendEmail` or an SMTP relay. The goal is to preserve
the behavior your product needs while replacing the code you no longer want to own.

| Current responsibility                     | Migration destination                                 |
| ------------------------------------------ | ----------------------------------------------------- |
| Direct send request and response handling  | `samva.messages.send` or `POST /v1/messages`          |
| Hand-built HTML or provider templates      | Versioned server-side templates, or inline HTML first |
| Configuration-set or notification plumbing | Signed delivery webhooks                              |
| Custom bounce and complaint list           | Suppression and message status in the email platform  |
| Receipt rules and raw inbound mail parsing | Inbound messages with conversation context            |
| A local `thread_id` convention             | The conversation returned by Samva                    |
| Separate operational scripts               | The dashboard, CLI, API, and typed SDK                |

Keep this inventory beside the migration pull request. It gives you a checklist for parity and
prevents a successful first send from hiding an unfinished bounce or reply path.

## Start with one transactional flow [#start-with-one-transactional-flow]

Choose a flow with a clear trigger and a measurable result, such as an order confirmation or a
password reset. Keep the existing path for other flows while you prove the new one.

Verify a sending domain first. Samva generates the DKIM and custom MAIL FROM records for the
domain; publish those records and wait for verification before routing production mail. The
[email authentication guide](/guides/email-authentication) explains how SPF, DKIM, and DMARC fit
together, and [Verify your sending domain](/docs/channels/email/verify-your-domain) covers the
Samva workflow.

Then make the smallest possible send call. This uses the same `email.send()` facade as the
[send-an-email quickstart](/docs/channels/email/send-an-email):

```typescript title="send-order-confirmation.ts"
import { createClient } from "samva";

const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });

const message = await samva.email.send(
  {
    to: "ada@example.com",
    subject: "Your order shipped",
    html: "<h1>On its way</h1><p>Order A-1042 is out for delivery.</p>",
  },
  { idempotencyKey: "order-A-1042-confirmation" },
);

console.log(message.id, message.status);
```

The REST equivalent is the same product operation:

```bash title="send-order-confirmation.sh"
curl -X POST https://api.samva.dev/v1/messages \
  -H "X-API-Key: $SAMVA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-A-1042-confirmation" \
  -d '{
    "to": [{ "email": "ada@example.com" }],
    "channel": "email",
    "email": {
      "subject": "Your order shipped",
      "html": "<h1>On its way</h1><p>Order A-1042 is out for delivery.</p>"
    }
  }'
```

The response gives your application a message id and an initial status. Persist the message id with
the order or account record. Do not treat acceptance as delivery. Subscribe to the events that
decide whether the flow succeeded.

## Make retries safe before cutover [#make-retries-safe-before-cutover]

Networks can drop a response after the server accepts a send. Give each logical email a stable
`Idempotency-Key` and reuse it when the application retries. An identical replay returns the
original message for 24 hours; changing the request under the same key returns a conflict instead
of silently sending different content.

Use a business identifier that stays stable across process restarts. For example, derive the key
from the order id and the email purpose, not from a random request id. Record the Samva message id
with the order so support can follow the send without searching provider logs.

## Move delivery events next [#move-delivery-events-next]

Create one endpoint for the events your flow needs. Delivery webhooks are signed, delivered at
least once, and retried when your endpoint does not return a successful response. Persist the
`webhook-id` in the same transaction as the effect it guards, or record durable processing state that
only flips to complete after the handler succeeds, so a retry after a partial failure cannot skip or
double-apply an event. The verifier must receive the original `Request`, before any JSON parsing:

```typescript title="samva-webhook.ts"
import { verifyRequest, WebhookVerificationError } from "samva/webhooks";

export async function POST(request: Request) {
  try {
    const verified = await verifyRequest(
      request,
      process.env.SAMVA_WEBHOOK_SECRET!,
    );

    await persistAndHandleOnce(verified.id, verified.event);
    return new Response(null, { status: 204 });
  } catch (error) {
    if (error instanceof WebhookVerificationError) {
      return new Response("Invalid webhook", { status: 401 });
    }
    throw error;
  }
}
```

Subscribe to `message.delivered`, `message.bounced`, and `message.failed` for the first flow. Add
`message.received` when you move inbound email. The [webhook documentation](/docs/platform/webhooks)
has the current retry schedule and raw-body verification rules; the [webhook guide](/guides/email-webhooks)
explains the event choices and idempotency pattern.

## Move templates after the send is proven [#move-templates-after-the-send-is-proven]

Do not block the migration on a template rewrite. Inline HTML is a valid first step. Once the
transport and event path are reconciled, move the body into a versioned template and pass data at
send time:

```typescript title="send-template.ts"
await samva.messages.send({
  to: [{ contactId: "contact_k9mvt4znw8rh7q2x" }],
  channel: "email",
  email: {
    templateSlug: "order-update",
    templateData: { orderId: "A-1042", eta: "Tomorrow" },
  },
});
```

Versioning separates a content change from a deploy. Publish and test the new version, then keep
the send code focused on the data that belongs in the message.

## Add inbound conversations when the outbound path is stable [#add-inbound-conversations-when-the-outbound-path-is-stable]

If your SES setup stores raw inbound mail or maintains a separate thread table, migrate that path
after the first outbound flow is healthy. Verify the domain's receiving setup, subscribe to
`message.received`, and use its conversation context to fetch the surrounding history.

```typescript title="handle-reply.ts"
async function handleReply(event: {
  type: "message.received";
  data: { conversationId: string; messageId: string };
}) {
  const conversation = await samva.conversations.getById(event.data.conversationId);
  await supportInbox.append({
    messageId: event.data.messageId,
    conversationId: conversation.id,
  });
}
```

The important boundary is the conversation id. Your application can keep its ticket or case id,
but it no longer needs to infer message membership from a subject line or maintain a parallel
`thread_id` convention. See [Inbound email](/features/inbound-email) for the product behavior and
[Enable inbound email](/docs/api-reference/email/enableReceiving) for setup.

## Cut over with evidence [#cut-over-with-evidence]

Run both paths only for the chosen flow while you compare the outcomes that matter to customers.
Do not send the same production email through both providers to the same recipient. Use controlled
test recipients for side-by-side checks, then route a small production slice to one path at a time:

1. Send a controlled test from the verified domain.
2. Reconcile the Samva message id with the delivery event and your order or account record.
3. Exercise a bounce or failure path in a non-production test flow.
4. If the flow receives replies, confirm the `message.received` event and conversation id.
5. Compare rendered templates, headers, links, and unsubscribe behavior where applicable.
6. Route the next small production slice through Samva and watch event and support records.
7. Record the observation window, volume, and any unexplained events in the migration pull request.
8. Remove the old path only after the retained traffic window is quiet.

Keep the old provider configuration available during the rollback window. A migration is complete
when your application can explain every send, failure, and reply from its own records.

## Known limits [#known-limits]

* This guide does not cover migration of historical SES events, raw inbound objects, or suppression
  records. Export and retain those records under your existing data and privacy policy before
  retiring an SES path.
* Route each production flow through one active path at a time. Do not duplicate a recipient's
  transactional email to compare providers.
* Inbound cutover needs its own rollback plan. Preserve the SES receipt-rule destination until you
  have tested receiving records, `message.received`, and the conversation lookup with a controlled
  reply.
* Provider acceptance, delivery, and recipient engagement are different events. The migration proof
  is your stored send and webhook record, not an inbox-placement claim.

## What changes in the cost model [#what-changes-in-the-cost-model]

Direct Amazon SES can be the right choice when your team wants to own the application layer and
optimize for raw transport cost. AWS still publishes à la carte outbound at $0.10 per 1,000 emails
(verified September 29, 2026 on [Amazon SES pricing](https://aws.amazon.com/ses/pricing/)). New
accounts start on Essentials at $0.16 per 1,000 unless you switch. Samva earns its place when the
saved engineering and operational work matters more: typed SDKs, idempotent sends, versioned
templates, authenticated domains, signed webhooks, inbound parsing, conversation records, a
dashboard, and a CLI in one email integration. The dated side-by-side lives on
[Samva vs Amazon SES](/compare/amazon-ses).

Price the migration honestly. Count the code you can delete, the operational paths you no longer
have to maintain, and the time it takes to answer “what happened to this email?” Then compare that
with the managed email plan that fits your volume. The result should be a simpler product path.

## Continue with a typed integration [#continue-with-a-typed-integration]

Once the first flow is stable, use the client that fits your runtime. The [Effect SDK guide](/docs/integrations/effect-sdk)
composes send, webhook, and conversation operations as Effects with typed errors and retry policy.
The [TypeScript SDK guide](/docs/developers/typescript-sdk) covers the Promise API, while the
[REST API guide](/docs/developers/rest-api) is the neutral path for other runtimes.

Start with one flow, prove the event loop, then move the next piece of the stack.

## Sources [#sources]

* [Amazon SES API reference](https://docs.aws.amazon.com/ses/latest/APIReference/Welcome.html)
* [Amazon SES pricing](https://aws.amazon.com/ses/pricing/)
* [Amazon SES email authentication](https://docs.aws.amazon.com/ses/latest/dg/email-authentication.html)

## FAQ

### Is Samva cheaper than sending directly with Amazon SES?

Raw Amazon SES transport can cost less at high volume. À la carte outbound is $0.10 per 1,000 on AWS's pricing page; new accounts start on Essentials at $0.16 per 1,000 as of September 29, 2026. Samva is useful when the engineering and operational work around sending, templates, inbound conversations, webhooks, domain setup, and a dashboard costs more than the transport savings. See Samva vs Amazon SES for the dated side-by-side.

### Can I migrate one email flow at a time?

Yes. Start with one low-risk transactional flow, verify its domain, send through the Samva API, and compare delivery events before moving more traffic. Keep the old path available until the new flow is reconciled.

### Does Samva preserve inbound email threads?

Inbound messages are parsed and associated with conversations. A message.received webhook includes the conversation context, so your application can fetch the thread instead of maintaining its own thread_id convention.

### Can I keep my existing templates?

Keep the source content, then move the production version into Samva's versioned template workflow or continue sending inline HTML while you migrate. The first send does not require a template rewrite.
## Related

- [Samva vs Amazon SES](/compare/amazon-ses): Side-by-side pricing and features, with sources verified against AWS and Samva pages.
- [How to Choose a Transactional Email API: Checklist](/guides/transactional-email-api): Evaluate the send, authentication, event, template, and suppression requirements before choosing an email API.
- [Email authentication: SPF, DKIM, and DMARC explained](/guides/email-authentication): Set up domain authentication in the right order before moving production traffic.
- [Email webhooks: events, signatures, and idempotency](/guides/email-webhooks): Verify signed delivery events and make webhook handling safe to retry.
- [Effect SDK](/docs/integrations/effect-sdk): Compose Samva email operations as Effects with typed errors and retry policy.
