Skip to the documentation
API

Threads

Every operation in this group: what it accepts, what it returns and the errors it can answer with.

Operations

GET/threads

List threads in a folder

Scopesthreads:readReads

Reads the local index. Passing query searches that same index.

Plain words must all appear, and each one matches loosely: case, accents and separators are ignored and part of a longer word counts, so min and ben jamin both find "Benjamin". A quoted phrase is matched as written apart from case and accents, so its separators have to line up: "ben jamin" does not find "Ben-Jamin", while "quarterly invoice" finds "Quarterly invoice". When nothing matches exactly, close spellings are returned instead, so benjimin finds "Benjamin": a plain word, or the value of from:, to:, cc:, subject:, body:, filename: or label:, may differ from the start of a word by one typo (a changed, missing, extra or swapped letter) when it has four to seven letters and by two when it has eight or more, while a quoted phrase, a word containing a digit, a shorter word and an excluded word still match exactly, and the pages that follow keep matching the same way. Filler words are dropped from a list of plain words when something else is left to search for, so mail server searches for server alone and emails from john searches for john. The filler is a, an, the, and, or, of, to, from, for, about, with, in, on, at, by, my, me, all, any, some, show, find, get, email, emails, mail, mails, message, messages, thread and threads, and it is kept when dropping it would leave nothing but a single letter.

Operators narrow the search: from:, to:, cc:, subject:, body:, label:, filename:, in: (inbox, sent, drafts, spam, trash, archive, snoozed, anywhere or a label name), is: (unread, read, starred, important, snoozed, muted, draft, sent, bounced for a delivery failure notice, unlabelled for a conversation that carries none of your labels), has: (attachment, pdf, image, video, audio, document, spreadsheet, presentation, userlabels, nouserlabels), after:YYYY/MM/DD, before:YYYY/MM/DD, newer_than:7d and older_than:1y (units h, d, w, m, y). Combine them with OR, AND, NOT, parentheses, braces for a set of alternatives ({stripe paddle}) and a leading - to exclude. Recipients are stored as one list without roles and never hold a Bcc, so cc: reads the same field as to: and bcc: matches nothing of its own. from:me is mail you sent, and to:me is mail carrying one of your own addresses, aliases included, among its recipients or as the address it was delivered to.

A value the search cannot use is ignored rather than narrowing, so a typo in a value widens the result instead of emptying it. Ignored: category:, larger:, smaller:, size:, messagesize:, list:, rfc822msgid:, received:, sent:, the category words (is:primary, is:personal, is:social, is:promotions, is:updates, is:forums, is:reservations, is:purchases), a has: word that names none of the kinds above, an importance: other than high or low, a date that cannot be read, and a duration whose unit is not h, d, w, m or y. An operator name it does not know, project: for instance, is searched as plain text instead.

Words and the from:, to:, cc:, subject: and body: operators read only the newest message on each thread: its sender, its recipients, its subject and the first 4,000 characters of its body. filename: and has: read every attachment on the whole conversation, and label:, in: and is: read the whole conversation. A plain word also matches the name of any attachment on the conversation, whichever message carried it. A message that arrived encrypted has no body text to match. folder still applies unless the query names one with in:, or with an is: that is a folder such as is:sent, and in:anywhere searches every folder, on its own as well as beside other terms. A drafts listing is the exception: GET /drafts and GET /threads?folder=draft stay in drafts whatever the query names.

Dates read the newest activity on the thread, in UTC. after: includes the day it names and before: excludes it. A date can be written YYYY/MM/DD, YYYY-MM-DD, YYYYMMDD, as a bare year, or as epoch seconds or milliseconds; A short date reads day first (16/09/2026), unless the second number cannot be a month (09/16/2026), and a number above 12 settles it either way. A day that does not exist is rejected and ignored.

Send semantic=true to match the plain words of query by meaning rather than by spelling. Describe the mail in a few words, such as flight to Berlin or invoices I still owe, in any language, and the threads closest in meaning come back best match first, together with every thread the words match as text, which ranks above a thread that only means the same. A thread is kept only when it is clearly closer to the words than the rest of the mailbox, so a description nothing fits returns only the text matches. Operators narrow the search exactly as above and folder and the other filters still apply, while sort does not. Meaning is read from the whole conversation, newest messages first and up to 12,000 characters, so an earlier message counts too. A conversation with an encrypted message is never read for meaning and is found by its text alone, new mail can be found by meaning about a minute after it arrives, and a workspace that turned search by meaning off in its settings (semanticSearch: false) gets a text search whatever semantic says. A query with no plain words, from:ada on its own for instance, is a text search whatever semantic says.

nextPageToken is opaque. Pass back what you were given, never construct one.

Requires the threads:read scope.

Query parameters

folderstring

Label the threads must carry, case insensitive. Defaults to inbox, and bin is read as trash.

Default"inbox"
querystring

Mailbox search. Plain words must all appear and match loosely, ignoring case, accents and separators, against the newest message on each thread and the name of any attachment on it, while a quoted phrase has to appear as written and filler words such as the or emails are dropped when something else is left to search for. Operators such as from:ada, label:Invoices, is:unread, has:pdf and newer_than:7d narrow it, and naming a folder with in: replaces folder, so in:anywhere searches them all. When nothing matches exactly, close spellings are returned instead, so benjimin finds "Benjamin". A value the search cannot use is ignored rather than narrowing.

