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"])idstring
thr_…
stringsubjectstring
The conversation's subject.
stringmailboxstring
The address on your side of the conversation.
stringparticipantsstring[]
Every address that has appeared on the thread.
string[]message_countnumber
Total messages, not just the page.
numberlast_message_atstring
Timestamp of the newest message.
stringmessagesFullEmail[]
Messages, oldest first.
FullEmail[]messages_next_cursorstring | null
Pass as message_cursor to fetch the next page of messages.
string | nullLong 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:
reply_all?boolean
Reply to every participant rather than just the last sender. The simplest way to widen a reply.
booleanfalseto?string | string[]
Replace the inferred recipients.
string | string[]cc?string | string[]
Carbon copies for this reply.
string | string[]bcc?string | string[]
Blind copies for this reply.
string | string[]from?string | object
Send as a different address on the thread — must still be a verified domain or connected mailbox.
string | objectreply_to?string | string[]
Where answers to this reply should go.
string | string[]subject?string
Override the inherited subject.
stringattachments?object[]
Same shape as on a send — up to 20.
object[]headers?object
Additional RFC 5322 headers.
objectscheduled_at?string
Defer the reply; up to 30 days out.
stringsuppression_key?string
List this reply is checked against.
stringunsubscribe?boolean
Mark the reply as optional mail.
booleanpacing?object
{ skip, count } — bypass or discount pacing rules.
objectawait 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.
tostring | string[]
Recipients of the forward.
string | string[]note?string
Text placed above the transcript. Up to 10,000 characters.
stringfrom?string | object
The thread's own mailbox unless set; must be a verified domain or connected mailbox.
string | objectsubject?string
`Fwd:` and the thread's subject unless set.
stringinclude_attachments?boolean
Whether the thread's files ride along. They are listed in the transcript either way.
booleantruecc?string | string[]
Carbon copies.
string | string[]bcc?string | string[]
Blind copies.
string | string[]reply_to?string | string[]
Where replies to the forward should go. Defaults to `from`.
string | string[]attachments?object[]
Files of your own, on top of the thread's — same shape as on a send.
object[]headers?object
Additional RFC 5322 headers.
objectscheduled_at?string
Defer the forward; up to 30 days out. The transcript is rendered at request time.
stringsuppression_key?string
List this forward is checked against.
stringpacing?object
{ skip, count } — bypass or discount pacing rules.
objectThe 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.