Mailboxes, messages, threads, search, attachments and sending. Everything is addressed under an account.
Guides: Read mail, Sending mail, Drafts, Mail merge.
| Method | Path | Operation | Scope |
|---|---|---|---|
GET | /v1/accounts/{accountId}/mailboxes | List mailboxes (folders or labels) | read |
POST | /v1/accounts/{accountId}/mailboxes | Create a mailbox (folder or label) | write |
PATCH | /v1/accounts/{accountId}/mailboxes/{mailboxId} | Rename or move a mailbox | write |
DELETE | /v1/accounts/{accountId}/mailboxes/{mailboxId} | Delete a mailbox | write |
GET | /v1/accounts/{accountId}/messages | List messages, newest first | read |
GET | /v1/accounts/{accountId}/messages/{messageId} | Get a message with its body | read |
PATCH | /v1/accounts/{accountId}/messages/{messageId} | Change flags, move, or change labels | write |
DELETE | /v1/accounts/{accountId}/messages/{messageId} | Delete a message | write |
GET | /v1/accounts/{accountId}/messages/{messageId}/raw | Download the message as RFC 5322 (message/rfc822) | read |
GET | /v1/accounts/{accountId}/messages/{messageId}/attachments/{attachmentId} | Download an attachment | read |
GET | /v1/accounts/{accountId}/search/messages | Search messages | read |
GET | /v1/accounts/{accountId}/threads | List threads, latest activity first | read |
GET | /v1/accounts/{accountId}/threads/{threadId} | Get a thread with its messages, oldest first | read |
PATCH | /v1/accounts/{accountId}/threads/{threadId} | Mark a thread read or unread, or move it | write |
DELETE | /v1/accounts/{accountId}/threads/{threadId} | Move a thread to Trash | write |
POST | /v1/accounts/{accountId}/drafts | Create a draft | write |
PUT | /v1/accounts/{accountId}/drafts/{messageId} | Replace a draft's content | write |
DELETE | /v1/accounts/{accountId}/drafts/{messageId} | Delete a draft | write |
POST | /v1/accounts/{accountId}/drafts/{messageId}/send | Send a draft | send |
GET | /v1/accounts/{accountId}/outbox | List queued and recent sends, newest first | read |
POST | /v1/accounts/{accountId}/outbox | Send a message | send |
GET | /v1/accounts/{accountId}/outbox/{outboxId} | Get a send | read |
PATCH | /v1/accounts/{accountId}/outbox/{outboxId} | Reschedule a queued send | send |
DELETE | /v1/accounts/{accountId}/outbox/{outboxId} | Cancel a queued send | send |
GET | /v1/accounts/{accountId}/outbox/{outboxId}/tracking | List a send's opens and clicks | read |
POST | /v1/accounts/{accountId}/mail-merges | Send one message per recipient (mail merge) | send |
Runs in your browser against api.workwae.com with the key you enter. Use a read-only key, limited to a test account if you can.
Required scope: read.
accountId*stringuuidOK
application/json- response
items*array<>curl -X GET 'https://api.workwae.com/v1/accounts/6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b/mailboxes'{ "items": [ { "id": "c4e6a8b0-1d3f-4a5c-8e7b-9d1f3a5c7e9b", "name": "string", "path": "string", "parentId": "1adf657e-5e25-4548-989a-ccbcf88b0d04", "role": "inbox", "kind": "folder", "totalCount": 0, "unreadCount": 0 } ]}Runs against the live API with the key you enter: this change really happens. Use a key limited to a test account.
Microsoft and IMAP create a folder; Gmail creates a label (nested with '/' in the label name when parentId is set). A folder that is not empty can be deleted later: Microsoft deletes the folder and the messages in it, Gmail removes the label and leaves the messages, IMAP deletes the mailbox and its messages and refuses a mailbox that still has child mailboxes (provider_rejected).
Required scope: write.
accountId*stringuuidapplication/json- body
name*string1 <= length <= 255parentId?stringuuidOK
application/json- response
id*stringuuidname*stringpath*stringparentId*|nullrole*MailboxRole"inbox""sent""drafts""trash""junk""archive""all""important""custom"kind*MailboxKind"folder""label"totalCount*integer|nullunreadCount*integer|nullcurl -X POST 'https://api.workwae.com/v1/accounts/6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b/mailboxes' \ -H 'Content-Type: application/json' \ -d '{ "name": "string"}'{ "id": "c4e6a8b0-1d3f-4a5c-8e7b-9d1f3a5c7e9b", "name": "string", "path": "string", "parentId": "1adf657e-5e25-4548-989a-ccbcf88b0d04", "role": "inbox", "kind": "folder", "totalCount": 0, "unreadCount": 0}Runs against the live API with the key you enter: this change really happens. Use a key limited to a test account.
Refused for system and special-use mailboxes (Inbox, Sent, Drafts, Trash, Junk, Archive, Gmail system labels) with mailbox_protected. parentId null moves it to the top level. On Gmail the label's name keeps '/' between a parent label and this one.
Required scope: write.
accountId*stringuuidmailboxId*stringuuidapplication/json- body
name?string1 <= length <= 255parentId?|nullOK
application/json- response
id*stringuuidname*stringpath*stringparentId*|nullrole*MailboxRole"inbox""sent""drafts""trash""junk""archive""all""important""custom"kind*MailboxKind"folder""label"totalCount*integer|nullunreadCount*integer|nullcurl -X PATCH 'https://api.workwae.com/v1/accounts/6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b/mailboxes/8a3f5c7e-2d4b-4c6e-9f1a-3b5d7f9e1c2a' \ -H 'Content-Type: application/json' \ -d '{}'{ "id": "c4e6a8b0-1d3f-4a5c-8e7b-9d1f3a5c7e9b", "name": "string", "path": "string", "parentId": "1adf657e-5e25-4548-989a-ccbcf88b0d04", "role": "inbox", "kind": "folder", "totalCount": 0, "unreadCount": 0}Runs against the live API with the key you enter: this change really happens. Use a key limited to a test account.
Refused for system and special-use mailboxes (mailbox_protected). Microsoft deletes the folder and the messages in it (child folders are refused, provider_rejected). Gmail deletes the label and takes it off its messages, which stay. IMAP deletes the mailbox and its messages; a mailbox that still has children is refused by the server (provider_rejected). The index drops the mailbox now; messages it held leave on the next sync, the same way a deletion made elsewhere does.
Required scope: write.
accountId*stringuuidmailboxId*stringuuidOK
application/json- response
curl -X DELETE 'https://api.workwae.com/v1/accounts/6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b/mailboxes/8a3f5c7e-2d4b-4c6e-9f1a-3b5d7f9e1c2a'nullRuns in your browser against api.workwae.com with the key you enter. Use a read-only key, limited to a test account if you can.
Covers the account's sync window. Without mailboxId, lists every mailbox. importance keeps the messages with that priority; messages indexed before priorities were read (importance null) match none. from and to match part of an address or display name, case-insensitive (to looks at To, Cc and Bcc); after and before bound receivedAt.
Required scope: read.
accountId*stringuuidmailboxId?stringuuidfrom?string1 <= length <= 320to?string1 <= length <= 320unread?boolean|flagged?boolean|hasAttachments?boolean|importance?string"high""normal""low"after?stringdate-timebefore?stringdate-timelimit?integer1 <= value <= 20050cursor?string1 <= length <= 512OK
application/json- response
items*array<>nextCursor*|nullcurl -X GET 'https://api.workwae.com/v1/accounts/6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b/messages'{ "items": [ { "id": "c4e6a8b0-1d3f-4a5c-8e7b-9d1f3a5c7e9b", "threadId": "18f2a3b4c5d6e7f8", "internetMessageId": "<CAF3x9a@mail.example.org>", "mailboxIds": [ "8a3f5c7e-2d4b-4c6e-9f1a-3b5d7f9e1c2a" ], "subject": "Quarterly report", "from": { "name": "Ada Lovelace", "address": "ada@example.com" }, "to": [ { "name": "Ada Lovelace", "address": "ada@example.com" } ], "cc": [ { "name": "Ada Lovelace", "address": "ada@example.com" } ], "snippet": "Hi Ada, the numbers for the quarter are attached.", "receivedAt": "2026-05-04T09:30:00.000Z", "sentAt": "2026-05-04T09:30:00.000Z", "isRead": true, "isFlagged": true, "isDraft": true, "hasAttachments": true, "categories": [ "string" ], "direction": "inbound", "outboxId": "6305f547-a117-46c0-811c-4395f19f91db", "reference": "string", "importance": "high" } ], "nextCursor": "eyJ0eG5pZCI6IjQyIiwiaWQiOjEwMDF9"}Runs in your browser against api.workwae.com with the key you enter. Use a read-only key, limited to a test account if you can.
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 ("1.2"); otherwise it is read from the provider. Drafts are always read from the provider.
Required scope: read.
accountId*stringuuidmessageId*stringuuidOK
application/json- response
id*stringuuidthreadId*|nullinternetMessageId*string|nullmailboxIds*array<>subject*string|nullfrom*|nullto*array<>cc*array<>snippet*string|nullreceivedAt*|nullsentAt*|nullisRead*booleanisFlagged*booleanisDraft*booleanhasAttachments*booleancategories*array<string>direction*MessageDirection"inbound""outbound"outboxId*|nullreference*string|nullimportance*|nullbcc*array<>replyTo*array<>body*attachments*array<>classification*|nullcurl -X GET 'https://api.workwae.com/v1/accounts/6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b/messages/c4e6a8b0-1d3f-4a5c-8e7b-9d1f3a5c7e9b'{ "id": "c4e6a8b0-1d3f-4a5c-8e7b-9d1f3a5c7e9b", "threadId": "18f2a3b4c5d6e7f8", "internetMessageId": "<CAF3x9a@mail.example.org>", "mailboxIds": [ "8a3f5c7e-2d4b-4c6e-9f1a-3b5d7f9e1c2a" ], "subject": "Quarterly report", "from": { "name": "Ada Lovelace", "address": "ada@example.com" }, "to": [ { "name": "Ada Lovelace", "address": "ada@example.com" } ], "cc": [ { "name": "Ada Lovelace", "address": "ada@example.com" } ], "snippet": "Hi Ada, the numbers for the quarter are attached.", "receivedAt": "2026-05-04T09:30:00.000Z", "sentAt": "2026-05-04T09:30:00.000Z", "isRead": true, "isFlagged": true, "isDraft": true, "hasAttachments": true, "categories": [ "string" ], "direction": "inbound", "outboxId": "6305f547-a117-46c0-811c-4395f19f91db", "reference": "string", "importance": "high", "bcc": [ { "name": "Ada Lovelace", "address": "ada@example.com" } ], "replyTo": [ { "name": "Ada Lovelace", "address": "ada@example.com" } ], "body": { "text": "string", "html": "string" }, "attachments": [ { "id": "string", "filename": "string", "contentType": "string", "size": 0, "isInline": true, "contentId": "string" } ], "classification": { "kind": "opt_out", "probabilities": { "property1": 0, "property2": 0 }, "optOutProbability": 0, "autoReplyCategory": "out_of_office", "returnDate": "2026-05-04", "alternateContacts": [ "string" ], "outboxId": "6305f547-a117-46c0-811c-4395f19f91db", "model": "string", "classifiedAt": "2026-05-04T09:30:00.000Z" }}Runs against the live API with the key you enter: this change really happens. Use a key limited to a test account.
mailboxId moves the message (Microsoft folders, or Gmail: replaces the message's folder-like labels). addMailboxIds/removeMailboxIds apply Gmail labels.
Required scope: write.
accountId*stringuuidmessageId*stringuuidapplication/json- body
isRead?booleanisFlagged?booleanmailboxId?stringuuidaddMailboxIds?array<>items <= 50removeMailboxIds?array<>items <= 50OK
application/json- response
id*stringuuidthreadId*|nullinternetMessageId*string|nullmailboxIds*array<>subject*string|nullfrom*|nullto*array<>cc*array<>snippet*string|nullreceivedAt*|nullsentAt*|nullisRead*booleanisFlagged*booleanisDraft*booleanhasAttachments*booleancategories*array<string>direction*MessageDirection"inbound""outbound"outboxId*|nullreference*string|nullimportance*|nullcurl -X PATCH 'https://api.workwae.com/v1/accounts/6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b/messages/c4e6a8b0-1d3f-4a5c-8e7b-9d1f3a5c7e9b' \ -H 'Content-Type: application/json' \ -d '{}'{ "id": "c4e6a8b0-1d3f-4a5c-8e7b-9d1f3a5c7e9b", "threadId": "18f2a3b4c5d6e7f8", "internetMessageId": "<CAF3x9a@mail.example.org>", "mailboxIds": [ "8a3f5c7e-2d4b-4c6e-9f1a-3b5d7f9e1c2a" ], "subject": "Quarterly report", "from": { "name": "Ada Lovelace", "address": "ada@example.com" }, "to": [ { "name": "Ada Lovelace", "address": "ada@example.com" } ], "cc": [ { "name": "Ada Lovelace", "address": "ada@example.com" } ], "snippet": "Hi Ada, the numbers for the quarter are attached.", "receivedAt": "2026-05-04T09:30:00.000Z", "sentAt": "2026-05-04T09:30:00.000Z", "isRead": true, "isFlagged": true, "isDraft": true, "hasAttachments": true, "categories": [ "string" ], "direction": "inbound", "outboxId": "6305f547-a117-46c0-811c-4395f19f91db", "reference": "string", "importance": "high"}Runs against the live API with the key you enter: this change really happens. Use a key limited to a test account.
Moves it to Trash; ?permanent=true deletes it outright.
Required scope: write.
accountId*stringuuidmessageId*stringuuidpermanent?|falseOK
application/json- response
curl -X DELETE 'https://api.workwae.com/v1/accounts/6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b/messages/c4e6a8b0-1d3f-4a5c-8e7b-9d1f3a5c7e9b'nullRuns in your browser against api.workwae.com with the key you enter. Use a read-only key, limited to a test account if you can.
Served from stored content when the account stores bodies.
Required scope: read.
accountId*stringuuidmessageId*stringuuidThe message as message/rfc822
response?stringmessage/rfc822curl -X GET 'https://api.workwae.com/v1/accounts/6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b/messages/c4e6a8b0-1d3f-4a5c-8e7b-9d1f3a5c7e9b/raw'"string"Runs in your browser against api.workwae.com with the key you enter. Use a read-only key, limited to a test account if you can.
Served from stored content when the account stores bodies; an id the stored message does not have is asked of the provider, which answers provider_not_found when it has no such attachment either.
Required scope: read.
accountId*stringuuidmessageId*stringuuidattachmentId*string1 <= length <= 1024The attachment, with the Content-Type it was sent with
response?string*/*curl -X GET 'https://api.workwae.com/v1/accounts/6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b/messages/c4e6a8b0-1d3f-4a5c-8e7b-9d1f3a5c7e9b/attachments/string'"string"Runs in your browser against api.workwae.com with the key you enter. Use a read-only key, limited to a test account if you can.
source=index searches the index with full-text search (q 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 (bodyStorage cache or full), best matches first. source=provider passes q to the provider unchanged (Gmail search syntax, Microsoft KQL, IMAP SEARCH TEXT), newest first; results the index holds carry their id, others id null. source=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. from, to, unread, flagged and hasAttachments narrow either source's results as on messages.list; a provider page they narrow can hold fewer than limit results while nextCursor goes on. The response names the source that answered; nextCursor stays with it. A provider search of a Gmail account counts against the account's Gmail searches per minute; past them it answers rate_limited with Retry-After.
Required scope: read.
accountId*stringuuidq*string1 <= length <= 500mailboxId?stringuuidfrom?string1 <= length <= 320to?string1 <= length <= 320unread?boolean|flagged?boolean|hasAttachments?boolean|after?stringdate-timebefore?stringdate-timesource?string"auto""auto""index""provider"limit?integer1 <= value <= 10025cursor?string1 <= length <= 512OK
application/json- response
items*array<>nextCursor*|nullsource*SearchResultSource"index""provider"curl -X GET 'https://api.workwae.com/v1/accounts/6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b/search/messages?q=invoice'{ "items": [ { "id": "c4e6a8b0-1d3f-4a5c-8e7b-9d1f3a5c7e9b", "threadId": "18f2a3b4c5d6e7f8", "internetMessageId": "<CAF3x9a@mail.example.org>", "mailboxIds": [ "8a3f5c7e-2d4b-4c6e-9f1a-3b5d7f9e1c2a" ], "subject": "Quarterly report", "from": { "name": "Ada Lovelace", "address": "ada@example.com" }, "to": [ { "name": "Ada Lovelace", "address": "ada@example.com" } ], "cc": [ { "name": "Ada Lovelace", "address": "ada@example.com" } ], "snippet": "Hi Ada, the numbers for the quarter are attached.", "receivedAt": "2026-05-04T09:30:00.000Z", "sentAt": "2026-05-04T09:30:00.000Z", "isRead": true, "isFlagged": true, "isDraft": true, "hasAttachments": true, "categories": [ "string" ], "direction": "inbound", "outboxId": "6305f547-a117-46c0-811c-4395f19f91db", "reference": "string", "importance": "high" } ], "nextCursor": "eyJ0eG5pZCI6IjQyIiwiaWQiOjEwMDF9", "source": "index"}Runs in your browser against api.workwae.com with the key you enter. Use a read-only key, limited to a test account if you can.
Covers the account's sync window. With mailboxId, lists threads with a message in that mailbox, ordered by their latest message there; each summary still describes the whole thread.
Required scope: read.
accountId*stringuuidmailboxId?stringuuidlimit?integer1 <= value <= 10025cursor?string1 <= length <= 512OK
application/json- response
items*array<>nextCursor*|nullcurl -X GET 'https://api.workwae.com/v1/accounts/6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b/threads'{ "items": [ { "id": "string", "subject": "Quarterly report", "snippet": "Hi Ada, the numbers for the quarter are attached.", "participants": [ { "name": "Ada Lovelace", "address": "ada@example.com" } ], "messageCount": 0, "unreadCount": 0, "isFlagged": true, "hasAttachments": true, "hasDrafts": true, "mailboxIds": [ "8a3f5c7e-2d4b-4c6e-9f1a-3b5d7f9e1c2a" ], "latestMessageId": "3e8ac85f-c412-433a-a738-8be7894e4050", "lastMessageAt": "2026-05-04T09:30:00.000Z" } ], "nextCursor": "eyJ0eG5pZCI6IjQyIiwiaWQiOjEwMDF9"}Runs in your browser against api.workwae.com with the key you enter. Use a read-only key, limited to a test account if you can.
Required scope: read.
accountId*stringuuidthreadId*string1 <= length <= 1024OK
application/json- response
id*string1 <= length <= 1024subject*string|nullsnippet*string|nullparticipants*array<>messageCount*integerunreadCount*integerisFlagged*booleanhasAttachments*booleanhasDrafts*booleanmailboxIds*array<>latestMessageId*stringuuidlastMessageAt*|nullmessages*array<>curl -X GET 'https://api.workwae.com/v1/accounts/6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b/threads/18f2a3b4c5d6e7f8'{ "id": "string", "subject": "Quarterly report", "snippet": "Hi Ada, the numbers for the quarter are attached.", "participants": [ { "name": "Ada Lovelace", "address": "ada@example.com" } ], "messageCount": 0, "unreadCount": 0, "isFlagged": true, "hasAttachments": true, "hasDrafts": true, "mailboxIds": [ "8a3f5c7e-2d4b-4c6e-9f1a-3b5d7f9e1c2a" ], "latestMessageId": "3e8ac85f-c412-433a-a738-8be7894e4050", "lastMessageAt": "2026-05-04T09:30:00.000Z", "messages": [ { "id": "c4e6a8b0-1d3f-4a5c-8e7b-9d1f3a5c7e9b", "threadId": "18f2a3b4c5d6e7f8", "internetMessageId": "<CAF3x9a@mail.example.org>", "mailboxIds": [ "8a3f5c7e-2d4b-4c6e-9f1a-3b5d7f9e1c2a" ], "subject": "Quarterly report", "from": { "name": "Ada Lovelace", "address": "ada@example.com" }, "to": [ { "name": "Ada Lovelace", "address": "ada@example.com" } ], "cc": [ { "name": "Ada Lovelace", "address": "ada@example.com" } ], "snippet": "Hi Ada, the numbers for the quarter are attached.", "receivedAt": "2026-05-04T09:30:00.000Z", "sentAt": "2026-05-04T09:30:00.000Z", "isRead": true, "isFlagged": true, "isDraft": true, "hasAttachments": true, "categories": [ "string" ], "direction": "inbound", "outboxId": "6305f547-a117-46c0-811c-4395f19f91db", "reference": "string", "importance": "high" } ]}Runs against the live API with the key you enter: this change really happens. Use a key limited to a test account.
Applies the change to every message of the thread through the message operations; messages already in that state are skipped. mailboxId moves each message there (Gmail: replaces its folder-like labels). A failure part way leaves the messages done so far changed; repeating the call finishes the rest.
Required scope: write.
accountId*stringuuidthreadId*string1 <= length <= 1024application/json- body
isRead?booleanmailboxId?stringuuidOK
application/json- response
id*string1 <= length <= 1024subject*string|nullsnippet*string|nullparticipants*array<>messageCount*integerunreadCount*integerisFlagged*booleanhasAttachments*booleanhasDrafts*booleanmailboxIds*array<>latestMessageId*stringuuidlastMessageAt*|nullmessages*array<>curl -X PATCH 'https://api.workwae.com/v1/accounts/6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b/threads/18f2a3b4c5d6e7f8' \ -H 'Content-Type: application/json' \ -d '{}'{ "id": "string", "subject": "Quarterly report", "snippet": "Hi Ada, the numbers for the quarter are attached.", "participants": [ { "name": "Ada Lovelace", "address": "ada@example.com" } ], "messageCount": 0, "unreadCount": 0, "isFlagged": true, "hasAttachments": true, "hasDrafts": true, "mailboxIds": [ "8a3f5c7e-2d4b-4c6e-9f1a-3b5d7f9e1c2a" ], "latestMessageId": "3e8ac85f-c412-433a-a738-8be7894e4050", "lastMessageAt": "2026-05-04T09:30:00.000Z", "messages": [ { "id": "c4e6a8b0-1d3f-4a5c-8e7b-9d1f3a5c7e9b", "threadId": "18f2a3b4c5d6e7f8", "internetMessageId": "<CAF3x9a@mail.example.org>", "mailboxIds": [ "8a3f5c7e-2d4b-4c6e-9f1a-3b5d7f9e1c2a" ], "subject": "Quarterly report", "from": { "name": "Ada Lovelace", "address": "ada@example.com" }, "to": [ { "name": "Ada Lovelace", "address": "ada@example.com" } ], "cc": [ { "name": "Ada Lovelace", "address": "ada@example.com" } ], "snippet": "Hi Ada, the numbers for the quarter are attached.", "receivedAt": "2026-05-04T09:30:00.000Z", "sentAt": "2026-05-04T09:30:00.000Z", "isRead": true, "isFlagged": true, "isDraft": true, "hasAttachments": true, "categories": [ "string" ], "direction": "inbound", "outboxId": "6305f547-a117-46c0-811c-4395f19f91db", "reference": "string", "importance": "high" } ]}Runs against the live API with the key you enter: this change really happens. Use a key limited to a test account.
Moves every message of the thread that is not already there to Trash. A failure part way leaves the messages done so far changed; repeating the call finishes the rest.
Required scope: write.
accountId*stringuuidthreadId*string1 <= length <= 1024OK
application/json- response
curl -X DELETE 'https://api.workwae.com/v1/accounts/6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b/threads/18f2a3b4c5d6e7f8'nullRuns against the live API with the key you enter: this change really happens. Use a key limited to a test account.
Content is rendered when the request is taken, so later template edits never change it.
Required scope: write.
accountId*stringuuidapplication/json- body
to?array<>items <= 500[]cc?array<>items <= 500[]bcc?array<>items <= 500[]replyTo?array<>items <= 10[]subject?stringlength <= 998text?stringlength <= 2097152html?stringlength <= 2097152attachments?array<>items <= 50[]inReplyToMessageId?stringuuidfrom?signature?booleanpriority?MessagePriority"normal""high""normal""low"template?stringuuidrender?previewText?stringlength <= 2000format?TemplateFormat"html""markdown"OK
application/json- response
id*stringuuidthreadId*|nullinternetMessageId*string|nullmailboxIds*array<>subject*string|nullfrom*|nullto*array<>cc*array<>snippet*string|nullreceivedAt*|nullsentAt*|nullisRead*booleanisFlagged*booleanisDraft*booleanhasAttachments*booleancategories*array<string>direction*MessageDirection"inbound""outbound"outboxId*|nullreference*string|nullimportance*|nullcurl -X POST 'https://api.workwae.com/v1/accounts/6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b/drafts' \ -H 'Content-Type: application/json' \ -d '{}'{ "id": "c4e6a8b0-1d3f-4a5c-8e7b-9d1f3a5c7e9b", "threadId": "18f2a3b4c5d6e7f8", "internetMessageId": "<CAF3x9a@mail.example.org>", "mailboxIds": [ "8a3f5c7e-2d4b-4c6e-9f1a-3b5d7f9e1c2a" ], "subject": "Quarterly report", "from": { "name": "Ada Lovelace", "address": "ada@example.com" }, "to": [ { "name": "Ada Lovelace", "address": "ada@example.com" } ], "cc": [ { "name": "Ada Lovelace", "address": "ada@example.com" } ], "snippet": "Hi Ada, the numbers for the quarter are attached.", "receivedAt": "2026-05-04T09:30:00.000Z", "sentAt": "2026-05-04T09:30:00.000Z", "isRead": true, "isFlagged": true, "isDraft": true, "hasAttachments": true, "categories": [ "string" ], "direction": "inbound", "outboxId": "6305f547-a117-46c0-811c-4395f19f91db", "reference": "string", "importance": "high"}Runs against the live API with the key you enter: this change really happens. Use a key limited to a test account.
Required scope: write.
accountId*stringuuidmessageId*stringuuidOK
application/json- response
curl -X DELETE 'https://api.workwae.com/v1/accounts/6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b/drafts/c4e6a8b0-1d3f-4a5c-8e7b-9d1f3a5c7e9b'nullRuns against the live API with the key you enter: this change really happens. Use a key limited to a test account.
Content is rendered when the request is taken, so later template edits never change it.
Required scope: write.
accountId*stringuuidmessageId*stringuuidapplication/json- body
to?array<>items <= 500[]cc?array<>items <= 500[]bcc?array<>items <= 500[]replyTo?array<>items <= 10[]subject?stringlength <= 998text?stringlength <= 2097152html?stringlength <= 2097152attachments?array<>items <= 50[]inReplyToMessageId?stringuuidfrom?signature?booleanpriority?MessagePriority"normal""high""normal""low"template?stringuuidrender?previewText?stringlength <= 2000format?TemplateFormat"html""markdown"OK
application/json- response
id*stringuuidthreadId*|nullinternetMessageId*string|nullmailboxIds*array<>subject*string|nullfrom*|nullto*array<>cc*array<>snippet*string|nullreceivedAt*|nullsentAt*|nullisRead*booleanisFlagged*booleanisDraft*booleanhasAttachments*booleancategories*array<string>direction*MessageDirection"inbound""outbound"outboxId*|nullreference*string|nullimportance*|nullcurl -X PUT 'https://api.workwae.com/v1/accounts/6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b/drafts/c4e6a8b0-1d3f-4a5c-8e7b-9d1f3a5c7e9b' \ -H 'Content-Type: application/json' \ -d '{}'{ "id": "c4e6a8b0-1d3f-4a5c-8e7b-9d1f3a5c7e9b", "threadId": "18f2a3b4c5d6e7f8", "internetMessageId": "<CAF3x9a@mail.example.org>", "mailboxIds": [ "8a3f5c7e-2d4b-4c6e-9f1a-3b5d7f9e1c2a" ], "subject": "Quarterly report", "from": { "name": "Ada Lovelace", "address": "ada@example.com" }, "to": [ { "name": "Ada Lovelace", "address": "ada@example.com" } ], "cc": [ { "name": "Ada Lovelace", "address": "ada@example.com" } ], "snippet": "Hi Ada, the numbers for the quarter are attached.", "receivedAt": "2026-05-04T09:30:00.000Z", "sentAt": "2026-05-04T09:30:00.000Z", "isRead": true, "isFlagged": true, "isDraft": true, "hasAttachments": true, "categories": [ "string" ], "direction": "inbound", "outboxId": "6305f547-a117-46c0-811c-4395f19f91db", "reference": "string", "importance": "high"}Runs against the live API with the key you enter: this change really happens. Use a key limited to a test account.
A message without a sendAt later than the account's undo window (undoSendSeconds) from now waits out the window; undoUntil says until when cancelling the entry (DELETE .../outbox/{outboxId}) undoes the send. With tracking, the HTML part gets a 1x1 pixel (opens) and its http(s) links point at Workwae's redirect (clicks), reported as message.opened and message.clicked and counted on the outbox entry; text parts are left as they are. With clicks, tracking.honeypot (else the account's trackingHoneypot) also adds one hidden link only machines follow; it does nothing without clicks, so asking for it with clicks off is refused rather than ignored. Tracking is per message, not per recipient: every recipient gets the same MIME. A Gmail or Microsoft 365 draft is sent by the provider as it is stored, so it is never tracked (the account's defaults do not apply); an IMAP draft is. A cancelled draft-send leaves the draft in place.
Required scope: send.
accountId*stringuuidmessageId*stringuuidapplication/json- body
sendAt?stringdate-timereference?stringlength <= 255tracking?OK
application/json- response
id*stringuuidaccountId*stringuuidstatus*OutboxStatus"queued""sending""sent""failed""cancelled"reference*string|nullsendAt*stringdate-timeattempt*integerlastError*string|nullproviderMessageId*|nullinternetMessageId*string|nullsentAt*|nullcreatedAt*stringdate-timetrackOpens*booleantrackClicks*booleantrackHoneypot*booleanhumanOpenCount*integermachineOpenCount*integerhumanClickCount*integermachineClickCount*integerfirstOpenedAt*|nulllastOpenedAt*|nullundoUntil*|nullcurl -X POST 'https://api.workwae.com/v1/accounts/6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b/drafts/c4e6a8b0-1d3f-4a5c-8e7b-9d1f3a5c7e9b/send' \ -H 'Content-Type: application/json' \ -d '{}'{ "id": "c4e6a8b0-1d3f-4a5c-8e7b-9d1f3a5c7e9b", "accountId": "6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b", "status": "queued", "reference": "string", "sendAt": "2026-05-04T09:30:00.000Z", "attempt": 0, "lastError": "string", "providerMessageId": "string", "internetMessageId": "<CAF3x9a@mail.example.org>", "sentAt": "2026-05-04T09:30:00.000Z", "createdAt": "2026-05-04T09:30:00.000Z", "trackOpens": true, "trackClicks": true, "trackHoneypot": true, "humanOpenCount": 0, "machineOpenCount": 0, "humanClickCount": 0, "machineClickCount": 0, "firstOpenedAt": "2026-05-04T09:30:00.000Z", "lastOpenedAt": "2026-05-04T09:30:00.000Z", "undoUntil": "2026-05-04T09:30:00.000Z"}Runs in your browser against api.workwae.com with the key you enter. Use a read-only key, limited to a test account if you can.
status keeps the entries in that status.
Required scope: read.
accountId*stringuuidstatus?string"queued""sending""sent""failed""cancelled"limit?integer1 <= value <= 20050cursor?string1 <= length <= 512OK
application/json- response
items*array<>nextCursor*|nullcurl -X GET 'https://api.workwae.com/v1/accounts/6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b/outbox'{ "items": [ { "id": "c4e6a8b0-1d3f-4a5c-8e7b-9d1f3a5c7e9b", "accountId": "6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b", "status": "queued", "reference": "string", "sendAt": "2026-05-04T09:30:00.000Z", "attempt": 0, "lastError": "string", "providerMessageId": "string", "internetMessageId": "<CAF3x9a@mail.example.org>", "sentAt": "2026-05-04T09:30:00.000Z", "createdAt": "2026-05-04T09:30:00.000Z", "trackOpens": true, "trackClicks": true, "trackHoneypot": true, "humanOpenCount": 0, "machineOpenCount": 0, "humanClickCount": 0, "machineClickCount": 0, "firstOpenedAt": "2026-05-04T09:30:00.000Z", "lastOpenedAt": "2026-05-04T09:30:00.000Z" } ], "nextCursor": "eyJ0eG5pZCI6IjQyIiwiaWQiOjEwMDF9"}Runs against the live API with the key you enter: this change really happens. Use a key limited to a test account.
Queues the message as an outbox entry; delivery is reported with send.sent, send.retrying and send.failed events carrying its id as outboxId, and a later bounce with message.bounced. Delivery is at-least-once, except outcome_unknown (below), and without order across retries: an account's entries are first tried in queue order, but one backing off after a failed attempt does not hold up the later ones. A retry first looks for a copy that already went out. When no SMTP reply comes after the final dot was handed to the operating system (the connection dropped or timed out, or the attempt ran out of time), the server may have accepted the message, so it is not sent again: send.failed carries code outcome_unknown and the client app decides whether to resend. A message without a sendAt later than the account's undo window (undoSendSeconds) from now waits out the window; undoUntil says until when cancelling the entry (DELETE .../outbox/{outboxId}) undoes the send. With tracking, the HTML part gets a 1x1 pixel (opens) and its http(s) links point at Workwae's redirect (clicks), reported as message.opened and message.clicked and counted on the outbox entry; text parts are left as they are. With clicks, tracking.honeypot (else the account's trackingHoneypot) also adds one hidden link only machines follow; it does nothing without clicks, so asking for it with clicks off is refused rather than ignored. Tracking is per message, not per recipient: every recipient gets the same MIME. With listId, content is always rendered, {{rcpt.unsubscribeUrl}}, {{rcpt.address}} and {{rcpt.name}} are the recipient's, and the message carries List-Unsubscribe with that same URL and List-Unsubscribe-Post: List-Unsubscribe=One-Click (RFC 8058). Click tracking never rewrites a List-Unsubscribe URL, so the body's link stays as written. A recipient on that list or on the global list is not sent to, at queue time and again at delivery (send.failed with code suppressed). Without listId no list is consulted. Whether the provider's DKIM signature covers the List-Unsubscribe headers is up to its signing. For one message per recipient, see POST .../mail-merges. Content is rendered when the request is taken, so later template edits never change it. An unknown field is refused.
Required scope: send.
accountId*stringuuidapplication/json- body
to?array<>items <= 500[]cc?array<>items <= 500[]bcc?array<>items <= 500[]replyTo?array<>items <= 10[]subject?stringlength <= 998text?stringlength <= 2097152html?stringlength <= 2097152attachments?array<>items <= 50[]inReplyToMessageId?stringuuidfrom?signature?booleanpriority?MessagePriority"normal""high""normal""low"template?stringuuidrender?previewText?stringlength <= 2000format?TemplateFormat"html""markdown"sendAt?stringdate-timereference?stringlength <= 255tracking?listId?string^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$OK
application/json- response
id*stringuuidaccountId*stringuuidstatus*OutboxStatus"queued""sending""sent""failed""cancelled"reference*string|nullsendAt*stringdate-timeattempt*integerlastError*string|nullproviderMessageId*|nullinternetMessageId*string|nullsentAt*|nullcreatedAt*stringdate-timetrackOpens*booleantrackClicks*booleantrackHoneypot*booleanhumanOpenCount*integermachineOpenCount*integerhumanClickCount*integermachineClickCount*integerfirstOpenedAt*|nulllastOpenedAt*|nullundoUntil*|nullcurl -X POST 'https://api.workwae.com/v1/accounts/6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b/outbox' \ -H 'Content-Type: application/json' \ -d '{}'{ "id": "c4e6a8b0-1d3f-4a5c-8e7b-9d1f3a5c7e9b", "accountId": "6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b", "status": "queued", "reference": "string", "sendAt": "2026-05-04T09:30:00.000Z", "attempt": 0, "lastError": "string", "providerMessageId": "string", "internetMessageId": "<CAF3x9a@mail.example.org>", "sentAt": "2026-05-04T09:30:00.000Z", "createdAt": "2026-05-04T09:30:00.000Z", "trackOpens": true, "trackClicks": true, "trackHoneypot": true, "humanOpenCount": 0, "machineOpenCount": 0, "humanClickCount": 0, "machineClickCount": 0, "firstOpenedAt": "2026-05-04T09:30:00.000Z", "lastOpenedAt": "2026-05-04T09:30:00.000Z", "undoUntil": "2026-05-04T09:30:00.000Z"}Runs in your browser against api.workwae.com with the key you enter. Use a read-only key, limited to a test account if you can.
One entry with its tracking counts, for as long as the outbox keeps it: a sent message's outboxId names it.
Required scope: read.
accountId*stringuuidoutboxId*stringuuidOK
application/json- response
id*stringuuidaccountId*stringuuidstatus*OutboxStatus"queued""sending""sent""failed""cancelled"reference*string|nullsendAt*stringdate-timeattempt*integerlastError*string|nullproviderMessageId*|nullinternetMessageId*string|nullsentAt*|nullcreatedAt*stringdate-timetrackOpens*booleantrackClicks*booleantrackHoneypot*booleanhumanOpenCount*integermachineOpenCount*integerhumanClickCount*integermachineClickCount*integerfirstOpenedAt*|nulllastOpenedAt*|nullcurl -X GET 'https://api.workwae.com/v1/accounts/6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b/outbox/6305f547-a117-46c0-811c-4395f19f91db'{ "id": "c4e6a8b0-1d3f-4a5c-8e7b-9d1f3a5c7e9b", "accountId": "6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b", "status": "queued", "reference": "string", "sendAt": "2026-05-04T09:30:00.000Z", "attempt": 0, "lastError": "string", "providerMessageId": "string", "internetMessageId": "<CAF3x9a@mail.example.org>", "sentAt": "2026-05-04T09:30:00.000Z", "createdAt": "2026-05-04T09:30:00.000Z", "trackOpens": true, "trackClicks": true, "trackHoneypot": true, "humanOpenCount": 0, "machineOpenCount": 0, "humanClickCount": 0, "machineClickCount": 0, "firstOpenedAt": "2026-05-04T09:30:00.000Z", "lastOpenedAt": "2026-05-04T09:30:00.000Z"}Runs against the live API with the key you enter: this change really happens. Use a key limited to a test account.
Moves sendAt, earlier or later; a past sendAt sends as soon as possible. Announced as send.rescheduled.
Required scope: send.
accountId*stringuuidoutboxId*stringuuidapplication/json- body
sendAt*stringdate-timeOK
application/json- response
id*stringuuidaccountId*stringuuidstatus*OutboxStatus"queued""sending""sent""failed""cancelled"reference*string|nullsendAt*stringdate-timeattempt*integerlastError*string|nullproviderMessageId*|nullinternetMessageId*string|nullsentAt*|nullcreatedAt*stringdate-timetrackOpens*booleantrackClicks*booleantrackHoneypot*booleanhumanOpenCount*integermachineOpenCount*integerhumanClickCount*integermachineClickCount*integerfirstOpenedAt*|nulllastOpenedAt*|nullcurl -X PATCH 'https://api.workwae.com/v1/accounts/6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b/outbox/6305f547-a117-46c0-811c-4395f19f91db' \ -H 'Content-Type: application/json' \ -d '{ "sendAt": "2026-05-04T09:30:00.000Z"}'{ "id": "c4e6a8b0-1d3f-4a5c-8e7b-9d1f3a5c7e9b", "accountId": "6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b", "status": "queued", "reference": "string", "sendAt": "2026-05-04T09:30:00.000Z", "attempt": 0, "lastError": "string", "providerMessageId": "string", "internetMessageId": "<CAF3x9a@mail.example.org>", "sentAt": "2026-05-04T09:30:00.000Z", "createdAt": "2026-05-04T09:30:00.000Z", "trackOpens": true, "trackClicks": true, "trackHoneypot": true, "humanOpenCount": 0, "machineOpenCount": 0, "humanClickCount": 0, "machineClickCount": 0, "firstOpenedAt": "2026-05-04T09:30:00.000Z", "lastOpenedAt": "2026-05-04T09:30:00.000Z"}Runs against the live API with the key you enter: this change really happens. Use a key limited to a test account.
Undoes a send while it waits for its sendAt.
Required scope: send.
accountId*stringuuidoutboxId*stringuuidOK
application/json- response
curl -X DELETE 'https://api.workwae.com/v1/accounts/6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b/outbox/6305f547-a117-46c0-811c-4395f19f91db'nullRuns in your browser against api.workwae.com with the key you enter. Use a read-only key, limited to a test account if you can.
Newest first. At most 1000 are kept per send (the counts on the entry keep going), each for the tracking retention period. Each event carries what its request showed: the method (GET or HEAD), the Referer and Origin hosts, the mail client (Gmail's image proxy, web or app; Outlook on the web, desktop or mobile; Apple Mail and its Mail Privacy Protection; Yahoo, AOL, Thunderbird, Proton, Fastmail, Zoho, iCloud, a self-hosted webmail; else unknown), the device, OS and browser families, Cloudflare's visitor location (country, region, city, timezone, latitude and longitude, behind a trusted proxy only), the seconds after the send and since the send's previous hit, its sequence number, whether it was conditional, a Range request or a prefetch, the sec-fetch tuple, and the request headers (lower-cased, values cut at 1024 characters, at most 64, never Cookie, Authorization or Proxy-Authorization, forwarding and cf-* headers only from the trusted proxy; the full Referer is only here). Full addresses and headers stay with the tenant for the retention period.
Required scope: read.
accountId*stringuuidoutboxId*stringuuidlimit?integer1 <= value <= 20050cursor?string1 <= length <= 512OK
application/json- response
items*array<>nextCursor*|nullcurl -X GET 'https://api.workwae.com/v1/accounts/6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b/outbox/6305f547-a117-46c0-811c-4395f19f91db/tracking'{ "items": [ { "id": "c4e6a8b0-1d3f-4a5c-8e7b-9d1f3a5c7e9b", "kind": "open", "linkIndex": 0, "url": "string", "occurredAt": "2026-05-04T09:30:00.000Z", "userAgent": "string", "ip": "string", "actor": "human", "reasons": [ "string" ], "isMachine": true, "method": "string", "refererHost": "string", "originHost": "string", "client": "gmail_proxy", "device": "desktop", "os": "windows", "browser": "chrome", "country": "string", "region": "string", "city": "string", "timezone": "string", "latitude": 0, "longitude": 0, "secondsAfterSend": 0, "secondsSincePrevious": 0, "sequence": 0, "isConditional": true, "isRange": true, "isPrefetch": true, "secFetch": { "dest": "string", "mode": "string", "site": "string", "user": true }, "headers": { "property1": "string", "property2": "string" } } ], "nextCursor": "eyJ0eG5pZCI6IjQyIiwiaWQiOjEwMDF9"}Runs against the live API with the key you enter: this change really happens. Use a key limited to a test account.
Each of at most 100 entries becomes its own outbox entry and message, addressed to its to alone, with its own Message-ID, events, bounce matching and tracking; the answer has one result per entry, in order. Content is rendered per entry with its params (a template always; inline content only for entries that carry params, or with listId). An entry that cannot be queued fails alone, its result naming the reason (invalid_recipient, invalid_message_id, duplicate_message_id, template_render_failed, plan_limit_sends once the day's sends run out, suppressed when listId is given and the address is on that list or the global list, internal_error), while the others go ahead; the call itself succeeds. The body is the send body without to, cc, bcc and render: an unknown field is refused. Delivery is at-least-once, except outcome_unknown (below), and without order across retries: an account's entries are first tried in queue order, but one backing off after a failed attempt does not hold up the later ones. A retry first looks for a copy that already went out. When no SMTP reply comes after the final dot was handed to the operating system (the connection dropped or timed out, or the attempt ran out of time), the server may have accepted the message, so it is not sent again: send.failed carries code outcome_unknown and the client app decides whether to resend. A message without a sendAt later than the account's undo window (undoSendSeconds) from now waits out the window; undoUntil says until when cancelling the entry (DELETE .../outbox/{outboxId}) undoes the send. With tracking, the HTML part gets a 1x1 pixel (opens) and its http(s) links point at Workwae's redirect (clicks), reported as message.opened and message.clicked and counted on the outbox entry; text parts are left as they are. With clicks, tracking.honeypot (else the account's trackingHoneypot) also adds one hidden link only machines follow; it does nothing without clicks, so asking for it with clicks off is refused rather than ignored. Tracking is per message, not per recipient: every recipient gets the same MIME. With listId, content is always rendered, {{rcpt.unsubscribeUrl}}, {{rcpt.address}} and {{rcpt.name}} are the recipient's, and the message carries List-Unsubscribe with that same URL and List-Unsubscribe-Post: List-Unsubscribe=One-Click (RFC 8058). Click tracking never rewrites a List-Unsubscribe URL, so the body's link stays as written. A recipient on that list or on the global list is not sent to, at queue time and again at delivery (send.failed with code suppressed). Without listId no list is consulted. Whether the provider's DKIM signature covers the List-Unsubscribe headers is up to its signing. What holds for the whole call is checked once and refuses it. Content is rendered when the request is taken, so later template edits never change it. Answers 200: the merge itself is not kept, its entries are in the outbox.
Required scope: send.
accountId*stringuuidapplication/json- body
replyTo?array<>items <= 10[]subject?stringlength <= 998text?stringlength <= 2097152html?stringlength <= 2097152attachments?array<>items <= 50[]inReplyToMessageId?stringuuidfrom?signature?booleanpriority?MessagePriority"normal""high""normal""low"template?stringuuidpreviewText?stringlength <= 2000format?TemplateFormat"html""markdown"sendAt?stringdate-timereference?stringlength <= 255tracking?listId?string^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$entries*array<>1 <= items <= 100OK
application/json- response
items*array<|>curl -X POST 'https://api.workwae.com/v1/accounts/6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b/mail-merges' \ -H 'Content-Type: application/json' \ -d '{ "entries": [ { "to": { "address": "ada@example.com" } } ]}'{ "items": [ { "success": true, "to": { "name": "Ada Lovelace", "address": "ada@example.com" }, "outboxId": "6305f547-a117-46c0-811c-4395f19f91db", "messageId": "string", "sendAt": "2026-05-04T09:30:00.000Z", "undoUntil": "2026-05-04T09:30:00.000Z" } ]}