labelIdsstring

Comma-separated label ids. A thread must carry every one of them and sit in folder, which is how the app filters a folder by a label. To list a label's threads whichever folder they are in, pass its id as folder instead: folder=USER_BIG_CLIENTS.

sortstring

The four orders of the thread list: newest, oldest, sender and subject. sender and subject are alphabetical, newest first within one sender or subject. Threads that arrived in the same instant are ordered by id, so every order pages to the end without skipping or repeating one.

One of"newest""oldest""sender""subject"Default"newest"
dateFromstring

Keeps threads whose newest message arrived at or after this instant. ISO 8601 with a time and an offset, such as 2026-09-01T00:00:00Z.

Formatdate-time
dateTostring

Keeps threads whose newest message arrived at or before this instant. Both ends are included, and dateFrom after dateTo is a 422.

Formatdate-time
fromContactsstring

true keeps only threads whose newest message came from a saved contact, the From contacts filter of the thread list. A key limited to particular addresses reads the contacts its owner saved, and an app acting for a member reads the contacts that member saved.

One of"true""false"
semanticstring

true matches the plain words of query by meaning instead of spelling, in any language, and returns the closest threads best match first along with every thread the words match as text. Operators and the other filters still apply and sort does not. It changes nothing when query has no plain words or when the workspace has turned search by meaning off.

One of"true""false"
categorystring

Keep only the threads sorted into this inbox tab: primary, promotions, updates, social or forums. primary is every thread in no other tab. In query, category:promotions filters the same way in any folder.

One of"primary""promotions""updates""social""forums"
addressstring

Keep only the threads delivered to this address, the address filter of the thread list. One the key does not reach lists nothing rather than failing.

3 to 320 characters
limitinteger

Threads per page, a whole number from 1 to 100. Defaults to 25.

At least 1At most 100
pageTokenstring

The previous page's nextPageToken, passed back as it came. Send the same sort, semantic, dates and filters with it.

Returns

A page of threads.

Errors

The errors every operation can return400401403404422429500Error catalog

Also available in

TypeScript
threads.list()threads.listAll()threads.iterate()
Python
threads.list()threads.list_all()threads.iterate()
Ruby
threads.listthreads.list_allthreads.iterate
PHP
threads->listthreads->listAllthreads->iterate
Go
Threads.ListThreads.ListAllThreads.Iterate
Java
threads().listthreads().listAllthreads().iterate
C#
Threads.ListAsyncThreads.ListAllAsyncThreads.IterateAsync
CLI
openemail threads list
MCP
listThreads

GET/threads/{id}

Retrieve a thread

Scopesthreads:readReads

Every message in it, not only the most recent.

The messages are passed through as the mailbox stored them rather than projected onto a field list, and this document describes only encryption, which is the one field whose absence a caller cannot safely guess at. A message that arrived encrypted carries it and has an EMPTY BODY; read encryption.format before concluding a message was empty, and note that the two *-signed formats are ordinary readable mail.

Requires the threads:read scope.

Path parameters

idstringRequired

Thread id, as returned by list or carried on a message.

Returns

The thread, with every message on it.

Errors

The errors every operation can return400401403404422429500Error catalog

Also available in

TypeScript
threads.get()
Python
threads.get()
Ruby
threads.get
PHP
threads->get
Go
Threads.Get
Java
threads().get
C#
Threads.GetAsync
CLI
openemail threads get
MCP
getThread

PATCH/threads/{id}

Mark read/unread and change labels

Scopesthreads:writeChanges data

Applies and removes labels, and sets read state, on one conversation. Label ids come from GET /labels; the system ids INBOX, ARCHIVE, STARRED, IMPORTANT, SPAM and UNREAD are taken in any case, so archiving is addLabelIds: ["ARCHIVE"] with removeLabelIds: ["INBOX"]. MUTE in addLabelIds mutes the thread as POST /threads/{id}/mute does, and the tab labels CATEGORY_PROMOTIONS, CATEGORY_UPDATES, CATEGORY_SOCIAL and CATEGORY_FORUMS move a thread between inbox tabs. An id in addLabelIds that names no label is a 422 label_not_found, and nothing on the thread changes: create the label with POST /labels first. An unknown id in removeLabelIds is not an error, since the thread does not carry it. TRASH, SNOOZED and DRAFT are refused with label_not_directly_settable; use the trash and snooze endpoints.

Requires the threads:write scope.

Path parameters

idstringRequired

Thread id.

Request body

readboolean

true removes UNREAD and false adds it.

addLabelIdsstring[]

Label ids to put on the thread. Each must name a label.

Up to 50 items
removeLabelIdsstring[]

Label ids to take off the thread.

Up to 50 items

Returns

Errors

422

label_not_found for an id in addLabelIds that names no label, or label_not_directly_settable for TRASH, SNOOZED or DRAFT.

The errors every operation can return400401403404429500Error catalog

Also available in

TypeScript
threads.update()
Python
threads.update()
Ruby
threads.update
PHP
threads->update
Go
Threads.Update
Java
threads().update
C#
Threads.UpdateAsync
CLI
openemail threads update
MCP
markThreadsReadmarkThreadsUnreadmodifyLabelsrestoreThreads

DELETE/threads/{id}

Delete a thread for good

Scopesthreads:writeDeletes

Deletes every message in the thread, with its attachments, and cannot be undone, which is what Delete from Bin does in the app. It works on a thread in any folder, so move it to the Bin with POST /threads/{id}/trash when you only mean to throw it away.

