The Nylas alternative for AI agents
Nylas connects an agent to a human's existing Gmail or Outlook over OAuth, which is the right tool when the agent must act inside a real person's mailbox. Production means your own verified OAuth app, a CASA security review, token refresh, and a reply that goes out as the connected human. MailKite (which we build) gives the agent its own scoped address on a domain you control and pushes the parsed message as JSON. For developers wiring an autonomous email agent.
The unit Nylas hands you is a grant: one authenticated connection to one human’s existing mailbox, reached over OAuth. That is exactly what you want when the agent’s job is to live inside a real person’s Gmail or Microsoft 365 account and act on their behalf. It’s a different shape from what an autonomous agent usually needs, which is its own address that nobody signs into. This post is the honest version of that split: where Nylas is the correct pick, what its agent path costs to stand up, and the 25 lines that are the whole MailKite side.
Everything below is a repo you can run while you read. Clone demo-nylas-ai-agent and npm start, or open it in StackBlitz (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). It also ships the Nylas path next to it in nylas-contrast/, so every contrast here is something you can diff and test, not take on faith. A domain comes in only when you want real inbound email to arrive.
Here’s the whole bring-your-own-agent loop on MailKite. 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 — it wraps the loop 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):
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) => {
// HMAC signature, 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;
// The body is untrusted INPUT, never 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 receive→think→reply loop, whole. npm start boots the server and self-fires the exact payload MailKite’s delivery worker sends, 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, no OAuth app to register, no mailbox to sign into, and no OAuth token refresh to babysit. The identical handler shape exists for Python, Ruby, Go, PHP, and Java; see the receiving docs and sending docs.
Where Nylas wins for agents, honestly
Nylas is not a send-first ESP wearing an inbound hat. It’s a bidirectional email, calendar, and contacts API, and it does one thing MailKite deliberately does not: it operates a human’s existing account. If the agent’s job is to read the actual mail sitting in someone’s Gmail or Outlook, reconcile it against their calendar, and reply from their real address, Nylas is built precisely for that.
So this isn’t a “Nylas can’t do email” post. It does a great deal, and for the operate-a-real-person’s-mailbox job it’s the right call. The friction shows up when what you actually want is an autonomous agent with its own address that no human ever logs into.
What Nylas asks of an agent builder
The classic Email API path is: connect a mailbox, get told when something happens, then go read it. Concretely, that’s a hosted-OAuth code exchange to mint a grant, and a webhook that hands you IDs so you call the Messages API for the body. Here’s that path in Nylas’ own idiom — it’s nylas-contrast/handler.mjs in the same repo, sitting right next to the MailKite server.mjs above so you can put them side by side:
import Nylas from "nylas";
const nylas = new Nylas({ apiKey: process.env.NYLAS_API_KEY });
// 1) One-time per user: exchange the hosted-auth code for a grant you store.
const { grantId } = await nylas.auth.exchangeCodeForToken({
clientId: process.env.NYLAS_CLIENT_ID,
code, // from your /callback after the OAuth redirect
redirectUri: process.env.REDIRECT_URI,
}); // Nylas refreshes the provider token for you from here on
// 2) The webhook is a NOTIFICATION with IDs, not the message body.
app.post("/nylas/webhook", async (req, res) => {
res.sendStatus(200); // ack within ~10s, then work async
const { type, data } = req.body;
if (type !== "message.created") return; // you also have to handle message.updated
const msg = await nylas.messages.find({ // GET /v3/grants/{grant}/messages/{id}
identifier: data.grant_id,
messageId: data.object.id,
});
const answer = await runAgent({ task: msg.data.body }); // untrusted input
await nylas.messages.send({ // sends AS the connected human's mailbox
identifier: data.grant_id,
requestBody: {
to: msg.data.from,
subject: `Re: ${msg.data.subject}`,
replyToMessageId: data.object.id,
body: answer.html,
},
});
});
The code is clean. What that snippet doesn’t show is the part that takes weeks, not minutes:
Here’s the setup chain an agent’s own Nylas mailbox walks before it reads a single email, top to bottom:
None of this is a knock if the agent is meant to run inside a person’s account. It’s exactly the wrong amount of ceremony if you just want agent@yourco.dev answering its own mail.
The comparison, no adjective inflation
| Nylas | MailKite | |
|---|---|---|
| The address | Federates a human’s existing Gmail/M365/IMAP (Agent Accounts beta for an agent-owned one) | Agent’s own scoped address on a domain you control |
| Getting to production | Your own OAuth app + Google verification + annual CASA for restricted scopes | DNS-verify (SPF+DKIM to send, MX to receive), no review |
| Inbound delivery | Parsed message inline on the webhook; re-fetch only when it tops 1 MB | Full parsed message pushed as one signed JSON webhook |
| Sender trust signal | Call the Messages API and read headers yourself | auth block (SPF/DKIM/DMARC/spam) in the payload |
| Credential upkeep | OAuth grants Nylas refreshes; provider send caps (Gmail ~2k/day) | An API key; no per-mailbox OAuth or token refresh |
| Free tier | 5 connected accounts, sandbox/testing only | 3,000 messages/mo, inbound + outbound |
| Per-mailbox cost | ~$2.00 per connected account/mo over the included 5 (Full Platform) | No per-domain or per-address fee |
| Who runs the loop | You | You, or a route with action: 'agent' |
The through-line: Nylas wins when the agent must operate a real human’s existing mailbox, calendar, and contacts. MailKite wins when the agent should just have its own address, with the message pushed already-parsed and no OAuth app between you and the first email.
What actually hits your agent’s webhook
MailKite decodes the message at the edge, so your handler gets fields, not IDs to go fetch and not MIME to parse:
{
"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": "<p>Looks good — approved!</p>",
"threadId": "<a1b2c3@mail.example.com>",
"auth": { "spf": "pass", "dkim": "pass", "dmarc": "pass", "spam": "ham" },
"attachments": [
{ "id": "msg_2Hk9…:0", "filename": "po.pdf", "contentType": "application/pdf",
"size": 18213, "url": "https://api.mailkite.dev/att/2Hk9…/0?exp=…&sig=…" }
]
}
Don’t take the shape on faith — fire one and watch it come back, auth block and all. Spoof the SPF/DKIM to fail and see the verdict flip; on Nylas that verdict isn’t in the payload at all — you’d call the Messages API and read headers to derive it yourself:
That auth block is the part an agent needs and a bare inbox API doesn’t hand you. From: is plain text, so a sender is trivially forged, and the moment your agent follows what an email says, the body is a prompt-injection vector. The verdicts let the loop weight a sender before it acts. They’re necessary, not sufficient: treat the body as untrusted data, bound the agent’s authority, and read agent inbox security by design before you point it at anything that matters.
Where I won’t overclaim
MailKite, which we build, is an inbound-to-webhook platform that also sends, and it gives an agent its own scoped address on a domain you control with no OAuth, no IMAP, and no token refresh. What it does not do is reach into a mailbox that already exists. If your agent’s whole reason to be is reading and replying inside a specific person’s Gmail or Outlook, Nylas is the tool and I’m not going to pretend otherwise. The claim here is narrow: for an autonomous agent that needs its own inbox, MailKite skips the provider mailbox, the verified OAuth app, the CASA review, and the token refresh, and pushes the parsed message straight to your loop. The repo runs it both ways: bring your own (server.mjs), the code at the top — the inbound webhook hits your endpoint, your model runs, you reply with mk.send() — or hand the loop to us (managed-route.mjs): a route with action: 'agent' runs the model on a durable queue and keeps a per-route transcript, described in give your agent an inbox. And npm test runs the Nylas side (nylas-contrast/handler.mjs) right next to it, showing the grant-scoped path — the body is inline until a payload tops 1 MB (then a re-fetch), it ships no auth verdict, and the reply goes out as the connected human — so you can watch the difference before you decide which shape you want.
FAQ
Can Nylas give an AI agent its own email address? Newly, yes: Nylas has an Agent Accounts product (in beta) that provisions a Nylas-hosted mailbox for an agent. The classic Email API doesn’t; it federates a human’s existing Gmail, Outlook, or IMAP account over OAuth. MailKite gives the agent its own address on a domain you own, and pushes inbound as parsed JSON on the free tier.
Does Nylas receive inbound email, and in what shape?
Yes. You subscribe to webhooks like message.created and message.updated, but the notification carries message IDs, not the body. You then call GET /v3/grants/{grant_id}/messages/{id} to fetch the parsed message, and re-query if the payload exceeded 1 MB and got truncated. MailKite delivers the full parsed message in the webhook itself.
Do I need Google’s OAuth app review to use Nylas in production?
For restricted Gmail scopes (gmail.readonly, gmail.modify, gmail.compose), yes: your own Google Cloud app must pass OAuth verification plus an annual CASA security assessment, which can take weeks. Nylas sells a pre-verified Shared GCP App add-on to skip it on paid plans. MailKite has no OAuth app and no review; you verify a domain via DNS.
How is Nylas priced for an agent that connects many mailboxes? Per connected account. The free tier is 5 connected accounts for sandbox and testing only; the Full Platform plan is about $15/month including 5 accounts, then roughly $2.00 per additional connected account per month (confirm current numbers on Nylas’ pricing page). MailKite has no per-address or per-domain fee and a 3,000 message/month free tier.
Which should I pick for an email agent? If the agent must operate a real person’s existing mailbox, calendar, and contacts, pick Nylas. If the agent should have its own autonomous address with parsed inbound pushed to it and no OAuth to manage, pick MailKite. They solve adjacent problems, not the same one.
If you’re reaching for Nylas only to give an agent an address it can call its own, that’s a lot of OAuth ceremony for a mailbox nobody logs into. Clone demo-nylas-ai-agent (or run it in your browser), then point a domain at MailKite and your next inbound email arrives at the agent as parsed JSON.
Related: the pillar on giving your AI agent its own email inbox, and agent inbox security by design.