The Loops alternative for AI agents MailKite offers a developer tool that gives AI agents an inbox to receive and reply to emails, addressing a gap in send-only email tools like Loops. The tool parses incoming emails as JSON and enables a receive→verify→think→reply loop, demonstrated in a runnable demo repository. The Loops alternative for AI agents Loops is a genuinely nice product-email tool for SaaS: a visual editor, lifecycle loops, and free transactional sends. But it's send-only — it gives an autonomous agent no inbox, so the agent can't read the verification code or the reply it's waiting on. MailKite which we build gives the agent a real address that arrives as parsed JSON with a receive→reply loop. For developers wiring an agent that has to read its own mail. Picture the moment it breaks. Your agent signs up for some SaaS with agent@myapp.ai , the service emails a six-digit code to that address, and the agent sits there waiting for a message that has nowhere to arrive. Loops sent the welcome email, the onboarding drip, and the receipt just fine. It has no idea that a code is trying to reach the agent, because receiving mail was never something it does. That gap — not price, not deliverability — is the whole reason a send-first product-email tool and an autonomous agent are a mismatch. Everything below is a repo you can run while you read. Clone demo-loops-ai-agent https://github.com/mailkite/demo-loops-ai-agent and npm start , or open it in StackBlitz https://stackblitz.com/github/mailkite/demo-loops-ai-agent?file=server.mjs real Node in a browser tab, no account where it runs on load. You don’t need a domain to see the loop run: a webhook normally needs a public URL, so npm start boots the server and self-fires a correctly signed email.received event at its own localhost — the whole receive→verify→think→reply loop runs in one command the reply is a dry-run, and managed-route.mjs dry-runs the hosted path too . It also ships the Loops path next to it in loops-contrast/ , so the contrast is something you can diff and test, not take on faith: a clean transactional send , and the receiving half that simply isn’t there. A domain comes in only when you want real inbound email to arrive. Here’s the receiving half Loops doesn’t have, whole: email in, verify the signature, hand the body to your model, reply through the same client. This is the heart of server.mjs in that repo — the repo wraps it in a dry-run harness and a stub agent so it runs with no key and no LLM, but the loop is exactly this, and it runs as pasted on Node 18+ npm install mailkite express : python import express from "express"; import { MailKite } from "mailkite"; const app = express ; const mk = new MailKite process.env.MAILKITE API KEY ; const SECRET = process.env.MAILKITE WEBHOOK SECRET; app.use "/hooks/agent", express.raw { type: "application/json" } ; app.post "/hooks/agent", async req, res = { // signature check, replay window, constant-time compare — one call if MailKite.verifyWebhook req.headers "x-mailkite-signature" , req.body, SECRET { return res.sendStatus 401 ; } res.sendStatus 200 ; // ack fast; run the agent out of band const event = JSON.parse req.body ; if event.type == "email.received" return; // Treat the body as untrusted INPUT, never as instructions see the security section . const answer = await runAgent { task: event.text, from: event.from.address, trusted: event.auth.spf === "pass" && event.auth.dmarc === "pass", } ; 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: answer.html, } ; } ; app.listen 3000 ; That’s the full receive→think→reply loop, whole: the agent hears, thinks, and answers. npm start boots the server and self-fires the exact payload MailKite’s delivery worker sends at its own localhost, so in one command you watch the agent read the task, take its trust verdict straight off event.auth , and dry-run the reply — no account or LLM required. mk.send returns { id, status } , and the identical handler shape exists for Python, Ruby, Go, PHP, and Java; see the receiving docs /docs/receiving and sending docs /docs/sending . The rest of this post is the honest version: where Loops genuinely wins, what a split stack asks of an agent builder who needs to receive, and what the parsed payload actually contains. Where Loops wins, honestly I want to be clear that Loops is a good product, and none of this is a knock on what it’s built for. Loops is one of the nicest developer experiences in product email right now. The visual editor is genuinely pleasant, the lifecycle “loops” model trigger an automated series off an event maps cleanly to how SaaS onboarding actually works, and unifying marketing, lifecycle, and transactional mail in one place means you’re not stitching two vendors together. Transactional sends are free on paid plans they used to be metered , sends are template-driven by a transactionalId you publish in the editor, and there are clean integrations with Clerk, Stripe, Supabase, and Polar so a signup or payment can kick off a series without glue code. If your job is “send lovely onboarding and product email from a SaaS,” Loops is a strong pick and this post isn’t trying to move you off it. The mismatch is narrower and specific: Loops is a send tool, and an autonomous agent’s hard problem is receiving . Two things people sometimes mistake for inbound on Loops, worth clearing up. Its incoming webhooks receive events from platforms like Stripe and Clerk to create contacts and trigger loops; they don’t receive email. Its outbound webhooks notify you of send-side events delivered, soft bounce, hard bounce , which is useful but is still telemetry about mail you sent, not mail arriving for you to read. Neither one is an inbox. I confirmed this against Loops’ own docs: there’s no MX setup, no inbound parse, no address that receives. What Loops asks of an agent builder So the honest DIY shape is a split stack. Loops keeps doing the send side it’s good at, and you bolt a real receiving provider next to it for the agent’s inbox, because Loops has no seam for that half. Here’s the send side in Loops’ own idiom — a template-based transactional call, which is genuinely clean. It’s loops-contrast/handler.mjs in the demo repo, sitting right next to the MailKite server.mjs above so you can put the two shapes side by side: // Loops: outbound only. This is the whole send side, and it's nice. await fetch "https://app.loops.so/api/v1/transactional", { method: "POST", headers: { Authorization: Bearer ${process.env.LOOPS API KEY} , "Content-Type": "application/json", }, body: JSON.stringify { transactionalId: "clfq6...", // a template you published in the Loops editor email: "ada@example.com", dataVariables: { code: "418207" }, // fills the template } , } ; // …now the agent needs to READ ada's reply — or the code a service just mailed it. // Loops has no endpoint for that. You add a second, receiving provider here. That comment is the whole story, and the repo makes it concrete: npm test builds that transactional payload and passes — the send side genuinely works — then asks the same handler to receive and gets null back, because loops-contrast/handler.mjs has no inbound endpoint to hand it. To give the agent an inbox you now run a second vendor or your own MX for receiving, wire up its inbound parse, verify its signatures, decode MIME, and reconcile threading between the two systems. None of that is exotic, but it’s a stack you assemble and keep alive specifically because the send-only tool can’t reach the receiving half of the loop. The comparison, no adjective inflation | Loops | MailKite | | |---|---|---| | Product/lifecycle email | Excellent visual editor + loops | API templates, no visual builder | | Transactional send | Template-based transactionalId , free on paid plans | from / to / subject / html + inReplyTo | | Inbound email an agent’s inbox | None — send-only | Parsed JSON webhook per address | | Read a verification code / reply | Not possible | text + html + threadId delivered | | SPF/DKIM/DMARC on inbound | n/a no inbound | auth block on every message | | Reply threading | n/a | inReplyTo threads the reply | | Run the agent for you | No | Route action: 'agent' on a queue | | Pricing model | By subscribed contacts, from $49/mo free: ~4k sends / 1k contacts | Metered; free 3,000/mo in + out, no per-domain fee | The through-line: Loops wins the product-email experience — the editor and lifecycle model are better than anything MailKite offers there. MailKite wins the thing an agent actually needs, which is a real inbox and the receive→reply loop. They’re not the same job. What actually hits your agent’s webhook Here’s what MailKite which we build POSTs when mail arrives for the agent. No MIME to parse, a resolved threadId , attachments as short-lived signed URLs, and an auth block: { "id": "msg 2Hk9…", "type": "email.received", "from": { "address": "no-reply@acme.dev" }, "to": { "address": "agent@myapp.ai" } , "subject": "Your verification code", "text": "Your code is 418207. It expires in 10 minutes.", "html": "