Requires the threads:write scope.

Path parameters

idstringRequired

The thread, as GET /threads returns it.

Returns

Gone for good.

Errors

500

thread_delete_failed: the thread could not be deleted, and nothing was removed.

The errors every operation can return400401403404422429Error catalog

Also available in

TypeScript
threads.delete()
Python
threads.delete()
Ruby
threads.delete
PHP
threads->delete
Go
Threads.Delete
Java
threads().delete
C#
Threads.DeleteAsync
CLI
openemail threads delete
MCP
deleteThreadsForever

POST/threads/{id}/trash

Move a thread to the Bin

Scopesthreads:writeDeletes

Clears INBOX, SPAM, SNOOZED and ARCHIVE together, which is what the app does. Adding the TRASH label by hand through PATCH is refused, because doing only half of it leaves the thread listed in the Bin AND its old folder.

Requires the threads:write scope.

Path parameters

idstringRequired

Thread id.

Returns

Trashed.

Errors

The errors every operation can return400401403404422429500Error catalog

Also available in

TypeScript
threads.trash()
Python
threads.trash()
Ruby
threads.trash
PHP
threads->trash
Go
Threads.Trash
Java
threads().trash
C#
Threads.TrashAsync
CLI
openemail threads trash
MCP
trashThreads

POST/threads/{id}/snooze

Snooze a thread

Scopesthreads:writeChanges data

Hides it and schedules its return. Both halves happen together: the label hides it, a stored wake time brings it back, and a thread snoozed by label alone never returns.

Requires the threads:write scope.

Path parameters

idstringRequired

Thread id.

Request body

wakeAtstringRequired
Formatdate-time

Returns

Snoozed.

Errors

The errors every operation can return400401403404422429500Error catalog

Also available in

TypeScript
threads.snooze()
Python
threads.snooze()
Ruby
threads.snooze
PHP
threads->snooze
Go
Threads.Snooze
Java
threads().snooze
C#
Threads.SnoozeAsync
CLI
openemail threads snooze
MCP
snoozeThreads

POST/threads/{id}/unsnooze

Unsnooze a thread

Scopesthreads:writeChanges data

Brings it back now and cancels the scheduled return.

Requires the threads:write scope.

Path parameters

idstringRequired

Thread id.

Returns

Back in the inbox, with snoozedUntil null.

Errors

The errors every operation can return400401403404422429500Error catalog

Also available in

TypeScript
threads.unsnooze()
Python
threads.unsnooze()
Ruby
threads.unsnooze
PHP
threads->unsnooze
Go
Threads.Unsnooze
Java
threads().unsnooze
C#
Threads.UnsnoozeAsync
CLI
openemail threads unsnooze
MCP
unsnoozeThreads

GET/threads/{id}/messages/{messageId}/attachments

A message's attachments

Scopesthreads:readReads

content is base64, and an empty string where the stored bytes could not be found.

This is the DISPLAY list. Of the parts named by a message's encryption.parts, the ciphertext part IS listed and downloads like any other file (it is the message, and downloading it is the only way an API client reads this mail). The PGP/MIME version part and any detached signature are held out deliberately, because they rendered as junk attachments and there is nothing a caller can do with them. Both keep their ids in encryption.parts, which correlates the two views, and this endpoint does not return them.

Requires the threads:read scope.

Path parameters

idstringRequired

Thread id the message belongs to.

messageIdstringRequired

Message id from that thread's messages.

Returns

Attachments.

Errors

The errors every operation can return400401403404422429500Error catalog

Also available in

TypeScript
threads.listAttachments()
Python
threads.list_attachments()
Ruby
threads.list_attachments
PHP
threads->listAttachments
Go
Threads.ListAttachments
Java
threads().listAttachments
C#
Threads.ListAttachmentsAsync
CLI
openemail threads list-attachments

GET/threads/counts

Count the mail in each folder

Scopesthreads:readReads

What the sidebar of the app shows: how many conversations each folder holds and how many of them are unread, how many drafts are waiting, how many conversations each of your labels has in each folder, how many inbox conversations sit in each tab, and how many inbox conversations arrived at each address. unread is a pseudo-folder whose count is the unread conversations in the inbox, and draft counts drafts, which are never unread. A key or an app limited to particular addresses counts only the mail that arrived at them and the drafts written from them, and address narrows the counts to one address, never wider than that.

Requires the threads:read scope.

Query parameters

addressstring

Count only the mail delivered to this address. One the key does not reach counts nothing rather than failing.

3 to 320 characters

Returns

The counts.

Errors

The errors every operation can return400401403404422429500Error catalog

Also available in

TypeScript
threads.counts()
Python
threads.counts()
Ruby
threads.counts
PHP
threads->counts
Go
Threads.Counts
Java
threads().counts
C#
Threads.CountsAsync
CLI
openemail threads counts
MCP
getMailboxCounts

GET/threads/{id}/summary

Read the summary of a thread

Scopesthreads:readReads

The short AI summary the reading pane shows above a thread. It is written the first time it is asked for and then kept, and the first request after a newer message arrives writes it again, so reading it is cheap. state is ready with the summary, pending while the first one is being written (ask again in a few seconds), or none when there is nothing to summarise, such as a thread holding a message that arrived encrypted. While a newer summary is being written, the one before it comes back as ready. Summaries spend no AI actions.

