A Gmail account plus the Gmail API is the go-to way to give an agent an inbox — free, familiar, and fine for an assistant a human supervises. Productionize an autonomous agent on it and you inherit OAuth restricted-scope CASA review, a Pub/Sub watch() that expires every 7 days, and base64url MIME you decode by hand. MailKite gives the agent its own scoped address and delivers inbound as parsed JSON with an auth verdict.
Gmail is the right tool when the agent works inside a specific human's mailbox — a co-pilot that triages their inbox and drafts replies they approve, with the user's consent and a person in the loop. Two decades of deliverability, full-text search, labels, threads, and Workspace admin controls come free, and none of the friction below applies to that case.
The agent gets agent@yourco.dev on a domain you control — an address it owns, not a person's Gmail account with a person's permissions bolted onto a bot. No consent screen, no user whose identity it operates under.
Reading mail via Gmail needs the restricted gmail.readonly/gmail.modify scopes, which in production past 100 users require an annual CASA security assessment. MailKite has no OAuth flow at all — you DNS-verify the domain and you're live.
Gmail push is a users.watch() subscription that expires in 7 days; miss the daily renewal cron and mail keeps arriving while your agent hears nothing, with no error. MailKite's webhook is registered once and stays put.
No Pub/Sub round-trip, no history.list → messages.get, no base64url MIME to walk. Every event arrives with decoded text/html, a resolved threadId, and an auth{spf,dkim,dmarc,spam} block — load-bearing for weighting trust on an injection surface.
| MailKite | Gmail API | |
|---|---|---|
| Agent's address | Scoped address on a domain you control | A Gmail/Workspace account (a human artifact) |
| Getting to production | DNS-verify (SPF+DKIM to send, MX to receive) | OAuth client + consent; restricted-scope CASA |
| Inbound delivery | One parsed JSON webhook | Pub/Sub push of a historyId → get → decode |
| Push longevity | Register the webhook once | watch() expires in 7 days; renew ~daily |
| Message body | Decoded text / html in the payload | base64url MIME you walk and decode |
| Auth verdict | auth block on every event | Parse SPF/DKIM/DMARC headers yourself |
| Reply / threading | mk.send({ inReplyTo }) resolves it | Build the RFC 2822 message + threading yourself |
| Automation posture | Built for programmatic, per-domain use | Account limits + ToS written for humans |
Competitor capabilities change — we re-audit these tables regularly. Spot something out of date? Tell us and we'll fix it.
Gmail hands you a historyId and leaves the lookup, MIME decode, and header auth to you; MailKite POSTs the decoded message with its auth verdict already computed.
POST /pubsub (Cloud Pub/Sub push, base64 envelope)
{ "message": { "data": "eyJlbWFpbEFkZHJlc3MiOiJhZ2VudEB5b3Vy…" } }
// data base64-decodes to → { emailAddress, historyId }
// NOT the message. You still have to:
// gmail.users.history.list({ startHistoryId }) → what changed
// gmail.users.messages.get({ id }) → the message
// walk payload.parts, pick text/plain,
// Buffer.from(part.body.data, "base64url") → the body
// parse SPF/DKIM/DMARC out of the headers yourself
// ( and users.watch() re-called before its 7-day expiry ) 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=…" }
]
} Gmail's dollar cost is zero, but the operational cost of running an autonomous agent on a human-account product — CASA review, watch renewals, per-user limits — is the real bill. MailKite prices on one combined volume, domains always free.
Add the MX records to receive and SPF+DKIM to send on a domain you control. No OAuth client, no consent screen, no CASA assessment — the agent gets its own scoped address like agent@yourco.dev.
Delete the watch() renewal cron, the history.list/messages.get lookup, and the base64url decode. Point one webhook URL at your handler and read event.text, event.from.address, and event.auth directly.
Check x-mailkite-signature with the SDK's verifyWebhook helper, then reply through the same send API with mk.send({ inReplyTo }) to thread it. Or set a route to action:'agent' and let MailKite run the receive→reply loop on a queue.
You can, and for a single agent that assists one real person on their own mailbox it's a good fit. For an autonomous agent with its own address, expect OAuth restricted-scope verification (CASA) for production, a Pub/Sub watch() subscription you renew before its 7-day expiry, and base64url MIME to decode. MailKite gives the agent a scoped address on your domain and delivers parsed JSON, with none of that setup.
Because users.watch() creates a subscription that expires after 7 days — it's not a register-once webhook, and Google recommends re-calling it roughly daily. If the renewal cron fails, new mail arrives but your service gets no notification and no error, so the agent goes silently deaf. MailKite's webhook is registered once and stays live.
No, and it shouldn't try to. If the job is triaging a specific human's existing inbox with their consent, use the Gmail API — that's what it's built for. MailKite is for the other case: an agent that needs its own address, receiving and replying on a domain you control.
The Gmail API 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.