Microsoft Graph gives an agent a real Outlook mailbox, at the cost of an Entra app registration, admin-consented (tenant-wide) mail permissions, and a change-notification subscription that expires every few days. MailKite gives the agent its own scoped address on a domain you control and pushes parsed, authenticated JSON to a receive→reply loop — nothing to register, renew, or keep alive.
Graph is the right tool — sometimes the only tool — when the agent must operate inside a real Microsoft 365 tenant: one consistent API across mail, calendar, contacts, Teams, OneDrive, and SharePoint, carrying the mailbox's real history, identity, and compliance. Nothing on the market matches that reach into M365.
Skip the Entra ID app registration, the client secret or certificate (which itself expires), and the admin-consent chase. You DNS-verify a domain and point a webhook at it — the address is the whole setup.
Graph application permissions like Mail.Read read every mailbox in the tenant by default, and Mail.Send sends as any user — so you add RBAC for Applications and remember an unscoped grant isn't narrowed by it. On MailKite the address is the scope; there's nothing wider to confine.
Graph mail subscriptions cap under 7 days (under 1 day if you include the body); a dropped renewal cron silently stops inbound. MailKite's webhook is durable — no expirationDateTime, no PATCH-on-a-cron, no validationToken handshake.
A Graph change notification is a pointer: you GET the message with an app token to read it, and nothing tells you if SPF/DKIM/DMARC passed. MailKite POSTs the full parsed message with an auth block already resolved — no second round-trip, no re-deriving sender trust.
| MailKite | Microsoft Graph (Outlook) | |
|---|---|---|
| Give the agent an inbox | Scoped address on a domain you control | Licensed M365 mailbox + Entra app |
| Setup to start | DNS-verify, one webhook URL | App registration + admin consent (tenant-wide) |
| Inbound delivery | One parsed JSON webhook | Change-notification ping → you GET the message |
| Push subscription | Durable — nothing to renew | Expires (≤7d, ≤1d with body); renew on a cron |
| Scope to one mailbox | The address is the scope | RBAC for Applications / access policy |
| Sender auth for the agent | auth block in every payload | You derive SPF/DKIM/DMARC yourself |
| Reach beyond mail (calendar, Teams, files) | Email only | One API across all of M365 |
| Cost floor | Free: 3,000 msg/mo, no per-domain fee | Per-user license (~$4–6/mo) + your infra |
Competitor capabilities change — we re-audit these tables regularly. Spot something out of date? Tell us and we'll fix it.
Graph hands you a pointer you must authenticate, GET, and re-subscribe to keep alive; MailKite POSTs the parsed message with the auth verdict already resolved.
// Needs an Entra app with admin-consented application Mail.Read,
// ideally RBAC-scoped to one mailbox. A notification is a pointer,
// not the message — you GET it with an app token.
await fetch("https://graph.microsoft.com/v1.0/subscriptions", {
method: "POST",
headers: { authorization: `Bearer ${appToken}`, "content-type": "application/json" },
body: JSON.stringify({
changeType: "created",
notificationUrl: NOTIFY_URL,
resource: `/users/${MAILBOX}/mailFolders('inbox')/messages`,
expirationDateTime: new Date(Date.now() + 6 * 864e5).toISOString(), // lapses — re-PATCH it
}),
});
// On your notify endpoint:
// 1. validation handshake — echo ?validationToken back, text/plain, 200, within 10s
// 2. a change notification arrives → GET the message with a fresh app token
const { value } = JSON.parse(body);
const msg = await fetch(`https://graph.microsoft.com/v1.0/${value[0].resource}`, {
headers: { authorization: `Bearer ${await token()}` },
}).then((r) => r.json());
// msg.body.content is HTML; nothing here tells you if SPF/DKIM/DMARC passed. POST /your-webhook Content-Type: application/json
x-mailkite-signature: t=…,v1=… (HMAC-SHA256 — verify locally)
{
"id": "msg_2Hk9…",
"type": "email.received",
"from": { "address": "ada@example.com" },
"to": [{ "address": "support@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": [
{ "filename": "po.pdf", "contentType": "application/pdf",
"size": 18213, "url": "https://api.mailkite.dev/att/2Hk9…/0?sig=…" }
]
} Graph's mailbox needs a licensed account and you host the subscription lifecycle yourself; MailKite's agent address lives on a domain you already control, with no per-domain fee and one combined quota.
DNS-verify a domain (MX to receive, SPF + DKIM to send) and pick an address like agent@yourco.dev. No Entra app, no admin to chase for consent.
Delete the token cache, the /subscriptions POST, the validationToken handshake, and the renewal cron. Point one webhook URL at your endpoint — inbound arrives as email.received, already parsed.
Verify x-mailkite-signature with the SDK's verifyWebhook, weight the sender by event.auth, then reply through the same send API — or let a route with action:'agent' run the receive→reply loop for you.
Yes. For an autonomous, no-user agent you register an app in Entra ID, get admin consent for application mail permissions (Mail.Read, Mail.Send), then subscribe to change notifications and GET each message. It works well — especially when the agent lives inside a real M365 tenant. It's just more setup than pointing a domain at a webhook, which is the MailKite path.
Yes, and it's the part that bites. A messages subscription caps at 10,080 minutes (just under 7 days), or 1,440 minutes (under 1 day) if you include the body in the notification. You renew it with a PATCH before it lapses; if the cron dies, inbound quietly stops. MailKite's webhook is durable, so there's nothing to renew.
When the agent must operate as someone inside a real M365 tenant and touch the rest of it — calendar, contacts, Teams, OneDrive, SharePoint — under that org's identity, compliance, and retention. Graph is one API across all of M365. Pick MailKite when the agent just needs its own scoped inbox with no directory app, consent, or subscription lifecycle.
The Microsoft Graph (Outlook) alternative for AI agents →
The long-form take on our blog — receipts, runnable code, and where we won't overclaim.
Point a domain, drop in a webhook URL, receive your first email. Unlimited domains, no credit card.