Requires the threads:read scope.

Path parameters

idstringRequired

The thread, as GET /threads returns it.

Returns

The summary, or the state it is in.

Errors

The errors every operation can return400401403404422429500Error catalog

Also available in

TypeScript
threads.summary()
Python
threads.summary()
Ruby
threads.summary
PHP
threads->summary
Go
Threads.Summary
Java
threads().summary
C#
Threads.SummaryAsync
CLI
openemail threads summary
MCP
getThreadSummary

POST/threads/{id}/restore

Take a thread out of the Bin

Scopesthreads:writeChanges data

Puts a thread back in the inbox, out of the Bin and out of Spam, which is what Restore from Bin and Move to inbox do in the app. It is the undo of POST /threads/{id}/trash, and calling it again changes nothing.

Requires the threads:write scope.

Path parameters

idstringRequired

The thread, as GET /threads returns it.

Returns

Back in the inbox.

Errors

500

thread_update_failed: the labels could not be written, and nothing on the thread changed.

The errors every operation can return400401403404422429Error catalog

Also available in

TypeScript
threads.restore()
Python
threads.restore()
Ruby
threads.restore
PHP
threads->restore
Go
Threads.Restore
Java
threads().restore
C#
Threads.RestoreAsync
CLI
openemail threads restore
MCP
modifyLabelsrestoreThreads

GET/threads/{id}/reply-suggestions

Suggest replies to a thread

Scopesthreads:readReads

Up to 3 short replies to the latest message of the thread, the ones the reading pane offers under it. Each has a label, a few words that say what it answers, and a body, the reply itself, ready to send with POST /emails or to keep with POST /drafts. They are written in the language of that message and the voice of the mail this workspace sends, from the conversation, what was written to this correspondent before, what is known about their organisation and, when the message asks for a time, the free and busy times of the calendar.

Suggestions are written once for each new message and kept, so asking again is free until another message arrives. state is ready with the suggestions, pending while they are being written (ask again in a few seconds), or none when the message needs no reply: your own reply came last, or it is a newsletter, an automated notice, mail from a no-reply address, spam or an encrypted message, and every thread is none in a workspace that turned replySuggestions off in its settings. messageId names the message they answer. Writing suggestions spends no AI action, and a workspace has suggestions written for at most 500 new messages a day.

Requires the threads:read scope.

Path parameters

idstringRequired

The thread, as GET /threads returns it.

Returns

The suggested replies, or the state they are in.

Errors

409

ai_not_configured: suggesting replies is not available on this server.

The errors every operation can return400401403404422429500Error catalog

Also available in

TypeScript
threads.replySuggestions()
Python
threads.reply_suggestions()
Ruby
threads.reply_suggestions
PHP
threads->replySuggestions
Go
Threads.ReplySuggestions
Java
threads().replySuggestions
C#
Threads.ReplySuggestionsAsync
CLI
openemail threads reply-suggestions
MCP
getReplySuggestions

POST/threads/{id}/mute

Mute a thread

Scopesthreads:writeChanges data

Mutes a conversation, as Mute does in the app. A thread in the inbox moves to the archive, and from then on new mail in it goes straight to the archive, unread, with no push, sound or desktop notice. A webhook for that mail carries muted: true. Thread state belongs to the whole workspace, so the thread is muted for everyone on it, and mail addressed only to you does not bring it back. Calling it again changes nothing. PATCH /threads/{id} with MUTE in addLabelIds does the same.

Requires the threads:write scope.

Path parameters

idstringRequired

The thread, as GET /threads returns it.

Returns

Muted, with muted true.

Errors

The errors every operation can return400401403404422429500Error catalog

Also available in

TypeScript
threads.mute()
Python
threads.mute()
Ruby
threads.mute
PHP
threads->mute
Go
Threads.Mute
Java
threads().mute
C#
Threads.MuteAsync
CLI
openemail threads mute
MCP
muteThreads

POST/threads/{id}/unmute

Unmute a thread

Scopesthreads:writeChanges data

Takes the mute off, so the next message in the thread arrives in the inbox and alerts people again. The thread stays where it is: move it back with PATCH /threads/{id} and INBOX if you want it in the inbox now. A thread that is not muted is left as it is.

Requires the threads:write scope.

Path parameters

idstringRequired

The thread, as GET /threads returns it.

Returns

Not muted any more, with muted false.

Errors

The errors every operation can return400401403404422429500Error catalog

Also available in

TypeScript
threads.unmute()
Python
threads.unmute()
Ruby
threads.unmute
PHP
threads->unmute
Go
Threads.Unmute
Java
threads().unmute
C#
Threads.UnmuteAsync
CLI
openemail threads unmute
MCP
unmuteThreads

GET/follow-ups

List reminders

Scopesthreads:readReads

The reminders you set on mail you sent, newest first: the ones still waiting for a reply and the ones that ended. A reminder is waiting until somebody replies (answered), its time comes with no reply (due, and the thread is back at the top of the inbox, unread) or it is cancelled. An API key lists the reminders of the workspace owner, and an app those of the person who connected it. A key or an app limited to particular addresses lists only reminders on threads delivered to them.

Requires the threads:read scope.

Query parameters

statusstring

Only reminders in this state.

One of"waiting""answered""due""cancelled"
threadIdstring

Only the reminders on this thread.

1 to 256 characters
limitinteger

