Skip to content

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.

Agent MailExtrovertNotes
Client or API keyAgent API key (pk_agent_...)The key fixes the organization, project, and permission ceiling.
Create a clientRedeem an enrollment keyPOST /v1/enroll creates or reuses an agent by agent_handle and returns its API key.
Resource namespaceProjectAgents, inboxes, domains, and keys belong to a project.
InboxInboxA full email account for sending and receiving, with optional metadata.
ThreadThreadAddressed by an opaque, stable thread_id; clients never key conversations by subject.
MessageMessageSource text and html fields stay faithful to MIME. extracted_text and extracted_html are derived views.
LabelsFolders, read state, and searchA message has one folder and standard read or unread state.
WebhooksWebhooksSigned deliveries for inbound messages and other configured events.
Wait for messagewait_for_emailWaits server-side and can return a matching OTP or link.
Framework adaptersMCP server or TypeScript SDKUse hosted MCP from an agent host, or call the API through the SDK.
No equivalentReview LoopEvery agent-plane send creates a review record. The inbox policy decides whether it waits or is released.
TaskREST endpointTypeScript SDKMCP tool
Create or reuse an agentPOST /v1/enrollExtrovert.enrolledredeem_enrollment
Check identityGET /v1/auth/mewhoamiwhoami
Create an inboxPOST /v1/inboxesinboxes.createcreate_inbox
List or get inboxesGET /v1/inboxes, GET /v1/inboxes/{id}inboxes.list, inboxes.getlist_inboxes, get_inbox
SendPOST /v1/inboxes/{id}/sendinbox.sendsend_email
ReplyPOST /v1/inboxes/{id}/replyinbox.replyreply_email
ForwardPOST /v1/inboxes/{id}/messages/{message_id}/forwardinbox.forwardforward_email
List or get messagesGET /v1/inboxes/{id}/messages, GET /v1/messages/{message_id}inbox.messages, messages.getread_messages, get_message
Search messagesGET /v1/inboxes/{id}/messages/searchinbox.searchsearch
Update several messagesPATCH /v1/inboxes/{id}/messages/batchinbox.batchUpdateMessagesbatch_update_messages
List or get threadsGET /v1/inboxes/{id}/threads, GET /v1/inboxes/{id}/threads/{thread_id}inbox.threads, inbox.threadlist_threads, get_thread
List or download attachmentsGET .../attachments, GET .../attachments/{attachment_id}inbox.attachments, inbox.attachmentlist_attachments, get_attachment
Wait for a messagePOST /v1/inboxes/{id}/waitinbox.waitForEmailwait_for_email
Manage webhooks/v1/webhookswebhooks.*register_webhook, update_webhook, list_webhooks, get_webhook, delete_webhook
Manage contacts and suppression/v1/contacts, /v1/suppressionSee the SDK referenceadd_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.

migration.ts
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.

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.

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.

Agent Mail featureExtrovert status
Per-message overlapping labelsUse folders, read state, and search.
Draft lifecycle and scheduled sendNot available.
Semantic searchUse full-text message and thread search.