DEVELOPER TIER
An API and MCP server for AI-answered email.
inxo is an email-response engine with two faces: a REST API and an MCP server over the same operations. Connect a Microsoft 365 shared mailbox, and the full loop — list threads, read drafts, regenerate, approve, send, read sources — is callable. The Console is a client of the same public API. There is no privileged path.
Capabilities
| Capability | REST | MCP | Description |
|---|---|---|---|
| connect a mailbox | POST /v1/mailboxes/connect | connect_mailbox | Begin a scoped grant to one shared mailbox; returns the admin-consent URL and grants nothing by itself. |
| verify the grant | POST /v1/mailboxes/{connection_id}/verify | verify_mailbox | The two-authority check against live tenant state; records the scoping evidence, not a pass/fail flag. |
| activate | POST /v1/mailboxes/{connection_id}/activate | activate_mailbox | Start processing; refuses until a processing instruction is on record for the mailbox’s controller. |
| list threads | GET /v1/threads | list_threads | The triage queue: ids, states, classification and subject — never message bodies. |
| read a thread | GET /v1/threads/{thread_id} | get_thread | The full message history; inbound content comes back as data and is flagged untrusted. |
| read a draft | GET /v1/drafts/{draft_id} | get_draft | Body, state, version, the grounding and sources it drew on, the safety verdict, and the audit record. |
| edit the body | PATCH /v1/drafts/{draft_id} | edit_draft | Replace the reply text under a version guard; there is no recipient field to change. |
| regenerate | POST /v1/drafts/{draft_id}/regenerate | regenerate_draft | A fresh model take through the same safety gates; same draft, bumped version. |
| approve & send | POST /v1/drafts/{draft_id}/approve | approve_and_send | Gated by the outbound policy; Idempotency-Key required; returns accepted, not delivered. |
| reject with reason | POST /v1/drafts/{draft_id}/reject | reject_draft | Terminal for the draft, with a reason from the closed taxonomy. |
| list knowledge documents | GET /v1/kb/documents | list_knowledge_documents | Metadata only; corpus writes are REST-only and refuse an agent actor by design. |
| subscribe a webhook | POST /v1/webhooks | create_webhook | Register a signed https endpoint for catalog events; the signing secret is shown once. |
Worked example
01
Connect, verify, activate
POST /v1/mailboxes/connect Authorization: Bearer ik_… { "provider": "m365", "mailbox_address": "support@brand-a.com", "scope_probe_known_other": "finance@brand-a.com" } # a real mailbox inxo must NOT be able to read 200 { "connection_id": "01a08679-bbc0-7440-878d-f922c47faa3e", "state": "pending", "consent_url": "https://login.microsoftonline.com/…" } # your Microsoft admin opens consent_url (pending → consented), then: POST /v1/mailboxes/01a08679-bbc0-7440-878d-f922c47faa3e/verify 200 { "state": "verified", "verify_result": { "exchange_assignments": [ { "role": "Application Mail.ReadWrite", "scope_type": "mailbox", "scope_ref": "…" } ], "exchange_unscoped_assignments": [], "entra_mail_app_roles": [], "target_in_scope": true, "out_of_scope_denied": true, "alias_sendable": true, "checked_at": "2026-09-09T14:01:52Z", … } } POST /v1/mailboxes/01a08679-bbc0-7440-878d-f922c47faa3e/activate 200 { "state": "activating", "subscription_expires_at": "2026-09-12T14:02:00Z" } # active once the initial backfill completes. Nothing is drafted before then.02
A thread arrives — and its draft with it
mcp poll_events { "cursor": "184467" } { "outcome": "ok", "data": [ { "event_id": "184468", "type": "thread.created", "account_id": "01a0867a-dcd0-7c82-8f69-7b6f99e89137", "workspace_id": "01a0867a-4c48-7dea-9e6a-164b91f848f2", "occurred_at": "2026-09-09T14:02:11Z", "erased": false, "data": { "thread_id": "01a0867b-6d58-793b-9d46-6729ef2a53ff", "mailbox_id": "01a08679-bbc0-7440-878d-f922c47faa3e", "sender": "dana@customer.example" } }, { "event_id": "184471", "type": "draft.created", "occurred_at": "2026-09-09T14:02:19Z", "erased": false, "data": { "thread_id": "01a0867b-6d58-793b-9d46-6729ef2a53ff", "draft_id": "01a0867c-8e68-718d-8045-320ae2141d63", "classification": "question" } } ], "next_cursor": "184471", "resync_required": false }Events carry ids, enums, timestamps and the sender — never a body. Drafting runs on eligible mail on its own; there is no "draft this thread" call to make.
03
Read the draft — body, grounding, and sources
GET /v1/drafts/01a0867c-8e68-718d-8045-320ae2141d63 200 { "id": "01a0867c-8e68-718d-8045-320ae2141d63", "thread_id": "01a0867b-6d58-793b-9d46-6729ef2a53ff", "status": "awaiting_approval", "version": 1, "from_address": "support@brand-a.com", # server-applied; a draft has no "to" field anywhere "body_plain": "Hi Dana — yes, annual billing is available on every plan. …", "grounding": "thread_and_knowledge_base", "sources": [ { "document_id": "01a0867e-4000-784a-b324-7f9c4b746901", "chunk_id": "01a0867e-d088-7580-944e-8aeb118e8073", "content_version": 3, "ordinal": 4, "rank": 0, "available": true, "text": "Annual plans are billed once per year at …" } ], "safety": { "scanned_draft_version": 1, "current": true, "findings": [] }, "created_at": "2026-09-09T14:02:19Z" }sources is a draft-level list of the knowledge chunks the model saw, by identity and content version — not a per-sentence citation, because a guessed citation is worse than none. A source that has since been reindexed comes back unavailable rather than stale.
04
An agent hits the approval gate
POST /v1/drafts/01a0867c-8e68-718d-8045-320ae2141d63/approve Authorization: Bearer ak_… # an agent principal Idempotency-Key: 01a08681-12a8-7c72-829a-9da901db4f83 { "version": 1 } 403 { "error": { "category": "policy", "code": "policy_requires_human", "message": "the workspace outbound policy requires a human approver", "request_id": "4bf92f3577b34da6a3ce929d0e0e4736", "retryable": false, "data": { "policy_mode": "human_approval_required", "gate_failed": [], "agent_remediable": false, "draft_state": "awaiting_approval" } } }The default policy holds. agent_remediable: false is the stop signal, and the draft stays awaiting_approval for a person. Your agent branches on the typed fields, never on the message.
05
A person approves — accepted now, reconciled later
POST /v1/drafts/01a0867c-8e68-718d-8045-320ae2141d63/approve Authorization: Bearer ik_… # a workspace key — a human actor Idempotency-Key: 01a08681-a330-768b-92c4-844519f13ffe # REQUIRED; bound to (draft, version) { "version": 1 } 200 { "draft": { "id": "01a0867c-8e68-718d-8045-320ae2141d63", "state": "approved", "version": 1 }, "send": { "state": "accepted" }, # queued — NOT delivered "audit": { "id": "01a0867d-1ef0-7d98-a873-db16db2ce49f", "action": "approved" } } # send.state advances on its own: accepted → pending → reconciled GET /v1/drafts/01a0867c-8e68-718d-8045-320ae2141d63 200 { "status": "sent", "version": 1, "send_state": "reconciled", "provider_message_id": "AAMkAD…", # resolved by the reconcile "audit": { "action": "approved", "actor_type": "human", "draft_version": 1, "ai_provenance": true, "review_kind": "human_approved_no_edit", "at": "2026-09-09T14:05:40Z" }, … }Send is asynchronous: approve returns accepted, which means queued. Replaying the same Idempotency-Key replays this result — it never sends twice.
Your agent is a first-class operator.
Every operation is an MCP tool with the same authorization and the same policy gate as the Console. Consequential tools state their irreversibility and their gate inline. A refusal is structured — a stable reason code and typed fields your agent can branch on, not an error string. Point your agent at the server and it can triage, regenerate, and edit today; approval stays with whoever your policy says it stays with.
The contract is versioned and additive.
Breaking changes are a new version with a sunset window, not a surprise. The Console consumes the same API you do — when we ship a feature, the API is how we shipped it.
Included in the Developer tier.
The REST API, the MCP server, outbound webhooks Coming, and higher rate limits Coming. Starter and Team include the Console; the Developer tier adds the surface underneath it. See pricing →
Copilot?
It's built around a person composing in Outlook. inxo is the API around the mailbox's response loop — triage, draft, approve, send, audit — with a policy your systems can query.
Build it on Graph directly?
You'd own consented scoped access and its verification, delta sync, threading, idempotent ingestion, send reconciliation, retrieval with provenance, drafting guardrails, and an approval model — before your product. That's the layer inxo is.
Mailbox access is scoped and verified; every send carries its approval record. → Security & trust