How many to return, newest first. 100 when left out.

At least 1At most 200

Returns

The reminders.

Errors

The errors every operation can return400401403404422429500Error catalog

Also available in

TypeScript
followUps.list()
Python
follow_ups.list()
Ruby
follow_ups.list
PHP
followUps->list
Go
FollowUps.List
Java
followUps().list
C#
FollowUps.ListAsync
CLI
openemail follow-ups list
MCP
listFollowUps

POST/follow-ups

Set a reminder on a thread

Scopesthreads:writeChanges data

Asks to be reminded about a thread if nobody replies by remindAt. If a real reply arrives first, the reminder closes by itself. Otherwise, when the time comes, the thread returns to the top of the inbox, unread, with the built-in FOLLOW_UP label and a push to your phones. Automatic replies, delivery reports and your own sending address do not count as a reply. A thread holds one waiting reminder per person, so setting another moves the time. To set one as you send, pass followUpAt to POST /emails instead.

Requires the threads:write scope.

Request body

threadIdstringRequired

The thread to be reminded about, as threads.list returns it.

1 to 256 characters
remindAtstringRequired

When the thread comes back if nobody replied: a future instant, at most 365 days out.

Formatdate-time

Returns

The reminder, waiting.

Errors

422

invalid_follow_up: remindAt is not in the future or is more than 365 days out, or somebody has already replied in the thread since your last message.

The errors every operation can return400401403404429500Error catalog

Also available in

TypeScript
followUps.set()
Python
follow_ups.set()
Ruby
follow_ups.set
PHP
followUps->set
Go
FollowUps.Set
Java
followUps().set
C#
FollowUps.SetAsync
CLI
openemail follow-ups set
MCP
setFollowUp

DELETE/follow-ups/{id}

Cancel a reminder

Scopesthreads:writeDeletes

Cancels a waiting reminder, so the thread stays where it is when the time comes. A reminder that already ended is returned as it is. Only your own reminders can be cancelled.

Requires the threads:write scope.

Path parameters

idstringRequired

The reminder, as the list returns it.

Returns

The reminder as it is now.

Errors

The errors every operation can return400401403404422429500Error catalog

Also available in

TypeScript
followUps.cancel()
Python
follow_ups.cancel()
Ruby
follow_ups.cancel
PHP
followUps->cancel
Go
FollowUps.Cancel
Java
followUps().cancel
C#
FollowUps.CancelAsync
CLI
openemail follow-ups cancel
MCP
cancelFollowUp

GET/sender-categories

List sender choices

Scopesthreads:readReads

The senders whose mail always goes to one inbox tab, by address. New mail is sorted into Primary, Promotions, Updates, Social or Forums by plain rules, and a choice saved here wins over them. GET /threads takes category to read one tab.

Requires the threads:read scope.

Returns

The choices, by sender.

Errors

The errors every operation can return400401403404422429500Error catalog

Also available in

TypeScript
threads.listSenderCategories()
Python
threads.list_sender_categories()
Ruby
threads.list_sender_categories
PHP
threads->listSenderCategories
Go
Threads.ListSenderCategories
Java
threads().listSenderCategories
C#
Threads.ListSenderCategoriesAsync
CLI
openemail threads list-sender-categories
MCP
listSenderCategories

PUT/sender-categories/{email}

Always sort a sender into one tab

Scopesthreads:writeChanges data

Saves the tab for one sender and moves the inbox threads whose newest message is from them, up to the newest 1,000, into it. Mail from them is sorted there from now on, whatever the rules would say. Saving again replaces the choice. A workspace holds at most 2,000 choices, and they apply to every address in it, so a key or an app limited to particular addresses or domains can read them but not change them.

Requires the threads:write scope.

Path parameters

emailstringRequired

The address of the sender, compared without case.

Up to 320 charactersFormatemail

Request body

categorystringRequired
One of"primary""promotions""updates""social""forums"

Returns

Errors

422

invalid_parameter: the address or the category is not valid. sender_category_limit_reached: the workspace holds as many choices as it may. capability_unsupported: the key or app is limited to particular addresses or domains.

The errors every operation can return400401403404429500Error catalog

Also available in

TypeScript
threads.setSenderCategory()
Python
threads.set_sender_category()
Ruby
threads.set_sender_category
PHP
threads->setSenderCategory
Go
Threads.SetSenderCategory
Java
threads().setSenderCategory
C#
Threads.SetSenderCategoryAsync
CLI
openemail threads set-sender-category
MCP
setSenderCategory

DELETE/sender-categories/{email}

Remove a sender choice

Scopesthreads:writeDeletes

Removes the choice, so new mail from the sender is sorted by the rules again. Threads already sorted stay where they are.

Requires the threads:write scope.

Path parameters

emailstringRequired

The address of the sender, compared without case.

Up to 320 charactersFormatemail

Returns

Errors

The errors every operation can return400401403404422429500Error catalog

Also available in

TypeScript
threads.clearSenderCategory()
Python
threads.clear_sender_category()
Ruby
threads.clear_sender_category
PHP
threads->clearSenderCategory
Go
Threads.ClearSenderCategory
Java
threads().clearSenderCategory
C#
Threads.ClearSenderCategoryAsync
CLI
openemail threads clear-sender-category
MCP
clearSenderCategory

GET/threads/{id}/messages

List the messages of a thread

Scopesthreads:readReads

