Skip to content
AI Inbx
Esc
navigateopen⌘Jpreview
On this page

Threads

Read a conversation, page through a long one, and reply without reconstructing a single header.

A thread is the conversation, not a folder you put messages in. Every send opens one or joins one, every inbound reply is matched onto one, and thread_id is the handle your code should hold onto.

Listing threads

const page = await aiinbx.threads.list({ limit: 50, query: "invoice" })

for (const thread of page.data) {
  console.log(thread.subject, thread.message_count, thread.last_message_at)
}
page = client.threads.list(limit=50, query="invoice")

for thread in page["data"]:
    print(thread["subject"], thread["message_count"], thread["last_message_at"])
curl "https://api.aiinbx.com/api/v2/threads?limit=50&query=invoice" \
  -H "Authorization: Bearer $AI_INBX_API_KEY"

query matches subjects, mailboxes, and participants; mailbox narrows to one address. Both SDKs also expose the list as an iterator that keeps your filters across every page:

for await (const thread of aiinbx.threads.list({
  mailbox: "team@example.com",
})) {
  console.log(thread.subject)
}
for thread in client.threads.iter(mailbox="team@example.com"):
    print(thread["subject"])

query is a literal subject/address filter for finding an operational thread, not a knowledge-base search. Persist any email data your product needs long-term in your own database.

Retrieving one

threads.retrieve returns the conversation with its messages inline, each a full email: bodies, headers, attachments, delivery events, and engagements.

const thread = await aiinbx.threads.retrieve("thr_...")

console.log(thread.participants, thread.message_count)

for (const message of thread.messages) {
  console.log(message.direction, message.from.address, message.text)
}
thread = client.threads.retrieve("thr_...")

for message in thread["messages"]:
    print(message["direction"], message["from"]["address"], message["text"])
PropType
idstring

thr_…

Typestring
subjectstring

The conversation's subject.

Typestring
mailboxstring

The address on your side of the conversation.

Typestring
participantsstring[]

Every address that has appeared on the thread.

Typestring[]
message_countnumber

Total messages, not just the page.

Typenumber
last_message_atstring

Timestamp of the newest message.

Typestring
messagesFullEmail[]

Messages, oldest first.

TypeFullEmail[]
messages_next_cursorstring | null

Pass as message_cursor to fetch the next page of messages.

Typestring | null

Long threads

Messages paginate independently of the thread list — message_limit (default 40, max 100) and message_cursor. Both SDKs wrap that in an iterator:

for await (const message of aiinbx.threads.iterateMessages("thr_...", {
  message_limit: 100,
})) {
  console.log(message.subject)
}
for message in client.threads.iter_messages("thr_..."):
    print(message["snippet"])

Replying

This is the call the API exists for. Give it the thread and the content; the sender, recipients, subject, and the In-Reply-To and References headers are derived from the conversation:

await aiinbx.threads.reply(
  "thr_...",
  { text: "Sounds good — see you Thursday." },
  { idempotencyKey: "reply-thr-123-v1" }
)
client.threads.reply(
    "thr_...",
    text="Sounds good — see you Thursday.",
    idempotency_key="reply-thr-123-v1",
)
curl https://api.aiinbx.com/api/v2/threads/thr_.../reply \
  -H "Authorization: Bearer $AI_INBX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: reply-thr-123-v1" \
  -d '{ "text": "Sounds good — see you Thursday." }'

As with a send, at least one of html or text is required, and everything else is optional.

Overriding what’s inferred

Each inferred field can be set explicitly when the default isn’t what you want:

PropType
reply_all?boolean

Reply to every participant rather than just the last sender. The simplest way to widen a reply.

Typeboolean
Defaultfalse
to?string | string[]

Replace the inferred recipients.

Typestring | string[]
cc?string | string[]

Carbon copies for this reply.

Typestring | string[]
bcc?string | string[]

Blind copies for this reply.

Typestring | string[]
from?string | object

Send as a different address on the thread — must still be a verified domain or connected mailbox.

Typestring | object
reply_to?string | string[]

Where answers to this reply should go.

Typestring | string[]
subject?string

Override the inherited subject.

Typestring
attachments?object[]

