{"slug": "the-amazon-ses-alternative-for-ai-agents", "title": "The Amazon SES alternative for AI agents", "summary": "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.", "body_md": "# The Amazon SES alternative for AI agents\n\nAmazon 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.\n\nAn 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.\n\nHere’s the whole MailKite side: the agent’s receive→think→reply loop. It runs as pasted on Node 18+, one dependency (`npm install mailkite`\n\n).\n\n```\n// server.mjs — the whole agent loop. Full repo: github.com/mailkite/demo-amazon-ses-ai-agent\nimport { createServer } from \"node:http\";\nimport { MailKite } from \"mailkite\";\n\nconst mk = new MailKite(process.env.MAILKITE_API_KEY);\nconst SECRET = process.env.MAILKITE_WEBHOOK_SECRET;\n\ncreateServer(async (req, res) => {\n  let raw = \"\"; for await (const c of req) raw += c;\n  // signature check, replay window, constant-time compare — one call\n  if (!MailKite.verifyWebhook(req.headers[\"x-mailkite-signature\"], raw, SECRET)) {\n    return res.writeHead(401).end();\n  }\n  res.writeHead(200).end(\"ok\");                     // ack fast; run the agent out of band\n\n  const event = JSON.parse(raw);                    // already parsed — no S3, no MIME parser\n  if (event.type !== \"email.received\") return;\n\n  // Body is UNTRUSTED input, not instructions. Weight it by the auth block.\n  const trusted = event.auth.spf === \"pass\" && event.auth.dmarc === \"pass\";\n  const reply = await runAgent({ task: event.text, from: event.from.address, trusted });\n\n  await mk.send({\n    from: event.to[0].address,       // reply from the address it was sent to\n    to: event.from.address,\n    subject: `Re: ${event.subject}`,\n    inReplyTo: event.id,             // threads the reply\n    html: reply.html,\n  });\n}).listen(3000);\n```\n\nThat’s a fully autonomous email agent: it hears, thinks, and answers. `mk.send()`\n\nreturns `{ id, status }`\n\nso 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`\n\n.\n\n## Where SES wins for agents, honestly\n\nSES 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.\n\n## What SES asks of an agent builder\n\nTwo things stand between an SES account and an agent that can read and answer its own mail. The first is on the way out.\n\nA 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.\n\nThe 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.\n\nHere is the inbound half, honestly, in SES’s own idiom, *after* you’ve built the receipt rule, bucket policy, SNS topic, and IAM role:\n\n```\n// SES inbound: receipt rule → S3 → this Lambda → parse the MIME yourself, then act\nimport { S3Client, GetObjectCommand } from \"@aws-sdk/client-s3\";\nimport { simpleParser } from \"mailparser\";\n\nconst s3 = new S3Client({});\n\nexport const handler = async (event) => {\n  // the event has headers but no body — go get the raw email from S3\n  const { bucketName, objectKey } = event.Records[0].ses.receipt.action;\n  const obj = await s3.send(new GetObjectCommand({ Bucket: bucketName, Key: objectKey }));\n  const mail = await simpleParser(await obj.Body.transformToString());\n\n  const code = mail.text?.match(/\\b\\d{6}\\b/)?.[0];  // your parsing, your regex, your job\n  // ...now run the agent. And to actually email a reply back, the account\n  //    must already be OUT of the SES sandbox. See the outbound half.\n};\n```\n\nThat 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.\n\n## The comparison, agent-relevant rows only\n\n| Amazon SES | MailKite | |\n|---|---|---|\n| Agent’s first reply | Blocked until AWS approves production access | Send once the domain’s DNS verifies |\n| Reading an inbound email | Raw MIME → S3 → SNS/Lambda → you parse | One parsed JSON webhook |\n| Verification codes / magic links | Parse them out of MIME yourself | `event.text` / `event.html` , already decoded |\n| Threading a reply | Reconstruct from headers | `inReplyTo: event.id` , resolved `threadId` |\n| Trust signal for injection defense | Derive SPF/DKIM/DMARC yourself | `auth` block in every payload |\n| Setup to receive | Receipt rules + S3 + SNS/Lambda + IAM | One webhook URL |\n| Who runs the loop | You (and the pipeline under it) | You, or a route with `action: \"agent\"` |\n| Free tier | ~$200 in credits, 6 months (new accounts) | 3,000 messages/mo, in + out, no per-domain fee |\n\nThe 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.\n\n## What actually hits your agent’s webhook\n\nThe same inbound email, delivered parsed. No S3 round-trip, no MIME parser, and an `auth`\n\nblock so the agent never has to re-derive SPF/DKIM/DMARC to decide how much to trust the sender:\n\n```\n{\n  \"id\": \"msg_2Hk9…\",\n  \"type\": \"email.received\",\n  \"from\": { \"address\": \"ada@example.com\" },\n  \"to\": [{ \"address\": \"agent@myapp.ai\" }],\n  \"subject\": \"Re: invoice #1042\",\n  \"text\": \"Looks good — approved!\",\n  \"html\": \"<p>Looks good — approved!</p>\",\n  \"threadId\": \"<a1b2c3@mail.example.com>\",\n  \"auth\": { \"spf\": \"pass\", \"dkim\": \"pass\", \"dmarc\": \"pass\", \"spam\": \"ham\" },\n  \"attachments\": [\n    { \"id\": \"msg_2Hk9…:0\", \"filename\": \"po.pdf\", \"contentType\": \"application/pdf\",\n      \"size\": 18213, \"url\": \"https://api.mailkite.dev/att/2Hk9…/0?exp=…&sig=…\" }\n  ]\n}\n```\n\nThat `auth`\n\nblock is load-bearing, and not because it’s convenient. Inbound email is untrusted input: `From:`\n\nis plain text, so anyone can forge a sender and then tell your agent what to do in the body. Checking `auth`\n\nbefore you weight a sender’s instructions is necessary but not sufficient. You can’t prompt your way out of prompt injection; the real defense is bounding what a fooled agent can do. That reasoning has its own post ([agent inbox security by design](/blog/agent-inbox-security-by-design/)), and it’s worth reading before either loop touches anything that matters.\n\nIf you’d rather not host the loop at all, MailKite, which we build, can run the agent for you: a route whose `action`\n\nis `\"agent\"`\n\ncarries a free-text `agentPrompt`\n\n, and each inbound message runs the model loop on a durable queue with a full transcript you can drill into. Same parsed inbound edge, no server of yours in the path. The [inbox-agents docs](/docs/inbox-agents) have the shape.\n\n## FAQ\n\n**Can a new Amazon SES account send email right away?**\nNo. New accounts are sandboxed: you can only send to verified addresses, capped at 200 messages a day and 1 per second, until you request production access and AWS approves it. That approval is a human review with an initial response within 24 hours and no guaranteed turnaround. MailKite has no sandbox; once your domain passes SPF + DKIM, an agent can send to anyone.\n\n**How does an agent read a verification code from SES?**\nIt doesn’t get it handed over. SES writes the raw MIME email to S3 and invokes a Lambda whose event has no body, so the Lambda fetches the object from S3 and parses the MIME (commonly with `mailparser`\n\n) to find the code. MailKite delivers the decoded `text`\n\nand `html`\n\nin the `email.received`\n\nwebhook, so the code is a field, not a parsing job.\n\n**Does SES give me a parsed inbound webhook like MailKite?**\nNo. SES inbound actions deliver the raw, unmodified message to S3 or over SNS, and you own the parsing. MailKite decodes the message at the edge and POSTs parsed JSON with `text`\n\n, `html`\n\n, a resolved `threadId`\n\n, attachments as signed URLs, and an `auth`\n\nresult.\n\n**Do I have to leave AWS to use MailKite?**\nNo. MailKite is a plain HTTPS webhook and REST API, so the receiver can be the Lambda, ECS task, or EC2 box your agent already runs on. You’re replacing the SES receiving pipeline and the sandbox, not your infrastructure.\n\n**Is SES cheaper than MailKite?**\nPer email at high volume, SES is about the cheapest anywhere, and I won’t pretend otherwise. But its inbound path adds S3, SNS, and Lambda costs plus the engineering time to build and operate the pipeline, and MailKite starts free at 3,000 messages a month in and out with no per-domain fee. For an agent workload, total cost of ownership favors MailKite until you’re sending at real scale.\n\nIf SES has your agent waiting on a sandbox approval to send its first reply, or maintaining a receipt-rule-to-S3-to-Lambda pipeline just to read a code, there’s a simpler shape. Clone the [demo repo](https://github.com/mailkite/demo-amazon-ses-ai-agent) (or [run it in your browser](https://stackblitz.com/github/mailkite/demo-amazon-ses-ai-agent?file=server.mjs)), then [point a domain at MailKite](/docs/quickstart) and your agent’s next inbound email arrives as parsed JSON.\n\n*Related: the pillar on giving your agent an inbox, agent inbox security by design, the full MailKite vs Amazon SES comparison, and the SES alternative for developers.*", "url": "https://wpnews.pro/news/the-amazon-ses-alternative-for-ai-agents", "canonical_source": "https://mailkite.dev/blog/amazon-ses-for-ai-agents/", "published_at": "2026-07-04 00:00:00+00:00", "updated_at": "2026-07-04 00:59:08.812383+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-infrastructure"], "entities": ["MailKite", "Amazon SES", "AWS", "Bedrock", "DynamoDB", "Lambda", "ECS", "StackBlitz"], "alternates": {"html": "https://wpnews.pro/news/the-amazon-ses-alternative-for-ai-agents", "markdown": "https://wpnews.pro/news/the-amazon-ses-alternative-for-ai-agents.md", "text": "https://wpnews.pro/news/the-amazon-ses-alternative-for-ai-agents.txt", "jsonld": "https://wpnews.pro/news/the-amazon-ses-alternative-for-ai-agents.jsonld"}}