Receiving
Two ways mail reaches your code — an MX record on a domain, or a connected Gmail or Outlook mailbox — and what arrives when it does.
There are two ways inbound mail reaches your application. Both work equally well for a domain or account you run and for one belonging to a customer who connected it through your app — what differs is what the setup needs and how much it covers.
A domain
Point an MX record at AI Inbx and every address on the domain becomes an inbox your code can read. Needs DNS access, and takes over the domain’s mail.
A connected mailbox
An existing Gmail or Outlook account, authorized over OAuth and synced in. Needs no DNS, changes nothing about the account, and covers one address.
Both land in the same place: a thread, and an email.received webhook.
Receiving on a domain
Add the INBOUND MX record from the domain’s records array and verify the domain — that’s the whole setup. Whose domain it is doesn’t matter to the API; if it’s a customer’s, you show them the records and they publish them. See Domains for the record set and how verification works.
Once the MX record resolves, mail to any address on the domain is accepted, parsed, threaded, and delivered to your webhook endpoints.
Receiving through a connected mailbox
When the address already has an inbox at Google or Microsoft — so pointing an MX record at AI Inbx would take mail away from it — connect the mailbox instead. AI Inbx creates a one-time authorization URL, whoever holds the account approves it, and their mail syncs in from then on:
const { url } = await aiinbx.mailboxes.connect({
provider: "google",
return_to: "https://app.example.com/settings/mailboxes",
ref: "user_8812",
})
// Send the customer to `url`.
ref is your own identifier, echoed back on the mailbox.connected event so you know which of your users just finished. Mailboxes covers the full flow — including a hosted page and a plain connect link that need no server call at all — and bringing your own OAuth app so the consent screen carries your brand.
What arrives
A received message is delivered as an email.received event whose data carries the routing you need without a follow-up fetch:
{
"id": "evt_...",
"type": "email.received",
"created_at": "2026-09-01T10:31:04Z",
"space_id": null,
"data": {
"email_id": "eml_...",
"thread_id": "thr_...",
"domain_id": "dom_...",
"mailbox_id": null,
"from": "grace@example.com",
"to": ["support@yourapp.com"],
"subject": "Re: Quick question",
"snippet": "Thursday at 10 works for me…",
"category": "human",
"verdicts": { "spam": "PASS", "spf": "PASS", "dkim": "PASS", "dmarc": "PASS" },
"attachments": []
}
}
Retrieve the full message — bodies, headers, attachments, delivery events — with emails.retrieve(data.email_id), or fetch the whole conversation with threads.retrieve(data.thread_id).
What was written
A reply carries the whole conversation underneath it, and people often type their answers inside that quote. stripped_text is the part the sender actually wrote: the quoted conversation and the signature removed, and an answer typed into the quote kept under the line it answers.
{
"text": "Hi Lena, answers inline.\n\nOn Mon, 1 Sep 2026 at 10:44, Lena Bergmann <lena@bergmann-metall.de> wrote:\n> 1. How many units per month?\n\n500, ramping to 800 in Q4.\n\n> 2. Which finish?\n\nAnodised black.\n\nBest\nTom",
"stripped_text": "Hi Lena, answers inline.\n\n> 1. How many units per month?\n500, ramping to 800 in Q4.\n\n> 2. Which finish?\nAnodised black.",
"segments": [
{ "kind": "written", "text": "Hi Lena, answers inline." },
{ "kind": "quoted", "text": "On Mon, 1 Sep 2026 at 10:44, Lena Bergmann <lena@bergmann-metall.de> wrote:\n1. How many units per month?" },
{ "kind": "written", "text": "500, ramping to 800 in Q4." },
{ "kind": "quoted", "text": "2. Which finish?" },
{ "kind": "written", "text": "Anodised black." },
{ "kind": "signature", "text": "Best\nTom" }
]
}
The cut reads the client’s own markup first, then the markers around the quote, and finally the message being answered: when that message is on the thread, every line that reappears from it is quote, whatever the client marked. That last reading is how answers typed into an Outlook quote — which carries no markers at all — are found. segments is the whole cut in order, for a view that folds the quote or a model that wants the context too.
A message that quotes something never on file — a forwarded conversation, a reply to a mail sent before the mailbox was connected — keeps its quote in stripped_text: nothing underneath has been seen, so it is the content. segments still marks it as quoted.
Give a model stripped_text for what was said and the thread for what was said before it.
Categories
Every inbound message is classified, so an autoresponder doesn’t get treated as a human answer. category is one of:
| Category | Meaning |
|---|---|
human |
Classified as a message written by a person. |
out_of_office |
An away or vacation autoreply. |
auto_reply |
Another kind of automatic response — ticket acknowledgements, “we got your message”. |
bounce |
A delivery failure notification that arrived as mail. |
verification |
A confirmation or code email. |
transactional |
Receipts, invoices, account notices. |
notification |
Alerts and system mail. |
marketing |
Bulk or promotional mail. |
spam |
Judged unsolicited. |
Gate your agent on it:
if (event.type === "email.received" && event.data.category === "human") {
await handleReply(event.data.thread_id)
}
This filter reduces automatic reply loops, but classification can be wrong. Also apply sender checks, conversation reply limits, and human handoff rules. See Email agents.
Authentication verdicts
verdicts reports what SPF, DKIM, DMARC, and spam scanning concluded about the sender. Treat a FAIL as reason not to act on the content, particularly for anything that changes state.
Threading
Replies are matched onto existing conversations from In-Reply-To and References first, then — when a client mangles or drops those headers — by comparing the quoted content against messages already on the thread. Subject alone is never enough to merge two conversations, so two unrelated messages that happen to share “Re: Invoice” stay apart.
The upshot for your code: trust thread_id. Threads covers reading and replying.
Routing which events go where
An endpoint subscribes to the event types it wants, and can further narrow by sender or recipient with routing rules — useful when one workspace serves several products:
await aiinbx.webhookEndpoints.create({
url: "https://app.example.com/webhooks/support",
subscriptions: ["email.received"],
routing: [{ effect: "allow", field: "to", pattern: "support@yourapp.com" }],
})
See Webhooks for how allow and block rules combine.
Attachments on inbound mail
Inbound attachments are stored and exposed as metadata plus a short-lived download URL. PDFs, documents, and spreadsheets are additionally prepared into Markdown or text so a model can read them without you running a parser — see Attachments.