Coming from Agent Mail
If you have built on Agent Mail, the core loop is familiar: create an inbox, send and receive email, work with threads, wait for verification messages, and react to webhooks. Extrovert adds a review step to outbound mail and uses folders, read state, and full-text search instead of labels.
Concept mapping
Section titled “Concept mapping”| Agent Mail | Extrovert | Notes |
|---|---|---|
| Client or API key | Agent API key (pk_agent_...) | The key fixes the organization, project, and permission ceiling. |
| Create a client | Redeem an enrollment key | POST /v1/enroll creates or reuses an agent by agent_handle and returns its API key. |
| Resource namespace | Project | Agents, inboxes, domains, and keys belong to a project. |
| Inbox | Inbox | A full email account for sending and receiving, with optional metadata. |
| Thread | Thread | Addressed by an opaque, stable thread_id; clients never key conversations by subject. |
| Message | Message | Source text and html fields stay faithful to MIME. extracted_text and extracted_html are derived views. |
| Labels | Folders, read state, and search | A message has one folder and standard read or unread state. |
| Webhooks | Webhooks | Signed deliveries for inbound messages and other configured events. |
| Wait for message | wait_for_email | Waits server-side and can return a matching OTP or link. |
| Framework adapters | MCP server or TypeScript SDK | Use hosted MCP from an agent host, or call the API through the SDK. |
| No equivalent | Review Loop | Every agent-plane send creates a review record. The inbox policy decides whether it waits or is released. |
Endpoint mapping
Section titled “Endpoint mapping”| Task | REST endpoint | TypeScript SDK | MCP tool |
|---|---|---|---|
| Create or reuse an agent | POST /v1/enroll | Extrovert.enrolled | redeem_enrollment |
| Check identity | GET /v1/auth/me | whoami | whoami |
| Create an inbox | POST /v1/inboxes | inboxes.create | create_inbox |
| List or get inboxes | GET /v1/inboxes, GET /v1/inboxes/{id} | inboxes.list, inboxes.get | list_inboxes, get_inbox |
| Send | POST /v1/inboxes/{id}/send | inbox.send | send_email |
| Reply | POST /v1/inboxes/{id}/reply | inbox.reply | reply_email |
| Forward | POST /v1/inboxes/{id}/messages/{message_id}/forward | inbox.forward | forward_email |
| List or get messages | GET /v1/inboxes/{id}/messages, GET /v1/messages/{message_id} | inbox.messages, messages.get | read_messages, get_message |
| Search messages | GET /v1/inboxes/{id}/messages/search | inbox.search | search |
| Update several messages | PATCH /v1/inboxes/{id}/messages/batch | inbox.batchUpdateMessages | batch_update_messages |
| List or get threads | GET /v1/inboxes/{id}/threads, GET /v1/inboxes/{id}/threads/{thread_id} | inbox.threads, inbox.thread | list_threads, get_thread |
| List or download attachments | GET .../attachments, GET .../attachments/{attachment_id} | inbox.attachments, inbox.attachment | list_attachments, get_attachment |
| Wait for a message | POST /v1/inboxes/{id}/wait | inbox.waitForEmail | wait_for_email |
| Manage webhooks | /v1/webhooks | webhooks.* | register_webhook, update_webhook, list_webhooks, get_webhook, delete_webhook |
| Manage contacts and suppression | /v1/contacts, /v1/suppression | See the SDK reference | add_contact, list_contacts, remove_contact, check_suppression, list_suppressions, suppress_email, unsuppress_email |
See the API overview for request and response details. The current OpenAPI document
is available at /openapi.yaml.
TypeScript flow
Section titled “TypeScript flow”import { Extrovert } from "@extrovert.dev/sdk";
const bootstrap = new Extrovert({ apiKey: process.env.EXTROVERT_ENROLLMENT_KEY!,});const { client: extrovert, enrollment } = await bootstrap.enrolled({ token: process.env.EXTROVERT_ENROLLMENT_KEY!, agent_handle: "support-bot",});
const inbox = await extrovert.inboxes.create({ display_name: "Support Bot", client_id: "support-bot-primary-inbox",});
await inbox.send({ to: "ops@acme.test", subject: "Status update", text: "The import is complete.", intent: { summary: "Tell operations that the requested import finished" }, idempotency_key: "import-complete-2026-09-01",});
const incoming = await inbox.waitForEmail({ from: "ops@acme.test", timeout_seconds: 120,});
const thread = await inbox.thread(incoming.message!.thread_id);const latest = thread.messages.at(-1);if (!latest) throw new Error("Thread has no messages");
await inbox.reply({ thread_id: thread.id, expected_last_message_id: thread.last_message_id, text: "I will check it now.", intent: { summary: "Acknowledge the follow-up request" }, idempotency_key: `reply-${latest.id}`,});This reads the oldest-first conversation before composing, then lets the server resolve the newest
reply parent from thread_id at submission. The optional expected-last-message guard returns 409 if
the conversation advanced between read and submit; it is not a delivery lock. Prefer each message’s
extracted_text for concise context, but fall back to source text when extraction is null or exact
wording matters.
Store enrollment.agent_key after the first redemption. Enrollment keys create agents. Agent API
keys authenticate later calls.
Differences to account for
Section titled “Differences to account for”Outbound mail always has a review record
Section titled “Outbound mail always has a review record”Every agent-plane send, reply, and forward creates a review record. New inboxes start with
require_review, so include an intent with every outbound request and expect
202 queued_for_review. Follow the returned review id until a terminal sent, send_failed, or
cancelled event arrives.
A human can change the inbox policy to allow_direct. Even then, Extrovert records the review and
returns its id. Read effective_review_policy before composing if your workflow needs to explain what
will happen.
See the Review Loop contract.
Source and extracted message bodies are separate
Section titled “Source and extracted message bodies are separate”text and html reflect the source MIME parts. Either may be absent. extracted_text and
extracted_html are derived views intended for reply cleanup and presentation. Extrovert does not
invent a text or HTML alternative that the message did not contain.
Extraction is best-effort and may omit signatures or quoted history. Keep the source fields when the agent needs evidence, precise quoting, or a fallback for a null extracted alternative.
The MCP get_message tool can select format: "auto" | "text" | "html" | "both" and
variant: "source" | "extracted". The structured message still keeps the four fields separate.
Folders replace overlapping labels
Section titled “Folders replace overlapping labels”A message has one folder and standard read state. Use folder filters, mark_read, full-text search,
contacts, and the suppression list for common triage and safety workflows. Extrovert does not expose
an overlapping per-message label graph.
Hosted MCP replaces framework-specific adapters
Section titled “Hosted MCP replaces framework-specific adapters”Connect an MCP-capable host to https://mcp.extrovert.dev/mcp and sign in through OAuth. Hosted MCP
permissions follow the signed-in organization role. It also accepts an agent API key as a bearer
credential. Enrollment keys are not accepted by hosted MCP.
Use the MCP setup guide for configuration details.
Features that do not map directly
Section titled “Features that do not map directly”| Agent Mail feature | Extrovert status |
|---|---|
| Per-message overlapping labels | Use folders, read state, and search. |
| Draft lifecycle and scheduled send | Not available. |
| Semantic search | Use full-text message and thread search. |