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 syncWindowDays days (90 by default; null keeps the full mailbox history). Set it in the tenant settings or per account in settings.
  • bodyStorage decides whether message content is stored: none (the default) reads bodies from the provider on request, cache stores a message's content the first time it is read, full stores every indexed message.
  • Changing syncWindowDays or bodyStorage restarts the account's syncs. full backfills every indexed message; none deletes stored content.
  • POST /v1/accounts/{accountId}/sync wakes 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"
FieldMeaning
roleinbox, sent, drafts, trash, junk, archive, all, important, or custom for the user's own.
kindfolder: a message is in exactly one. label: a message can carry several (Gmail).
pathThe full name, /-separated from the top level. parentId names the parent.
totalCount, unreadCountMessage 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"
FilterMatches
mailboxIdMessages in that mailbox. An id the account does not have answers 422 unknown_mailbox.
from, toPart of an address or display name, case-insensitive. to looks at To, Cc and Bcc.
unread, flagged, hasAttachmentstrue or false.
importancehigh, normal or low. Messages indexed before Workwae read priorities (importance: null) match none.
after, beforereceivedAt 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: outbound when the message is in a sent mailbox or its From is the account's address or one of its identities (drafts included); inbound otherwise.
  • outboxId and reference: 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's importance, and from the priority headers on Gmail and IMAP, where a message without one reads normal.

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:

sourceBehavior
indexFull-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.
providerPasses 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 source that answered; nextCursor stays with that source. A provider cursor expires after an hour (400 invalid_cursor).
  • A provider result the index does not hold (a message older than the sync window) has id: null and cannot be opened through the API.
  • Filters narrow provider results after the provider answers, so a provider page can hold fewer than limit results while nextCursor continues.
  • Provider searches of a Gmail account count against a per-account limit per minute; past it the search answers 429 rate_limited with Retry-After.
  • With source=provider, the provider's refusals surface as 400 provider_rejected (it rejected q), 400 provider_unsupported, 403 provider_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 bodyStorage cache or full, the body comes from stored content (cache fetches and stores the raw message on a miss), and attachment ids are MIME part numbers such as 1.2. With none, 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):

StatusReason
404message_not_found (the message does not exist), provider_not_found (the provider no longer has it)
409account_paused, account_disabled, account_reauth_required, account_misconfigured: the provider is needed and the account cannot reach it
403provider_forbidden: the provider refused to serve the message
429, 502provider_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}/threads lists threads by latest activity, newest first (limit up to 100, default 25). With mailboxId, 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"'" }'
FieldEffect
isRead, isFlaggedSet or clear the flag.
mailboxIdMoves the message there. On Gmail it replaces the message's folder-like labels.
addMailboxIds, removeMailboxIdsAdd 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 in data.ids.
  • Provider refusals answer 400 provider_rejected or provider_unsupported, 403 provider_forbidden, 404 provider_not_found or 409 provider_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" });
  • PATCH renames or moves a mailbox; parentId: null moves 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:

ProviderDELETE removes
MicrosoftThe folder and the messages in it. A folder with child folders is refused (provider_rejected).
GmailThe label, taken off its messages, which stay.
IMAPThe 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) and mailbox.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; changes names 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.

On this page