Same shape as on a send — up to 20.

Typeobject[]
headers?object

Additional RFC 5322 headers.

Typeobject
scheduled_at?string

Defer the reply; up to 30 days out.

Typestring
suppression_key?string

List this reply is checked against.

Typestring
unsubscribe?boolean

Mark the reply as optional mail.

Typeboolean
pacing?object

{ skip, count } — bypass or discount pacing rules.

Typeobject
await aiinbx.threads.reply("thr_...", {
  reply_all: true,
  cc: "manager@example.com",
  text: "Looping in Dana.",
})

The reply returns the same shape a send does — including suppressed and pacing — so the checks from Sending apply here too.

Forwarding

threads.forward sends a whole thread to addresses that were not on it. Pass the thread and a recipient; the API renders the conversation into one email and sends it:

await aiinbx.threads.forward(
  "thr_...",
  { to: "dana@example.com", note: "See the thread below." },
  { idempotencyKey: "forward-thr-123-v1" }
)
client.threads.forward(
    "thr_...",
    to="dana@example.com",
    note="See the thread below.",
    idempotency_key="forward-thr-123-v1",
)
curl https://api.aiinbx.com/api/v2/threads/thr_.../forward \
  -H "Authorization: Bearer $AI_INBX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: forward-thr-123-v1" \
  -d '{ "to": "dana@example.com", "note": "See the thread below." }'

Only to is required. The recipient gets one email: the note, if any, then every message in the thread, oldest first — who wrote it, when, to whom, and what they wrote. Each turn is the author’s own text; quoted tails and signatures are removed, so a ten-message thread reads as ten messages rather than ten copies of the chain. The thread’s attachments ride along in order, up to about 15 MB in total; anything past that is named in the transcript and stays on the thread, where the API serves it.

PropType
tostring | string[]

Recipients of the forward.

Typestring | string[]
note?string

Text placed above the transcript. Up to 10,000 characters.

Typestring
from?string | object

The thread's own mailbox unless set; must be a verified domain or connected mailbox.

Typestring | object
subject?string

`Fwd:` and the thread's subject unless set.

Typestring
include_attachments?boolean

Whether the thread's files ride along. They are listed in the transcript either way.

Typeboolean
Defaulttrue
cc?string | string[]

Carbon copies.

Typestring | string[]
bcc?string | string[]

Blind copies.

Typestring | string[]
reply_to?string | string[]

Where replies to the forward should go. Defaults to `from`.

Typestring | string[]
attachments?object[]

Files of your own, on top of the thread's — same shape as on a send.

Typeobject[]
headers?object

Additional RFC 5322 headers.

Typeobject
scheduled_at?string

Defer the forward; up to 30 days out. The transcript is rendered at request time.

Typestring
suppression_key?string

List this forward is checked against.

Typestring
pacing?object

{ skip, count } — bypass or discount pacing rules.

Typeobject

The forward is its own thread

A forward opens a new thread rather than joining the one it carries; the response’s thread_id is that new thread. Its participants are the forward’s recipients, not the original thread’s, so the two stay apart: threads.reply on the original still infers the original participants, and a reply to the forward lands on the forward thread.

The two are linked. The new thread’s forward_of names the original — on threads.retrieve, threads.list, and the thread.created event it fires. A reply to the forward arrives as an email.received on a thread whose forward_of is set:

const thread = await aiinbx.threads.retrieve(event.data.thread_id)

if (thread.forward_of) {
  // A reply to a forward. thread.forward_of is the thread it carried.
}

The forward returns the same shape a send does, and suppression, pacing, scheduling, and idempotency behave exactly as on one. An idempotency key on a forward is bound to the request — recipients, note, options — rather than to the transcript it rendered, so a retry made after the thread has grown still replays the first forward.

Automated replies

Use a durable worker to read the conversation, decide whether to respond, and send a reply with a stable idempotency key. Thread retrieval returns one page of messages; use iterateMessages or iter_messages when you need the full available conversation.

Classification alone does not make an agent safe to run unattended. Account for duplicate and out-of-order events, concurrent replies, and human handoff. The email agent guide describes the workflow and the application functions it requires.

Next

Last updated on September 9, 2026

Was this page helpful?