Every message on the thread, oldest first, each as a MailboxMessage with its sender, recipients, date, Message-ID, text and HTML body, attachments and headers. Unsent draft replies are in the list with draft: true. The fields here are a contract: they are added to, never renamed or taken away, whatever the mailbox stores underneath. GET /threads/{id} still returns each message the way the mailbox stored it.

Requires the threads:read scope.

Path parameters

idstringRequired

The thread, as GET /threads returns it.

Returns

The messages.

Errors

The errors every operation can return400401403404422429500Error catalog

Also available in

TypeScript
threads.listMessages()
Python
threads.list_messages()
Ruby
threads.list_messages
PHP
threads->listMessages
Go
Threads.ListMessages
Java
threads().listMessages
C#
Threads.ListMessagesAsync
CLI
openemail threads list-messages

GET/threads/{id}/messages/{messageId}

Read one message of a thread

Scopesthreads:readReads

One message, in the shape GET /threads/{id}/messages lists. The thread is checked first and then the message, so a message that is not on that thread is a 404 even when it exists elsewhere in the mailbox. The fields here are a contract: they are added to, never renamed or taken away, whatever the mailbox stores underneath. GET /threads/{id} still returns each message the way the mailbox stored it.

Requires the threads:read scope.

Path parameters

idstringRequired

The thread, as GET /threads returns it.

messageIdstringRequired

The message, as id on one of the messages GET /threads/{id}/messages returns.

Returns

The message.

Errors

The errors every operation can return400401403404422429500Error catalog

Also available in

TypeScript
threads.getMessage()
Python
threads.get_message()
Ruby
threads.get_message
PHP
threads->getMessage
Go
Threads.GetMessage
Java
threads().getMessage
C#
Threads.GetMessageAsync
CLI
openemail threads get-message

Objects

DeletedSenderCategoryobject

objectstringRequired
One of"sender_category"
senderstringRequired
Formatemail
deletedbooleanRequired
One oftrue

DeletedThreadobject

objectstring
One of"thread"
idstring
deletedboolean
One oftrue

FollowUpobject

objectstringRequired
One of"follow_up"
idstringRequired
threadIdstringRequired

The thread. Null until a scheduled message with a reminder goes out.

Can be null
messageIdstringRequired

The Message-ID of the mail the reminder is about, once it went out.

Can be null
emailIdstringRequired

The email, as GET /emails lists it, when the reminder was set with followUpAt.

Can be null
subjectstringRequired

The subject of the thread.

Can be null
remindAtstringRequired

When the thread comes back if nobody replied.

Formatdate-time
statusstringRequired

waiting for a reply, answered by one, due when the time came with none, or cancelled.

One of"waiting""answered""due""cancelled"
answeredAtstringRequired

When the reply arrived. Null unless answered.

Can be nullFormatdate-time
firedAtstringRequired

When the thread was brought back. Null unless due.

Can be nullFormatdate-time
createdAtstringRequired

When the reminder was set.

Formatdate-time

FollowUpListobject

objectstring
One of"list"

KnowledgeSourceRefobject

idstringRequired

The knowledge item, as GET /knowledge/{id} reads it.

titlestringRequired
kindstringRequired
One of"note""file""link"
scopestringRequired

A scope is empty for the whole workspace, @ and a domain for everything on that domain (@acme.com), or one address ([email protected]). The AI working for an address reads that address, then its domain, then the whole workspace, the most specific first.

urlstringRequired

The page a link item reads, or null.

Can be null

MailboxCountsobject

objectstringRequired
One of"mailbox_counts"
foldersobject[]Required
labelstringRequired

The folder, as its label id in lower case.

One of"inbox""sent""spam""archive""trash""snoozed""draft""unread"
countintegerRequired

Conversations in the folder. For draft, the drafts waiting to be sent.

unreadintegerRequired

How many of them are unread. Always 0 for draft.

labelsobject[]Required

One row for each of your labels and each folder it has conversations in. A label with no conversations in a folder has no row for it.

idstringRequired

The label, as GET /labels lists it.

folderstringRequired

The folder, as its label id in lower case.

One of"inbox""sent""spam""archive""trash""snoozed"
countintegerRequired

Conversations in the folder that carry the label.

unreadintegerRequired

How many of them are unread.

categoriesobject[]Required

One row for each inbox tab, counting inbox conversations only. primary is everything not sorted into another tab.

categorystringRequired

The tab.

One of"primary""promotions""updates""social""forums"
countintegerRequired

Inbox conversations in the tab.

unreadintegerRequired

How many of them are unread.

unlabelledobjectRequired

The inbox conversations that carry none of your labels, which is what the inbox of the app lists unless Labelled mail is ticked in its Filters menu. A conversation the default rule labelled BOUNCED counts as labelled.

countintegerRequired

Inbox conversations with no label.

unreadintegerRequired

How many of them are unread.

categoriesobject[]Required

The same count for each inbox tab.

categorystringRequired

The tab.

One of"primary""promotions""updates""social""forums"
countintegerRequired

Inbox conversations in the tab with no label.

unreadintegerRequired

How many of them are unread.

addressesobject[]Required
addressstringRequired

The address the mail arrived at, or null for mail that recorded none.

Can be null
countintegerRequired

Inbox conversations delivered to it.

teamobjectRequired

The team views of a shared inbox, counted for the person the key or app acts for. A conversation is open while it is in the inbox, and marking it done archives it.

assignedobjectRequired

