The Amazon SES alternative for AI agents MailKite launches an Amazon SES alternative for AI agents, providing parsed JSON inbound emails and a receive-think-reply loop without the need for MIME parsing or S3 buckets, while SES requires sandbox approval and manual plumbing for autonomous email handling. The Amazon SES alternative for AI agents Amazon SES can send and receive, but it hands an autonomous agent a human-reviewed sandbox before it can email anyone and raw MIME in an S3 bucket before it can read a reply. MailKite which we build gives the agent a real inbox: inbound arrives as parsed JSON and a receive→think→reply loop runs with no pipeline to operate. For developers wiring an agent to email. An agent’s first job on email is usually the least glamorous one: read a six-digit code out of a signup message and type it back, or take a task someone emailed in and answer it. On SES that job starts with plumbing, not with the agent. Here’s the same inbound email on each side, and everything the agent has to run to act on it. Here’s the whole MailKite side: the agent’s receive→think→reply loop. It runs as pasted on Node 18+, one dependency npm install mailkite . // server.mjs — the whole agent loop. Full repo: github.com/mailkite/demo-amazon-ses-ai-agent import { createServer } from "node:http"; import { MailKite } from "mailkite"; const mk = new MailKite process.env.MAILKITE API KEY ; const SECRET = process.env.MAILKITE WEBHOOK SECRET; createServer async req, res = { let raw = ""; for await const c of req raw += c; // signature check, replay window, constant-time compare — one call if MailKite.verifyWebhook req.headers "x-mailkite-signature" , raw, SECRET { return res.writeHead 401 .end ; } res.writeHead 200 .end "ok" ; // ack fast; run the agent out of band const event = JSON.parse raw ; // already parsed — no S3, no MIME parser if event.type == "email.received" return; // Body is UNTRUSTED input, not instructions. Weight it by the auth block. const trusted = event.auth.spf === "pass" && event.auth.dmarc === "pass"; const reply = await runAgent { task: event.text, from: event.from.address, trusted } ; await mk.send { from: event.to 0 .address, // reply from the address it was sent to to: event.from.address, subject: Re: ${event.subject} , inReplyTo: event.id, // threads the reply html: reply.html, } ; } .listen 3000 ; That’s a fully autonomous email agent: it hears, thinks, and answers. mk.send returns { id, status } so you can log the outbound message, and the identical shape exists for Python, Ruby, Go, PHP, and Java see the receiving docs /docs/receiving and sending docs /docs/sending . Open it in StackBlitz https://stackblitz.com/github/mailkite/demo-amazon-ses-ai-agent?file=server.mjs , fire the sample event, and watch a parsed email hit runAgent . Where SES wins for agents, honestly SES is cheap and it is capable, and I’m not going to pretend otherwise. Outbound is about $0.10 per 1,000 emails, inbound is roughly the same per 1,000 received, and AWS sending IPs have years of reputation behind them. If your agent already lives in AWS its brain is a Bedrock call, its state is in DynamoDB, its runtime is Lambda or ECS , then SES is one more service in a console you already operate, billed on one invoice you already read. Inbound receiving is available in most SES regions now, not the tiny handful it used to be, so region availability is rarely the blocker people remember it as. For a team fluent in IAM and CloudWatch, none of the pieces below are exotic. The catch is that “not exotic” and “not your problem” are different things, and for an autonomous agent the difference is most of the work. What SES asks of an agent builder Two things stand between an SES account and an agent that can read and answer its own mail. The first is on the way out. A brand-new SES account is in the sandbox . Until you request production access and AWS grants it, the account can only send to verified addresses, at most 200 messages per 24 hours and 1 per second. Production access is a human review: AWS commits to an initial response within 24 hours, not an approval, and says it may take longer if it needs more information. The sandbox is also per-region, so an agent that deploys to a second region is sandboxed again there. For an agent whose whole point is to email an arbitrary person on its first run confirm a booking, reply to a lead, answer a support thread , that’s a gate with no SLA sitting exactly where launch day is. The second is on the way in. SES can receive email, but it does not hand you a parsed webhook. A receipt rule writes the raw, unmodified MIME to an S3 bucket and notifies you over SNS or by invoking a Lambda. The Lambda event carries headers and metadata but not the message body, so the function has to fetch the object from S3 and parse the MIME itself. If you route the whole email inline through SNS instead, anything over 150 KB bounces, so real messages with attachments force the S3 round-trip anyway. Every resource in that chain the SNS topic, the Lambda, a KMS key if you encrypt has to live in the same region as the receiving endpoint. Here is the inbound half, honestly, in SES’s own idiom, after you’ve built the receipt rule, bucket policy, SNS topic, and IAM role: // SES inbound: receipt rule → S3 → this Lambda → parse the MIME yourself, then act import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"; import { simpleParser } from "mailparser"; const s3 = new S3Client {} ; export const handler = async event = { // the event has headers but no body — go get the raw email from S3 const { bucketName, objectKey } = event.Records 0 .ses.receipt.action; const obj = await s3.send new GetObjectCommand { Bucket: bucketName, Key: objectKey } ; const mail = await simpleParser await obj.Body.transformToString ; const code = mail.text?.match /\b\d{6}\b/ ?. 0 ; // your parsing, your regex, your job // ...now run the agent. And to actually email a reply back, the account // must already be OUT of the SES sandbox. See the outbound half. }; That runs, and it’s the pattern AWS itself documents. But it’s the visible tip. Below the agent sit the receipt rules, the bucket lifecycle, the SNS subscriptions, the IAM policies, the MIME parser’s edge cases encodings, multipart, attachments , and separately a bounce-and-complaint pipeline you subscribe to and act on, because if those rates drift up AWS pauses your sending. That is the pipeline the agent needs standing before it reads one code. The comparison, agent-relevant rows only | Amazon SES | MailKite | | |---|---|---| | Agent’s first reply | Blocked until AWS approves production access | Send once the domain’s DNS verifies | | Reading an inbound email | Raw MIME → S3 → SNS/Lambda → you parse | One parsed JSON webhook | | Verification codes / magic links | Parse them out of MIME yourself | event.text / event.html , already decoded | | Threading a reply | Reconstruct from headers | inReplyTo: event.id , resolved threadId | | Trust signal for injection defense | Derive SPF/DKIM/DMARC yourself | auth block in every payload | | Setup to receive | Receipt rules + S3 + SNS/Lambda + IAM | One webhook URL | | Who runs the loop | You and the pipeline under it | You, or a route with action: "agent" | | Free tier | ~$200 in credits, 6 months new accounts | 3,000 messages/mo, in + out, no per-domain fee | The through-line: SES wins raw per-email price and AWS-grade sending IPs. For an agent, MailKite wins the two things it actually blocks on: sending without a review, and reading mail without a pipeline. What actually hits your agent’s webhook The same inbound email, delivered parsed. No S3 round-trip, no MIME parser, and an auth block so the agent never has to re-derive SPF/DKIM/DMARC to decide how much to trust the sender: { "id": "msg 2Hk9…", "type": "email.received", "from": { "address": "ada@example.com" }, "to": { "address": "agent@myapp.ai" } , "subject": "Re: invoice 1042", "text": "Looks good — approved ", "html": "