- Inboxes live behind their own API key, dashboard, and
inbox_idnamespace. - That account scopes email and nothing else — it knows the agent’s inboxes and threads, but not its virtual card, its vault secrets, its connected apps, or its KYC status.
- “Who let this agent email a customer, and what else can it touch?” is answered in AgentMail for mail, and somewhere else for everything else. There is no shared accountability.
email primitive gives the agent the same capability —
provision an inbox on your domain, send mail, read replies — but rooted in one identity:
- The tenant user that owns the inbox is the same user that owns its cards, its vault secrets, its connections, and its KYC.
- Whether the agent may send mail at all — and whether a send freezes for human approval — is decided by that user’s Account Kit at execution time.
- Every inbox, send, and inbound reply lands in the same per-user activity log as everything else the agent does.
AgentMail is a trademark of its owner, used here for identification only. No endorsement or affiliation is implied.
Tested against: AgentMail Node SDK
agentmail (AgentMailClient,
API base https://api.agentmail.to/v0, docs snapshot June 2026) and the Naive Node SDK
@usenaive-sdk/server against the Naive API (base https://api.usenaive.ai/v1,
docs snapshot June 2026).Version notes:- AgentMail’s REST API is still on the
v0path — treat method and field names as pre-1.0 and verify against your installed SDK version. - AgentMail keys an inbox by its full address (
agent@agentmail.to) and uses that as theinbox_id. Naive returns an inbox UUID but the SDK’sfrom_inboxaccepts either the id or the address. - AgentMail sends a separate
textandhtmlbody. Naive takes a singlebodyand sends it as HTML if it contains tags, otherwise plain text.
Concept map
| AgentMail | Naive | Notes |
|---|---|---|
new AgentMailClient({ apiKey }) | new Naive({ apiKey }) | Server-side key in both cases |
| Account / API key — scopes email only | tenant user via naive.forUser(id) — scopes every primitive | The core consolidation win |
client.inboxes.create({ username, domain }) → inbox.inboxId | client.email.createInbox({ local_part }) → inbox.id / inbox.address | username ↔ local_part; Naive defaults to your auto-provisioned domain |
client.inboxes.list() | client.email.listInboxes() | List inboxes for the identity |
client.inboxes.delete(inboxId) | client.email.deleteInbox(inboxId) | Deactivate an inbox |
client.inboxes.messages.send(inboxId, { to, subject, text, html }) | client.email.send({ from_inbox, to, subject, body }) | text+html collapse to one body (HTML auto-detected) |
client.inboxes.messages.list(inboxId, { limit }) → .messages | client.email.inbox({ inboxId, limit }) → .emails | Received mail for an inbox |
client.inboxes.messages.get(inboxId, messageId) | client.email.getEmail(emailId) | Read one message |
message.received webhook | email.received webhook | HMAC-signed, retried; the inbound trigger |
Custom domains (client.domains.*, verify) | Domains primitive (/v1/domains, POST /v1/domains/:id/verify) | Naive auto-provisions a system domain; BYOD is supported |
| Pods (per-tenant isolation of inboxes/domains/data) | Tenant users (naive.forUser(id)) | Naive’s unit of isolation spans every primitive, not just mail |
Drafts + human-in-the-loop (drafts.create → review → drafts.send) | Account Kit requiresApproval + Approvals | Maps differently — see gaps; approval is execution-time policy, not a draft object |
| Send/receive control (which addresses may send/receive) | Account Kit primitives_config.email — enable/disable + approval gating | Coarse + approval, not per-address lists — see gaps |
Threads (threads.*, messages.reply/replyAll/forward) | — | No first-class thread object or reply helper — see gaps |
| Labels (state, read/unread, campaigns) | — | No email labels — see gaps |
| Lists (allow/block sender + recipient addresses) | — | No per-address email allow/block — see gaps |
Full-text search (messages.search) | — | email.inbox filters by inbox + pagination only |
| Attachments (send/reply files, download inbound) | — | send carries no attachment field today |
Agent self-sign-up (agent.signUp / agent.verify OTP) | — | You provision tenant users from your server key |
| IMAP / SMTP access, WebSockets | Partial — Webhooks | API + email.received webhook; no IMAP/SMTP or socket stream |
Before / after: the core path
The path that matters for almost every agent is create an inbox, then send a message from it. Here it is on both platforms.- One body, not two. AgentMail takes
textandhtml. Naive takes a singlebodyand sends it as HTML when it contains tags, otherwise plain text. - Inbox id is a UUID (or the address). AgentMail uses the address itself as
inbox_id. Naive returns a UUIDidplus theaddress;from_inboxaccepts either. - The id is your identity, not a separate account. In AgentMail the API key scopes email. In
Naive the same
forUser(id)handle also owns the agent’s cards, vault, connections, and KYC.
Receiving replies
Both platforms expose a list-the-inbox call and an inbound webhook. AgentMail’s event ismessage.received; Naive’s is email.received.
- On Naive, an inbound delivery fires a signed, retried
email.receivedwebhook. When present,data.tenant_user_ididentifies which tenant user the reply belongs to (company-wide subscriptions may omit it — see Webhooks). - Naive returns a
snippetin the list and the full body viagetEmail— there is no separate quoted-history-stripped field (see gaps).
Minimal viable migration
The smallest swap that keeps a working agent running is just create inbox + send + read inbound. You do not need tenant-user fan-out, Account Kits, or webhooks to make your first call.Install the SDK and set your key
NAIVE_API_KEY (a server-side key from the dashboard).Confirm you have an active domain
Naive auto-provisions a system domain on registration. Check
GET /v1/domains; if it shows
pending_dns, run POST /v1/domains/:id/verify. Bringing your own domain is the
Domains flow.Swap inbox creation
Replace
client.inboxes.create({ username }) with
client.email.createInbox({ local_part }). Reuse your AgentMail username as the
local_part. Keep the returned inbox.id.Swap send
Replace
client.inboxes.messages.send(inboxId, { to, subject, text, html }) with
client.email.send({ from_inbox, to, subject, body }). Collapse text/html into one
body.Swap inbound
Replace
messages.list(inboxId) / the message.received webhook with
client.email.inbox({ inboxId }) / the email.received webhook.Consolidate further once you’re on Naive
This is where the migration pays for itself. In AgentMail, the API key isolates email and nothing else. On Naive, the unit of isolation is a tenant user, and it isolates the agent’s entire footprint.Gain #1 — one identity across primitives
- With AgentMail, the agent’s inboxes are an island. With Naive,
naive.forUser(acme.id)is a single handle to email and cards and vault and connections and KYC. - You provision a customer’s whole agent footprint from one identity, and tear it down from one place. Sibling tenants can never read each other’s inboxes, cards, or secrets.
Gain #2 — execution-time permission enforcement
- Whether an agent may send mail — and whether a send freezes for human review — is policy on the Account Kit, not a check you hand-write and not a separate draft-review service.
- The agent’s code is identical for every tier —
client.email.send({ ... }). Whether the call runs is decided by the caller’s kit at execution time:primitives_config.email.enabled: falsemakes the exact same line returnforbidden, with no code change on your side.requiresApproval: truefreezes the send until a human approves — the API replays it only after approval. This is Naive’s answer to AgentMail’s human-in-the-loop drafts: the review gate is policy, applied to the live call, not a separate object you build a UI around.
Gain #3 — unified accountability
- Every inbox, send, and inbound reply for a customer lands in one per-user activity log — alongside their card, vault, and connection events, not in a separate mail dashboard:
- That is the question that is hard to answer when mail lives in AgentMail, cards live in Stripe, and secrets live somewhere else. Under Naive it is a single query.
What does not map yet
A migration guide that hides gaps is worse than none. The core path (create inbox → send → read inbound → webhook) maps cleanly, but the following AgentMail features have no direct equivalent on Naive’s email primitive today. Check this list against your app before you commit.| AgentMail feature | Status on Naive | Workaround |
|---|---|---|
Threads as first-class objects (threads.*, thread-grouped messages) | No thread object | List/read messages per inbox; group by subject/In-Reply-To in your own store |
Reply / reply-all / forward helpers (messages.reply, replyAll, forward) | Not provided — send only | Re-send to the original sender; set reply_to for the header. Threading headers are not exposed |
Drafts object + draft review/send (drafts.create → drafts.send) | Maps differently | Use Account Kit email.requiresApproval + Approvals for human-in-the-loop on the live send |
| Labels (state, read/unread, campaign tags) | No email labels | Track state in your own DB, or in Naive Memory / app-data |
| Lists (per-address/-domain allow + block) | No per-address email lists | Account Kit gates the email primitive (on/off + approval), not individual recipients |
Full-text search (messages.search, relevance + highlights) | Not exposed | email.inbox filters by inbox + paginates; index bodies yourself if you need search |
| Attachments (send/reply files, download inbound) | send has no attachment field | Inline content or link to Storage; no dedicated attachment helper |
extracted_text / extracted_html (quoted-history stripping) | Not provided | getEmail returns the full body; strip quotes yourself (e.g. Talon) |
Agent self-sign-up (agent.signUp / agent.verify OTP) | Different model | Provision tenant users from your server-side key |
| IMAP / SMTP access and WebSockets | Partial — Webhooks only | Use the email.received webhook; no IMAP/SMTP or socket stream |
Where to go next
emailprimitive — full inbox/send/receive lifecycle- Email API reference — exact request/response shapes
emailSDK sub-client — typed method signatures- Webhooks — the
email.receivedevent that makes inbound autonomous - Account Kits and Approvals — the policy model that makes execution-time send governance and human-in-the-loop real
- Tenant users — the identity that the consolidation hangs off