Read mail
List mailboxes, list and search messages, read bodies, threads and attachments, and change flags, folders and labels.
Workwae keeps an index of each account's mail. Read mailboxes, messages and threads from it, open bodies and attachments, and change flags or move messages; every change goes to the provider. The full list of operations is in the Mail reference.
Sync window and initial sync
A new account syncs in the background. Until its first mail sync finishes, the account's initializedAt is null. The account.initialized event announces the end of it with the number of mailboxes and messages indexed; messages from before it are not announced one by one.
- The index covers the account's sync window: the last
syncWindowDaysdays (90 by default;nullkeeps the full mailbox history). Set it in the tenant settings or per account insettings. bodyStoragedecides whether message content is stored:none(the default) reads bodies from the provider on request,cachestores a message's content the first time it is read,fullstores every indexed message.- Changing
syncWindowDaysorbodyStoragerestarts the account's syncs.fullbackfills every indexed message;nonedeletes stored content. POST /v1/accounts/{accountId}/syncwakes a sync now, at most 30 times per account per minute.
Listings cover the sync window only. Search can reach older mail through the provider.
List mailboxes
Mailboxes are the account's folders (Microsoft, IMAP) or labels (Gmail). Find the inbox and other system mailboxes by role.
curl "https://api.workwae.com/v1/accounts/$ACCOUNT_ID/mailboxes" \
-H "Authorization: Bearer $WORKWAE_API_KEY"| Field | Meaning |
|---|---|
role | inbox, sent, drafts, trash, junk, archive, all, important, or custom for the user's own. |
kind | folder: a message is in exactly one. label: a message can carry several (Gmail). |
path | The full name, /-separated from the top level. parentId names the parent. |
totalCount, unreadCount | Message counts, or null when the provider reports none. They change without an event. |
List messages
GET /v1/accounts/{accountId}/messages lists messages newest first. Without mailboxId it lists every mailbox. Results are paged with limit (1 to 200, default 50) and cursor; see Pagination.
curl "https://api.workwae.com/v1/accounts/$ACCOUNT_ID/messages?mailboxId=$MAILBOX_ID&unread=true" \
-H "Authorization: Bearer $WORKWAE_API_KEY"| Filter | Matches |
|---|---|
mailboxId | Messages in that mailbox. An id the account does not have answers 422 unknown_mailbox. |
from, to | Part of an address or display name, case-insensitive. to looks at To, Cc and Bcc. |
unread, flagged, hasAttachments | true or false. |
importance | high, normal or low. Messages indexed before Workwae read priorities (importance: null) match none. |
after, before | receivedAt at or after, and before. before must be later than after (400 invalid_range). |
Each item is a message summary: addresses, subject, snippet, receivedAt, isRead, isFlagged, hasAttachments, mailboxIds, threadId and these:
direction:outboundwhen the message is in asentmailbox or its From is the account's address or one of its identities (drafts included);inboundotherwise.outboxIdandreference: set when the message is a copy of a send made through the API. They stay after outbox retention deletes the entry.categories: Microsoft categories, or the Gmail inbox tab (personal,social,promotions,updates,forums).importance: read from Graph'simportance, and from the priority headers on Gmail and IMAP, where a message without one readsnormal.
Search messages
GET /v1/accounts/{accountId}/search/messages takes a query q (up to 500 characters) and the same filters as the list, except importance. Pages hold up to 100 results (default 25).
curl --get "https://api.workwae.com/v1/accounts/$ACCOUNT_ID/search/messages" \
-H "Authorization: Bearer $WORKWAE_API_KEY" \
--data-urlencode 'q="quarterly report" -draft' \
--data-urlencode "from=ada@example.com"source picks where the search runs:
source | Behavior |
|---|---|
index | Full-text search in web search syntax (words, "phrases", OR, -word) over subject, sender and recipient names and addresses, the snippet, and the body text of stored messages. Best matches first. |
provider | Passes q to the provider unchanged: Gmail search syntax, Microsoft KQL, or IMAP SEARCH TEXT. Newest first. |
auto (default) | Asks the provider when the index cannot cover the request: after lies before the sync window, or the index found nothing while it does not hold everything (the account has a sync window, or bodyStorage is not full). Otherwise, and whenever the provider cannot be asked (inactive account, rate limit, provider error), the index answers. |
- The response names the
sourcethat answered;nextCursorstays with that source. A provider cursor expires after an hour (400invalid_cursor). - A provider result the index does not hold (a message older than the sync window) has
id: nulland cannot be opened through the API. - Filters narrow provider results after the provider answers, so a provider page can hold fewer than
limitresults whilenextCursorcontinues. - Provider searches of a Gmail account count against a per-account limit per minute; past it the search answers 429
rate_limitedwithRetry-After. - With
source=provider, the provider's refusals surface as 400provider_rejected(it rejectedq), 400provider_unsupported, 403provider_forbidden, and the account-state 409s below.
Get a message
GET /v1/accounts/{accountId}/messages/{messageId} returns the summary fields plus bcc, replyTo, body (text and html, each null when the message has no such part) and attachments.
curl "https://api.workwae.com/v1/accounts/$ACCOUNT_ID/messages/$MESSAGE_ID" \
-H "Authorization: Bearer $WORKWAE_API_KEY"- With
bodyStoragecacheorfull, the body comes from stored content (cachefetches and stores the raw message on a miss), and attachment ids are MIME part numbers such as1.2. Withnone, the body is read from the provider. - Drafts are always read from the provider.
- Replies to your sends can carry an AI
classification; see AI.
Errors specific to reading a message (the same apply to the downloads below):
| Status | Reason |
|---|---|
| 404 | message_not_found (the message does not exist), provider_not_found (the provider no longer has it) |
| 409 | account_paused, account_disabled, account_reauth_required, account_misconfigured: the provider is needed and the account cannot reach it |
| 403 | provider_forbidden: the provider refused to serve the message |
| 429, 502 | provider_throttled, provider_unavailable, with Retry-After when the provider named a wait |
Download attachments and raw messages
Attachments are listed on the message with id, filename, contentType, size, isInline and contentId. Download one by id, or the whole message as RFC 5322 (message/rfc822). Both are served from stored content when the account stores bodies.
curl -o invoice.pdf \
"https://api.workwae.com/v1/accounts/$ACCOUNT_ID/messages/$MESSAGE_ID/attachments/$ATTACHMENT_ID" \
-H "Authorization: Bearer $WORKWAE_API_KEY"
curl -o message.eml \
"https://api.workwae.com/v1/accounts/$ACCOUNT_ID/messages/$MESSAGE_ID/raw" \
-H "Authorization: Bearer $WORKWAE_API_KEY"The SDK returns a File. An attachment id the stored message does not have is asked of the provider, which answers 404 provider_not_found when it has none either.
Threads
A thread groups a conversation's messages. Its id is the provider's thread id, the threadId on each message.
GET /v1/accounts/{accountId}/threadslists threads by latest activity, newest first (limitup to 100, default 25). WithmailboxId, it lists threads with a message in that mailbox, ordered by their latest message there; each summary still describes the whole thread.GET /v1/accounts/{accountId}/threads/{threadId}returns the thread with its messages, oldest first.
curl "https://api.workwae.com/v1/accounts/$ACCOUNT_ID/threads/$THREAD_ID" \
-H "Authorization: Bearer $WORKWAE_API_KEY"Thread summaries carry participants, messageCount, unreadCount, isFlagged, hasAttachments, hasDrafts, mailboxIds, latestMessageId and lastMessageAt. Both listings cover the sync window.
Mark read, flag and move
PATCH /v1/accounts/{accountId}/messages/{messageId} changes flags and mailboxes, and returns the updated summary.
curl -X PATCH "https://api.workwae.com/v1/accounts/$ACCOUNT_ID/messages/$MESSAGE_ID" \
-H "Authorization: Bearer $WORKWAE_API_KEY" \
-H "content-type: application/json" \
-d '{ "isRead": true, "isFlagged": true }'To move a message, name the target mailbox:
curl -X PATCH "https://api.workwae.com/v1/accounts/$ACCOUNT_ID/messages/$MESSAGE_ID" \
-H "Authorization: Bearer $WORKWAE_API_KEY" \
-H "content-type: application/json" \
-d '{ "mailboxId": "'"$ARCHIVE_ID"'" }'| Field | Effect |
|---|---|
isRead, isFlagged | Set or clear the flag. |
mailboxId | Moves the message there. On Gmail it replaces the message's folder-like labels. |
addMailboxIds, removeMailboxIds | Add or remove Gmail labels, up to 50 each. A mailbox that is not a label answers 400 mailbox_not_label. |
- A mailbox id the account does not have answers 422
unknown_mailbox, with the ids indata.ids. - Provider refusals answer 400
provider_rejectedorprovider_unsupported, 403provider_forbidden, 404provider_not_foundor 409provider_conflict.
PATCH /v1/accounts/{accountId}/threads/{threadId} takes isRead and mailboxId and applies them to every message of the thread, skipping those already in that state. A failure part way leaves the messages done so far changed; repeat the call to finish the rest.
Delete messages
DELETE /v1/accounts/{accountId}/messages/{messageId} moves the message to Trash. ?permanent=true deletes it outright.
curl -X DELETE "https://api.workwae.com/v1/accounts/$ACCOUNT_ID/messages/$MESSAGE_ID?permanent=true" \
-H "Authorization: Bearer $WORKWAE_API_KEY"DELETE /v1/accounts/{accountId}/threads/{threadId} moves every message of the thread that is not already there to Trash. Like the thread update, repeating it after a partial failure finishes the rest.
Manage folders and labels
POST /v1/accounts/{accountId}/mailboxes creates a folder (Microsoft, IMAP) or a label (Gmail) from a name and an optional parentId. A nested Gmail label is named with / between parent and child.
const projects = await workwae.mailboxes.create({ accountId, name: "Projects" });
await workwae.mailboxes.update({ accountId, mailboxId: projects.id, name: "Clients" });PATCHrenames or moves a mailbox;parentId: nullmoves it to the top level.- System and special-use mailboxes (Inbox, Sent, Drafts, Trash, Junk, Archive, Gmail system labels) cannot be renamed, moved or deleted: 409
mailbox_protected.
Deleting a mailbox depends on the provider:
| Provider | DELETE removes |
|---|---|
| Microsoft | The folder and the messages in it. A folder with child folders is refused (provider_rejected). |
| Gmail | The label, taken off its messages, which stay. |
| IMAP | The mailbox and its messages. A mailbox with child mailboxes is refused by the server (provider_rejected). |
The index drops the mailbox at once; the messages it held leave on the next sync, as for a deletion made elsewhere.
Follow changes
Every change is announced as an event, whether it was made in another mail client or through the API (an API change shows in the index at once and is announced once):
mailbox.created,mailbox.updated(renamed, moved or given another role) andmailbox.deleted.message.created: received, sent, drafted, or moved in from outside the sync window.message.updated: read, flagged, moved, or linked to the outbox entry it is a copy of;changesnames the fields.message.deleted: carries the message and its mailboxes as last indexed.
Payloads are in Mail events. Receive them with webhooks or poll the change feed.