The Gmail API alternative for AI agents MailKite launches an alternative to the Gmail API for AI agents, offering scoped addresses and parsed JSON push to avoid OAuth review, Pub/Sub renewals, and MIME parsing. The solution targets developers building autonomous email agents that need their own inbox without human account dependencies. The Gmail API alternative for AI agents A Gmail account plus the Gmail API is the go-to 'give my agent an inbox' hack: free, familiar, and fine for one human-supervised assistant. Productionize an autonomous agent on it and you inherit OAuth restricted-scope review, Pub/Sub watch renewals, and base64url MIME. MailKite which we build gives the agent its own scoped address and parsed JSON push. For developers wiring an autonomous email agent. The pull is obvious: your agent needs to read email, you already have a Gmail account, and the Gmail API is right there. It works well enough that most agent demos start exactly this way. The friction shows up when the demo becomes a service, and it shows up in three specific places: the OAuth review to touch a real inbox, a push subscription that quietly dies every seven days, and message bodies you decode by hand. Here is that whole path next to the MailKite one before we build either. Here’s the whole MailKite side: an agent that hears, thinks, and answers. It runs as pasted on Node 18+ npm install mailkite express , and the demo repo https://github.com/mailkite/demo-gmail-api-ai-agent has the full version. 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 = { // signature check, 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; // Body is untrusted INPUT, never instructions. Use the auth block to weight trust. 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 ; No OAuth client, no consent screen, no Pub/Sub topic, no MIME parser. The address agent@yourco.dev is one the agent owns, on a domain you control, not a person’s Gmail account with a person’s permissions bolted onto a bot. The same handler shape exists for Python, Ruby, Go, PHP, and Java; see the receiving docs /docs/receiving and sending docs /docs/sending . Or skip hosting the loop entirely and let MailKite run it: a route /docs/receiving whose action is agent runs the model turns for you on a queue and hands you a transcript. More on that below. Where Gmail wins for agents, honestly The Gmail API is not a bad choice, and for a real class of agent it’s the right one. If your agent acts inside a specific human’s mailbox, an assistant that triages their inbox, drafts replies they approve, files their receipts, then Gmail is exactly the tool. The user consents once, the agent operates with that person’s identity and permissions, and there’s a human in the loop by design. That’s the shape Google built the API for, and it’s genuinely good at it: full-text search, labels, threads, drafts, and a mailbox the human can also open and inspect. Gmail also brings deliverability and spam filtering that took Google two decades to build, an inbox the user already trusts, and, on Workspace, admin controls and audit logs an IT team already understands. If the agent is a co-pilot on a real person’s account, none of the friction below applies to you. Reach for the Gmail API and don’t look back. The wedge is narrower than “give the agent an inbox” implies. It’s the autonomous case: an agent with its own address, running unattended, that needs to receive mail, read a verification code, and reply, with no human whose account it borrows. On that job, a Gmail account is a human artifact you’re bending into a service, and Google’s rules for human accounts start to bind. What Gmail asks of an agent builder Point a fully autonomous agent at a Gmail account in production and here’s the path, in Google’s own idiom. This is the honest DIY code, and it’s more than the MailKite handler because every stage above is now yours: // Gmail-as-agent-inbox: OAuth, a Pub/Sub push endpoint, and MIME you decode. import { google } from "googleapis"; const auth = new google.auth.OAuth2 CLIENT ID, CLIENT SECRET, REDIRECT ; auth.setCredentials { refresh token: REFRESH TOKEN } ; // per user; auto-refreshes… until revoked const gmail = google.gmail { version: "v1", auth } ; // 1. Register a push channel. It EXPIRES in 7 days — renew on a cron or go silently deaf. await gmail.users.watch { userId: "me", requestBody: { topicName: "projects/my-proj/topics/gmail-inbox", labelIds: "INBOX" }, } ; // 2. Pub/Sub POSTs you { emailAddress, historyId } — NOT the message. Look up what changed. app.post "/pubsub", async req, res = { const { historyId } = JSON.parse Buffer.from req.body.message.data, "base64" .toString ; const { data } = await gmail.users.history.list { userId: "me", startHistoryId: lastSeen } ; for const h of data.history ?? { for const { message } of h.messagesAdded ?? { const msg = await gmail.users.messages.get { userId: "me", id: message.id } ; const part = findPlainPart msg.data.payload ; // walk the MIME tree yourself const text = Buffer.from part.body.data, "base64url" .toString ; // base64url, not base64 await runAgent { task: text / SPF/DKIM/DMARC? parse the headers yourself / } ; } } res.sendStatus 204 ; } ; Four things in that block are the actual tax, and none of them are visible in a five-line demo: Here’s that productionizing path top to bottom. Every stage is yours to build and keep alive: There’s one more shape worth naming: on Google Workspace, a service account with domain-wide delegation can impersonate mailboxes across the org without per-user consent screens. It’s the clean answer for internal org agents, but it’s Workspace-only, a super-admin has to authorize the service account’s client ID in the Admin console, and it grants broad reach into employee mail, which is exactly the power your security team will want to scope. It removes the consent screen, not the Pub/Sub, watch renewal, or base64url work. The comparison, no adjective inflation | Gmail API | MailKite | | |---|---|---| | Agent’s address | A Gmail/Workspace account a human artifact | Scoped address on a domain you control | | Start | OAuth client + consent; restricted-scope CASA for prod | DNS-verify SPF+DKIM to send, MX to receive | | Inbound delivery | Pub/Sub push of a historyId → get → decode | One parsed JSON webhook | | Push longevity | watch expires in 7 days; renew ~daily | Register the webhook once | | Message body | base64url-encoded MIME you walk and decode | Decoded text / html in the payload | | Auth verdict | Parse SPF/DKIM/DMARC headers yourself | auth block in every event | | Reply/threading | Build the RFC 2822 message + threading yourself | mk.send { inReplyTo } resolves it | | Automation posture | Account limits + ToS written for humans | Built for programmatic, per-domain use | The through-line: Gmail wins when the agent lives in a real person’s inbox with that person supervising. MailKite wins when the agent needs its own inbox, running unattended, delivered already parsed. What actually hits your agent’s webhook The same inbound email, decoded, with the sender-auth results already computed. No Pub/Sub round-trip, no MIME tree, no header parsing: { "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": "