Skip to main content
AgentMail
AgentMail gives an AI agent a real email account through one API — create an inbox, send and receive messages, group them into threads, and drive it all from webhooks or WebSockets. It does that job well, and the API is clean. But it is also a separate vendor account:
  • Inboxes live behind their own API key, dashboard, and inbox_id namespace.
  • 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.
Naive’s 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.
This guide maps AgentMail’s API to Naive’s, shows the smallest working swap, and is explicit about what does not map yet.
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 v0 path — 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 the inbox_id. Naive returns an inbox UUID but the SDK’s from_inbox accepts either the id or the address.
  • AgentMail sends a separate text and html body. Naive takes a single body and sends it as HTML if it contains tags, otherwise plain text.

Concept map

AgentMailNaiveNotes
new AgentMailClient({ apiKey })new Naive({ apiKey })Server-side key in both cases
Account / API key — scopes email onlytenant user via naive.forUser(id) — scopes every primitiveThe core consolidation win
client.inboxes.create({ username, domain })inbox.inboxIdclient.email.createInbox({ local_part })inbox.id / inbox.addressusernamelocal_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 }).messagesclient.email.inbox({ inboxId, limit }).emailsReceived mail for an inbox
client.inboxes.messages.get(inboxId, messageId)client.email.getEmail(emailId)Read one message
message.received webhookemail.received webhookHMAC-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 + ApprovalsMaps 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 gatingCoarse + 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, WebSocketsPartial — WebhooksAPI + 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.
import { AgentMailClient } from "agentmail";

const client = new AgentMailClient({ apiKey: process.env.AGENTMAIL_API_KEY! });

// 1. Create an inbox (clientId makes the create idempotent)
const inbox = await client.inboxes.create({
  username: "outreach",
  clientId: "acme-outreach-v1",
});
// → inbox.inboxId, e.g. "outreach@agentmail.to"

// 2. Send a message from it
const sent = await client.inboxes.messages.send(inbox.inboxId, {
  to: "prospect@company.com",
  subject: "Research findings",
  text: "Hi Sarah, here are the results...",
  html: "<p>Hi Sarah, here are the results...</p>",
});
// → sent.messageId, sent.threadId
import { Naive } from "@usenaive-sdk/server";

const naive = new Naive({ apiKey: process.env.NAIVE_API_KEY! });
const client = naive.forUser("acme"); // same id space as your own users

// 1. Create an inbox on your auto-provisioned (or custom) domain
const inbox = await client.email.createInbox({ local_part: "outreach" });
// → inbox.id (UUID) and inbox.address, e.g. "outreach@acme.usenaive.ai"

// 2. Send a message from it (one body — HTML auto-detected)
const sent = await client.email.send({
  from_inbox: inbox.id,
  to: "prospect@company.com",
  subject: "Research findings",
  body: "<p>Hi Sarah, here are the results...</p>",
});
// → sent.id, sent.status, sent.credits_used, sent.credits_remaining
The create-and-send shape lines up closely. The real differences to plan for:
  • One body, not two. AgentMail takes text and html. Naive takes a single body and 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 UUID id plus the address; from_inbox accepts 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 is message.received; Naive’s is email.received.
// Poll
const res = await client.inboxes.messages.list(inbox.inboxId, { limit: 20 });
for (const m of res.messages) {
  const body = m.extractedText ?? m.text; // quoted history stripped
}

// Or react to the webhook event: "message.received"
// Poll
const { emails } = await client.email.inbox({ inboxId: inbox.id, limit: 20 });
for (const m of emails) {
  console.log(m.from, m.subject, m.snippet);
  const full = await client.email.getEmail(m.id); // full body
}

// Or subscribe to the inbound event once, per company or per tenant:
const sub = await naive.webhooks.create(
  "https://app.example.com/api/webhooks/naive",
  ["email.received"],
);
// store sub.secret — verify with verifyWebhookSignature()
  • On Naive, an inbound delivery fires a signed, retried email.received webhook. When present, data.tenant_user_id identifies which tenant user the reply belongs to (company-wide subscriptions may omit it — see Webhooks).
  • Naive returns a snippet in the list and the full body via getEmail — 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.
1

Install the SDK and set your key

npm install @usenaive-sdk/server
Set NAIVE_API_KEY (a server-side key from the dashboard).
2

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.
3

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.
4

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.
5

Swap inbound

Replace messages.list(inboxId) / the message.received webhook with client.email.inbox({ inboxId }) / the email.received webhook.
6

Ship it

At this point you are off AgentMail for the core send/receive path. Everything below is upside, not a requirement.

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.
// One pod (or API key) per customer; isolates inboxes, and only those.
const inbox = await client.inboxes.create({ username: "acme-outreach" });
await client.inboxes.messages.send(inbox.inboxId, { to, subject, text });

// The card, the vault secrets, the connected apps, the KYC for this customer's
// agent live in entirely separate systems with their own tenancy.
// One tenant user per customer; isolates *every* primitive.
const acme = await naive.users.create({
  external_id: dbCustomer.id,
  email: dbCustomer.email,
});

const client = naive.forUser(acme.id);

const inbox = await client.email.createInbox({ local_part: "outreach" });
await client.email.send({ from_inbox: inbox.id, to, subject, body });

// The SAME client owns this customer's card, vault, connections, and KYC —
// one identity, isolated end to end.
await client.cards.create({ spending_limit_cents: 25_000 });
await client.vault.put("internal.api_key", "key_xyz");

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.
const starter = await naive.accountKits.create({
  name: "Starter",
  primitives_config: {
    email: { enabled: true, requiresApproval: true }, // every send freezes for approval
    cards: { enabled: false },                          // no card for this tier
  },
});
await naive.accountKits.assignUser(starter.id, acme.id);
  • 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: false makes the exact same line return forbidden, with no code change on your side.
    • requiresApproval: true freezes 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:
const { events } = await naive.forUser(acme.id).logs.query({ limit: 50 });
// "what did this agent do, for whom?" — email, cards, vault, connections, one timeline
  • 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 featureStatus on NaiveWorkaround
Threads as first-class objects (threads.*, thread-grouped messages)No thread objectList/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 onlyRe-send to the original sender; set reply_to for the header. Threading headers are not exposed
Drafts object + draft review/send (drafts.createdrafts.send)Maps differentlyUse Account Kit email.requiresApproval + Approvals for human-in-the-loop on the live send
Labels (state, read/unread, campaign tags)No email labelsTrack state in your own DB, or in Naive Memory / app-data
Lists (per-address/-domain allow + block)No per-address email listsAccount Kit gates the email primitive (on/off + approval), not individual recipients
Full-text search (messages.search, relevance + highlights)Not exposedemail.inbox filters by inbox + paginates; index bodies yourself if you need search
Attachments (send/reply files, download inbound)send has no attachment fieldInline content or link to Storage; no dedicated attachment helper
extracted_text / extracted_html (quoted-history stripping)Not providedgetEmail returns the full body; strip quotes yourself (e.g. Talon)
Agent self-sign-up (agent.signUp / agent.verify OTP)Different modelProvision tenant users from your server-side key
IMAP / SMTP access and WebSocketsPartial — Webhooks onlyUse the email.received webhook; no IMAP/SMTP or socket stream
If your agent depends on threads/replies as first-class objects, labels, per-recipient allow/block lists, full-text search, or attachments, those are the gaps most likely to matter. The create-inbox → send → receive loop with Account-Kit governance — the most common agent email pattern — maps cleanly today. Human-in-the-loop maps, but as an approval gate rather than a draft object.

Where to go next