All posts
The Nylas alternative for AI agents
Gabe 20 min read

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.

Nylas vs MailKite
Nylas vs MailKite — the same job (an inbox for your AI agent), two approaches.

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.

Nylas Gmail/M365existing mailbox OAuth grantgrant_id notify hookmessage IDs GET + modelyour loop send as useras the human Production needs your own OAuth app + a CASA review · handle created + updated · replies go from the human's account MailKite email inagent's address MX edgeparse + auth signed hookverifyWebhook your agentyour model mk.send()reply out Blue = operated by MailKite. The agent gets its own address, the webhook is signed and already parsed, and a route with action:'agent' can run the loop for you.
Two agent email paths. Nylas federates a human's mailbox and notifies you to go fetch. MailKite gives the agent its own address and pushes the parsed message.

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.

  • It lives in the human's real inbox. One grant reaches their existing Gmail, Microsoft 365, Exchange, Yahoo, iCloud, or IMAP account. If the agent has to see the mail a person already has, MailKite can't do that and Nylas can.
  • It sends as the user. A reply goes out through the connected provider as that person, landing in the existing thread with the account's own reputation behind it. No new domain to warm.
  • Calendar and contacts in the same API. An assistant that reads mail, checks free/busy, and books a meeting is one integration, not three.
  • It's moving toward agent-owned mailboxes too. Nylas now has an Agent Accounts product (in beta) that provisions a Nylas-hosted mailbox for an agent, so the "give the agent its own address" case isn't foreign to them, it's just newer, hosted by Nylas rather than on a domain you own, and on the paid plan.

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:

  • A real mailbox has to exist. A grant federates an account that's already there. To give the agent its own address you first create a Gmail/Workspace or Microsoft account somewhere and manage it, or buy the hosted Agent Accounts add-on.
  • Your own OAuth app, then Google's review. Production Gmail access with restricted scopes (gmail.readonly, gmail.modify) needs your own Google Cloud app to pass OAuth verification and an annual CASA security assessment. Nylas sells a pre-verified "Shared GCP App" to skip it, but only as a paid add-on.
  • Handle both triggers. Nylas' own docs tell you to handle message.created and message.updated to catch every new message. The payload carries the parsed message inline; only when it tops 1 MB is the body stripped (a .truncated suffix) and you re-query.
  • Provider caps still apply. You send as the underlying account, so Gmail's roughly 2,000 messages/day send limit and per-user request quotas are yours to live inside, on top of Nylas' own per-grant rate limits.

Here’s the setup chain an agent’s own Nylas mailbox walks before it reads a single email, top to bottom:

provision a mailboxa real Gmail / M365 account build your OAuth appGCP project or Entra app Google review + CASArestricted scopes, weeks hosted auth → grantuser consents, store grant_id webhook: IDs onlymessage.created / .updated GET message, then actfetch body, run your loop Boxes one through five are setup and plumbing you own before the agent reads one email. On MailKite, DNS verification is the setup and the blue box is the whole loop.
Standing up an agent's own mailbox on Nylas, stage by stage. MailKite collapses the gray stages into a DNS record and a signed webhook.

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

NylasMailKite
The addressFederates 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 productionYour own OAuth app + Google verification + annual CASA for restricted scopesDNS-verify (SPF+DKIM to send, MX to receive), no review
Inbound deliveryParsed message inline on the webhook; re-fetch only when it tops 1 MBFull parsed message pushed as one signed JSON webhook
Sender trust signalCall the Messages API and read headers yourselfauth block (SPF/DKIM/DMARC/spam) in the payload
Credential upkeepOAuth grants Nylas refreshes; provider send caps (Gmail ~2k/day)An API key; no per-mailbox OAuth or token refresh
Free tier5 connected accounts, sandbox/testing only3,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 loopYouYou, 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.

Discuss this post: Hacker News Share on X Share on LinkedIn

Related posts