Zero to first email
This guide starts with an enrollment key because it is the normal bootstrap path while free self-signup is paused. You will redeem the key, create an inbox, submit a message for review, wait for a reply, and answer in the same thread.
1. Redeem an enrollment key
Section titled “1. Redeem an enrollment key”In the console, open Credentials > Enrollment keys and issue a key when setup needs to create the
agent. Select only the permissions the agent needs. For this guide, include mailbox:create,
mailbox:read, and mailbox:send.
export EXTROVERT_API_BASE_URL="https://api.extrovert.dev"export EXTROVERT_ENROLLMENT_KEY="pk_enroll_…"
curl -sS -X POST "$EXTROVERT_API_BASE_URL/v1/enroll" \ -H "Content-Type: application/json" \ -d "{\"token\":\"$EXTROVERT_ENROLLMENT_KEY\",\"agent_handle\":\"support-agent\"}"Store the returned agent_key; it is not shown again.
export EXTROVERT_API_KEY="pk_agent_…"npm install @extrovert.dev/sdk@nextimport { 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-agent",});
console.log(enrollment.agent_key); // shown once{ "name": "redeem_enrollment", "arguments": { "enrollment_token": "pk_enroll_…", "agent_handle": "support-agent" }}The packaged local MCP server stores the returned agent key in its permission-restricted credential file.
2. Create an inbox
Section titled “2. Create an inbox”Omit username and domain to create an @extrovertmail.com address on a paid account. Use a
stable client_id or Idempotency-Key when retrying after a transport timeout.
curl -sS -X POST "$EXTROVERT_API_BASE_URL/v1/inboxes" \ -H "Authorization: Bearer $EXTROVERT_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: support-agent-primary" \ -d '{ "display_name": "Support agent" }'const inbox = await extrovert.inboxes.create({ display_name: "Support agent", client_id: "support-agent-primary",});console.log(inbox.id, inbox.address);{ "name": "create_inbox", "arguments": { "display_name": "Support agent", "client_id": "support-agent-primary" }}3. Read the review policy
Section titled “3. Read the review policy”Call GET /v1/inboxes/{inbox_id}, extrovert.inboxes.get(id), or get_inbox before the first send.
The single-inbox response includes effective_review_policy.
New accounts use require_review. Under that policy, every send, reply, and forward needs an
intent.summary. Without it, the API returns 422 intent_required; nothing is sent or queued.
4. Submit a message
Section titled “4. Submit a message”curl -sS -X POST \ "$EXTROVERT_API_BASE_URL/v1/inboxes/$INBOX_ID/send" \ -H "Authorization: Bearer $EXTROVERT_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: notify-ops-001" \ -d '{ "to": ["ops@acme.test"], "subject": "Support inbox ready", "text": "The support inbox is ready.", "intent": { "summary": "Tell operations that the support inbox is ready" } }'const outcome = await inbox.send({ to: "ops@acme.test", subject: "Support inbox ready", text: "The support inbox is ready.", intent: { summary: "Tell operations that the support inbox is ready" }, idempotency_key: "notify-ops-001",});{ "name": "send_email", "arguments": { "inbox": "pmbx_…", "to": ["ops@acme.test"], "subject": "Support inbox ready", "text": "The support inbox is ready.", "intent": { "summary": "Tell operations that the support inbox is ready" }, "client_id": "notify-ops-001" }}The normal response under require_review is:
{ "kind": "queued_for_review", "review": { "id": "rr_8Tz4kP", "state": "needs_review", "effective_mode": "review" }}The message has not been delivered. Wait for a terminal review event or poll
GET /v1/reviews/rr_8Tz4kP until closed is true.
5. Wait for a reply
Section titled “5. Wait for a reply”After the review reaches sent, wait for new mail:
const result = await inbox.waitForEmail({ from: "ops@acme.test", timeout_seconds: 120,});
if (result.timed_out) throw new Error("No reply arrived in time");{ "name": "wait_for_email", "arguments": { "inbox": "pmbx_…", "from": "ops@acme.test", "timeout_ms": 120000 }}curl -sS -X POST \ "$EXTROVERT_API_BASE_URL/v1/inboxes/$INBOX_ID/wait" \ -H "Authorization: Bearer $EXTROVERT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "ops@acme.test", "timeout_seconds": 120 }'wait_for_email returns the canonical message fields. It also returns an extracted OTP code and
verification link when present. See Messages, threads, and search
for the difference between source text and html fields and their best-effort extracted variants.
6. Reply in the same thread
Section titled “6. Reply in the same thread”Reference the returned message_id. The server derives recipients and threading headers. Reply follows
the same review policy as send.
await inbox.reply({ message_id: result.message!.id, text: "Thanks. I will keep this address monitored.", intent: { summary: "Acknowledge the reply and confirm the inbox remains monitored" }, idempotency_key: "reply-ops-001",});Optional self-signup path
Section titled “Optional self-signup path”GET /v1/signup-status currently reports that free self-signup is paused. When enabled,
POST /v1/agent/sign-up creates the agent and an @free.extrovertmail.com inbox, then returns a
temporary key with only signup:verify. That key can only verify the emailed OTP. It cannot create,
read, send, or export credentials. POST /v1/agent/verify revokes it and returns the durable key, the
activated inbox address, and copy-ready MCP calls for reading and waiting.
The verified human can then sign in to the console with the same verified email address. Extrovert shows the matching agent-created account and asks the human to claim it. Claiming makes that person the owner without exposing a claim token in the browser flow.