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

Every capability, as a REST endpoint and an MCP tool
CapabilityRESTMCPDescription
connect a mailboxPOST /v1/mailboxes/connectconnect_mailboxBegin a scoped grant to one shared mailbox; returns the admin-consent URL and grants nothing by itself.
verify the grantPOST /v1/mailboxes/{connection_id}/verifyverify_mailboxThe two-authority check against live tenant state; records the scoping evidence, not a pass/fail flag.
activatePOST /v1/mailboxes/{connection_id}/activateactivate_mailboxStart processing; refuses until a processing instruction is on record for the mailbox’s controller.
list threadsGET /v1/threadslist_threadsThe triage queue: ids, states, classification and subject — never message bodies.
read a threadGET /v1/threads/{thread_id}get_threadThe full message history; inbound content comes back as data and is flagged untrusted.
read a draftGET /v1/drafts/{draft_id}get_draftBody, state, version, the grounding and sources it drew on, the safety verdict, and the audit record.
edit the bodyPATCH /v1/drafts/{draft_id}edit_draftReplace the reply text under a version guard; there is no recipient field to change.
regeneratePOST /v1/drafts/{draft_id}/regenerateregenerate_draftA fresh model take through the same safety gates; same draft, bumped version.
approve & sendPOST /v1/drafts/{draft_id}/approveapprove_and_sendGated by the outbound policy; Idempotency-Key required; returns accepted, not delivered.
reject with reasonPOST /v1/drafts/{draft_id}/rejectreject_draftTerminal for the draft, with a reason from the closed taxonomy.
list knowledge documentsGET /v1/kb/documentslist_knowledge_documentsMetadata only; corpus writes are REST-only and refuse an agent actor by design.
subscribe a webhookPOST /v1/webhookscreate_webhookRegister a signed https endpoint for catalog events; the signing secret is shown once.

Worked example

  1. 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.
  2. 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.

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

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

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

Start free