Open conversations assigned to you.

countintegerRequired

Open conversations assigned to you.

unreadintegerRequired

How many of them are unread.

unassignedobjectRequired

Open conversations nobody is assigned to.

countintegerRequired

Open conversations nobody has.

unreadintegerRequired

How many of them are unread.

mentionsobjectRequired

Conversations outside Spam and the bin where a team comment mentions you.

countintegerRequired

Conversations that mention you.

unreadintegerRequired

How many of them are unread.

unseenintegerRequired

How many of them have a mention newer than the last time you opened their comments in the app.

MailboxMessageobject

A message in the mailbox, in a shape that does not change with how it is stored. The fields here are a contract: they are added to, never renamed or taken away, whatever the mailbox stores underneath. GET /threads/{id} still returns each message the way the mailbox stored it.

objectstringRequired
One of"message"
idstringRequired

The message, msg_ and 24 hex characters, for the message and attachment routes.

threadIdstringRequired

The conversation it belongs to.

messageIdstringRequired

Its Message-ID header, angle brackets included. A message that arrived without one has one Nextmail made up.

Can be null
fromobjectRequired

Who it is from.

namestringRequired

The display name, or null when there was none.

Can be null
emailstringRequired

The address.

toobject[]Required

Its To recipients.

namestringRequired

The display name, or null when there was none.

Can be null
emailstringRequired

The address.

ccobject[]Required

Its Cc recipients.

namestringRequired

The display name, or null when there was none.

Can be null
emailstringRequired

The address.

bccobject[]Required

Its Bcc recipients. Only mail the workspace sent knows them, so mail that arrived has none.

namestringRequired

The display name, or null when there was none.

Can be null
emailstringRequired

The address.

replyToobject[]Required

Where a reply goes, when that is not the sender.

namestringRequired

The display name, or null when there was none.

Can be null
emailstringRequired

The address.

subjectstringRequired

The subject line, (no subject) when it had none.

datestringRequired

When it was written, from its Date header. Null when that header could not be read.

Can be nullFormatdate-time
draftbooleanRequired

True for a reply written in the app and not sent yet.

inReplyTostringRequired

The Message-ID of the message it answers, from its In-Reply-To header.

Can be null
referencesstring[]Required

The Message-IDs of its References header, oldest first.

textstringRequired

The body as plain text. For a message that came as HTML only, this is the text drawn from that HTML, with links written out after their words. Null when it has no body.

Can be null
htmlstringRequired

The HTML body as it arrived or was sent, unprocessed. Null for a plain text message.

Can be null
attachmentsobject[]Required

Its attachments, without their bytes. An image shown inside the body is not listed. Read the bytes with GET /threads/{id}/messages/{messageId}/attachments.

idstringRequired

The attachment, as attachmentId in the attachment list.

filenamestringRequired
mimeTypestringRequired
sizeintegerRequired

Its size in bytes.

headersobject[]Required

The headers Nextmail keeps of the message, written out as it would send them: From, To, Cc, Reply-To, Subject, Date, Message-ID, In-Reply-To, References, List-Unsubscribe, List-Unsubscribe-Post. A header the message did not have is left out. The rest of what it arrived with is not kept.

namestringRequired
valuestringRequired
encryptionobjectRequired

Set when the message is signed or encrypted. sealed is true when its body cannot be read without the key, so text holds armour or nothing. Null for ordinary mail.

Can be null
formatstringRequired
One of"pgp-mime""pgp-signed""pgp-inline""smime-encrypted""smime-signed"
sealedbooleanRequired

MessageAttachmentListobject

objectstringRequired
One of"list"
dataobject[]Required
objectstringRequired
One of"attachment"
attachmentIdstringRequired
filenamestringRequired
contentTypestringRequired
sizeintegerRequired

Bytes.

At least 0
contentstringRequired

The bytes, base64. An empty string when the stored bytes could not be found.

Formatbyte

MessageEncryptionobject

What kind of encryption this message arrived carrying, read off its top-level Content-Type when it was ingested.

ABSENCE MEANS NOBODY LOOKED. The field is omitted on every message stored before detection existed. It never means "checked, and found none". A client reading a missing encryption as "this was plaintext" is asserting something no part of this system measured.

It is a statement about the ENVELOPE and not a verification. A message can be seen to be sealed without being opened, and those are different claims: nothing here says a signature checked out, says who holds a key, or licenses a padlock in a user interface.

SIGNED IS NOT SEALED, and that distinction is the whole reason format is an enum rather than a flag. For the three sealed formats the message's body fields are empty or hold PGP armor, and an empty body on such a message means "we cannot read this" rather than "there was nothing here". For the two signed formats the body is ordinary text and every consumer keeps working on it.

formatstringRequired

Which of the five envelope shapes was recognised. THREE of them mean the body is unreadable (pgp-mime, pgp-inline and smime-encrypted), and the other two, pgp-signed and smime-signed, mean the body arrived in the clear beside a detached signature and is read exactly like any other message. Branch on this value; branching on the mere presence of the object treats readable mail as unreadable.

One of"pgp-mime""pgp-signed""pgp-inline""smime-encrypted""smime-signed"
detectedAtstringRequired

When the classification was made, on our clock at ingest. It dates OUR READING of the envelope and says nothing about when the message was encrypted or signed, or by whom.

rawRetainedboolean

