Amazon SES
Use Amazon SES for MainSDK email and the notify email channel - verified identities, sandbox and production access, region, main.yaml and swapping.
Amazon Simple Email Service (SES) is one of the two email providers the email spec ships, next to Resend. Your app sends with email.send() and email.marketing() as usual, and SES delivers from your AWS account. The same driver carries the email channel of notify. Pick SES when your app already runs on AWS, when you want sending to stay inside your AWS account and region, or as a fallback behind Resend.
Interfaces it serves
Pick Amazon SES in main.yaml for any of these. Your calls stay the same, and you can swap later.
notifyBuilding for 0.3Driver ses Amazon SESPlanned driver
notify:
driver: sesemailBuilding for 0.3Driver ses Amazon SES
email:
driver: sesSwap to another provider
Before you start
You need an AWS account with permission to use SES in the region you choose.
- Pick a region. SES is a regional service: identities, sending limits and production access are set per region. Choose the region that matches your MainSDK data region (US, EU or India) and do every step below in it.
- Verify an identity. Add your sending domain as a verified identity (or, for early testing, a single email address). For a domain, SES gives you DKIM records to add at your DNS host; the identity is usable once SES shows it as verified. A custom MAIL FROM domain and a DMARC record help deliverability.
- Request production access. New SES accounts start in the sandbox in each region: you can send only to verified addresses, with low sending limits. Request production access for the region before you send to real users. AWS reviews the request, so allow time for it.
- Set up bounce and complaint notifications. Create a configuration set with an SNS event destination for bounces, complaints and deliveries, and subscribe your app's webhook URL to the SNS topic. MainSDK feeds bounces and complaints to its suppression list (see below).
- Give the app credentials. Create an IAM identity whose policy allows sending through SES, scoped to your identities, and nothing more. On AWS compute, prefer an IAM role over long-lived access keys.
Store any access keys in your secrets manager, never in code or a committed env file.
Configure
email:
driver: ses
region: eu-west-1 # the region of your verified identity
from: "Pinecart <hello@mail.pinecart.dev>"
configuration-set: mainsdk-events # publishes events to SNS
webhook: ses-events # bounces and complaints, see below
access-key-id: AKIAEXAMPLE # leave out on AWS compute to use the role
key: secret:AWS_SECRET_ACCESS_KEY # the secret for access-key-id
tracking: { opens: false, clicks: false } # the defaults
webhooks:
inbound:
ses-events:
preset: ses
topics: ["arn:aws:sns:eu-west-1:123456789012:mainsdk-events"]
path: /api/webhooks/ses- Region:
email.region, or theAWS_REGIONenvironment variable. Without either, the app fails to start withMAIN-EMAIL-0006. - Credentials: with
access-key-idandkeyset, the driver signs with that pair. With neither, it uses the runtime IAM role. A literal key inmain.yamlfails to load withMAIN-CONFIG-0004.
What the driver sends
Each message is one POST https://email.<region>.amazonaws.com/v2/email/outbound-emails (the SES v2 SendEmail call) signed with AWS Signature Version 4 for the ses service. The JSON body holds FromEmailAddress, Destination.ToAddresses, ReplyToAddresses, a simple Content with subject, text and HTML parts, Headers (the List-Unsubscribe pair on marketing mail), EmailTags for the template, the kind, the campaign and the tenant, and ConfigurationSetName when you set one. The call goes through MainSDK's network layer, is never retried by it, does not follow redirects, and reads at most 64 KiB of the answer. A 429, a 5xx or no answer is a retryable MAIN-EMAIL-0004, so a fallback driver is tried.
Bounces and complaints
Name an inbound webhook with the ses preset in email.webhook. MainSDK confirms the SNS subscription for the topics you list, then reads each event: a Permanent bounce and a complaint add the address to the suppression list, other bounces do not, and a delivery is only reported to email.on listeners. The tags tie each event to its campaign and tenant. Without email.webhook in production, mainsdk check warns with MAIN-EMAIL-0010.
For the notify email channel, add email to the channel list; it uses your email block:
notify:
channels: [push, sms, email]Use it
import { email } from "mainsdk";
export async function sendInvoice(to: string, invoiceId: string) {
return email.send({
to,
template: "invoice",
data: { invoiceId },
idempotencyKey: `invoice:${invoiceId}`,
});
}Through notify, SES delivers whenever email is the channel chosen:
import { notify } from "mainsdk";
await notify({ id: invoice.customerId }, {
template: "invoice.reminder",
data: { invoiceId: invoice.id },
category: "billing",
});Local development and testing
- No AWS account:
mainsdk devsends every email to the local inbox. - SES sandbox: while your account is in the sandbox, you can send to your own verified addresses. The SES mailbox simulator gives addresses that simulate delivery, bounces and complaints without hurting your sending reputation.
- Tests:
createTestApp()from@mainsdk/testingrunsemailon the local mailbox, andmailbox(app).failNext(1)simulates an outage.fakeMessagingServer("ses")records the signed request the real driver makes, with no network.
mainsdk check --env prod fails with MAIN-CHECK-0026 if email still resolves to the local driver.
What MainSDK adds on top
- Marketing email does not compile without an unsubscribe, and carries one-click
List-Unsubscribeheaders. - Unsubscribes, hard bounces and complaints are suppressed for every later send, in MainSDK, whichever provider sends.
- No open-tracking pixels or rewritten links by default.
- A retry with the same idempotency key never sends twice.
See Email for the full contract.
Limits and notes
- The SES driver declares its own host,
email.<region>.amazonaws.com, sonetwork.allowdoes not change. - The
email:sesdriver is built (specemail-notify).notifyhas no SES driver of its own: its email channel uses youremailblock. - Sandbox limits and production access are per region. Moving to another region means verifying identities and requesting production access again.
- SES keeps its own account-level suppression list as well. MainSDK checks its own list first, so a suppressed address never reaches SES.
email.raw()returns the SES client for featuresemaildoes not wrap, such as configuration sets.mainsdk checklists every use.
Swap to or from it
email:
driver: resend # was sesPoint key at the new provider's credential and verify your domain with it first. Email is stateless, so the swap needs no migration; see Swap a driver. To keep SES as a backup, use fallback: { email: [resend, ses] }.
FAQ
Why do my emails only reach some addresses? Your account is probably still in the SES sandbox in that region, which only sends to verified addresses. Request production access for the region.
Can SES send for my India users from India? Choose an SES region that matches your data region, verify your identity there and set email.region to it.
Do I keep SES's suppression list in sync? No. MainSDK keeps its own suppression list and checks it before every send.
Next
Was this page helpful?