# The Nylas alternative for AI agents

> Source: <https://mailkite.dev/blog/nylas-for-ai-agents/>
> Published: 2026-07-04 00:00:00+00:00

# 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 webhook that notifies with IDs so you go fetch the body. 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.

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. 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) => {
  // 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);
```

The agent gets a full, decoded message in one push. There’s no OAuth app to register, no mailbox to sign into, and no second API call to fetch the body. The identical handler shape exists for Python, Ruby, Go, PHP, and Java; see the [receiving docs](/docs/receiving) and [sending docs](/docs/sending). A companion repo, [ demo-nylas-ai-agent](https://github.com/mailkite/demo-nylas-ai-agent), has this running with a signed sample event you can fire;

[open it in StackBlitz](https://stackblitz.com/github/mailkite/demo-nylas-ai-agent?file=server.mjs).

## 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 (from [ demo-nylas-ai-agent](https://github.com/mailkite/demo-nylas-ai-agent), the

`nylas-contrast/`

folder):

``` python
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 | Webhook notifies with IDs, you GET the message; body stripped over 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=…" }
  ]
}
```

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](/blog/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 notify-then-fetch, and pushes the parsed message straight to your loop. You can also hand the loop to us: 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](/blog/give-your-agent-an-inbox/).

## 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](https://github.com/mailkite/demo-nylas-ai-agent) (or

[run it in your browser](https://stackblitz.com/github/mailkite/demo-nylas-ai-agent?file=server.mjs)), then

[point a domain at MailKite](/docs/quickstart)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.*