Whether the original RFC822 bytes were kept, so that something holding the key could open the message later. Today it is always false. Nothing retains the raw message yet. It is published now so a client can start reading it rather than needing a second pass over every stored message the day that changes.

Defaultfalse
partsobject[]

The envelope parts, each named by the id its bytes were written under. They are deliberately NOT in the message's attachment list (a PGP/MIME message would otherwise render two junk chips), so a sealed message commonly reports no attachments at all, and that emptiness is not evidence that nothing was attached. These ids are a CORRELATION KEY and not a handle: GET /threads/{id}/messages/{messageId}/attachments serves the display list, so it does not return them, and no other path in this API returns them either. index is the index into the original MIME parts.

indexintegerRequired
attachmentIdstringRequired
rolestringRequired
One of"version""ciphertext""signature"

MutedThreadobject

objectstringRequired
One of"thread"
idstringRequired
mutedbooleanRequired

Whether the thread is muted now.

ReplySuggestionobject

labelstringRequired

What the reply says, in a few words, as a button would show it.

Up to 60 characters
bodystringRequired

The reply in plain text, with no subject and no signature.

Up to 1200 characters

ReplySuggestionsobject

objectstringRequired
One of"reply_suggestions"
threadIdstringRequired
messageIdstringRequired

The message the suggestions answer, the latest one in the thread.

Can be null
statestringRequired
One of"ready""pending""none"
suggestionsReplySuggestion[]Required

Empty unless state is ready.

Up to 3 items
sourcesKnowledgeSourceRef[]Required

The knowledge base items the suggestions drew on: pinned notes at the levels of the receiving address and the passages that matched the message. Empty when none were used.

RestoredThreadobject

objectstring
One of"thread"
idstring
restoredboolean
One oftrue

SenderCategoryobject

objectstringRequired
One of"sender_category"
senderstringRequired

The sender, in lower case.

Formatemail
categorystringRequired

The inbox tab their mail goes to.

One of"primary""promotions""updates""social""forums"
updatedAtstringRequired

The last save.

Formatdate-time

Threadobject

objectstring
One of"thread"
idstring

The id that was asked for. Echoed back rather than read off the result. The mailbox answers a thread as messages, labels and counts, with no id of its own.

messagesThreadMessage[]

Every message on the thread, oldest first, not only the most recent.

labelsobject[]

The folders and labels the thread sits in. Backend-shaped like the messages are, so only the two fields every backend agrees on are described.

idstring
namestring
messageCountinteger

The length of messages, counted here.

hasUnreadboolean
totalRepliesinteger
deliveredTostring

The workspace address the thread belongs to, lower-cased: the address its first message was delivered to, with any +tag removed, or the address it was sent from when the thread began with a send. Later messages joining the thread do not change it. Null when no address was recorded.

Can be null

ThreadLabelChangeobject

objectstringRequired
One of"thread"
idstringRequired
addedLabelIdsstring[]Required

The labels put on, with UNREAD when read was false.

removedLabelIdsstring[]Required

The labels taken off, with UNREAD when read was true. Unknown ids are passed through.

updatedThreadIdsstring[]Required

The conversations the change was written to.

failedThreadIdsstring[]Required

The conversations it could not be written to.

ThreadListobject

objectstringRequired
One of"list"
dataobject[]Required
objectstringRequired
One of"thread"
idstringRequired
nextPageTokenstringRequired

Pass it back as pageToken for the next page. Null on the last page.

Can be null

ThreadMessageobject

A message, in whatever shape the mailbox stored it. The fields are deliberately not enumerated here. encryption, sentWith, forwardedFrom and verificationCode are the properties this API describes, because their absence cannot be guessed at safely.

sentWithstring

What the message was sent with, read from its headers when it arrived: a mail provider such as gmail or outlook, a sending service such as resend or sendgrid, openemail for mail sent through Nextmail, or amazon-ses when only the network it came through is known. Absent when nothing in the message says.

One of"openemail""gmail""outlook""icloud""yahoo""proton""zoho""fastmail""resend""sendgrid""mailgun""postmark""mailchimp""brevo""sparkpost""hubspot""amazon-ses"
forwardedFromstring

The address the message was first sent to, when it reached you through forwarding rather than directly. A reply cannot come from that address unless it is one of yours. Null when the message came to you directly, and absent on messages that arrived before this was recorded.

Can be null
verificationCodestring

The sign-in or verification code the message carries, as you would type it: digits only, or capital letters and digits, without spaces or dashes. Set only when Nextmail is sure: the sender passed authentication, nothing about the message was flagged, it was not filed to Spam or Trash, and it holds exactly one code worded as one. Null when the message carries none or any of that is in doubt. Absent where a listing leaves out message bodies and the message arrived before codes were read.

Can be null

ThreadSnoozeobject

objectstringRequired
One of"thread"
idstringRequired
snoozedUntilstringRequired

When the thread comes back to the inbox. Null once it is unsnoozed.

Can be nullFormatdate-time

ThreadSummaryobject

objectstringRequired
One of"thread_summary"
threadIdstringRequired
statestringRequired

ready when the summary is written. pending while it is being written, so ask again in a few seconds. failed when the last try did not work, and asking again tries once more. none when there is nothing to summarise.

One of"ready""pending""failed""none"
summarystringRequired

One or two sentences, present when state is ready.

Can be null

TrashedThreadobject

objectstringRequired
One of"thread"
idstringRequired
trashedbooleanRequired
One oftrue