Update flags, labels and keywords for every message a search selects
Applies one patch to every message POST /search/query would return for the same filter — the way to act on a whole result set without naming its messages. The filter grammar, and the set it selects, are that route's own.
SEND BACK THE excludeRoles YOU SEARCHED WITH. It narrows the selection here exactly as it narrows the results there, and it is not part of the filter — so a call that omits it patches the Trash and Junk mail the search left out.
THE CEILING. idMax is a message id (a ULID, so ordered by arrival) resolved on the first call and echoed back: mail that arrives while the gesture runs has a later id and is never touched. Send it back unchanged on every continuation.
IT MAY ANSWER A CURSOR, exactly as PATCH /labels/{label}/messages does: the call spends a bounded budget, and a non-null nextAfter means call again with after set to it, the same idMax, the same filter and the same patch. Unlike that route it reports no remaining — measure progress against the search's own total.
A FULL-TEXT filter is bounded: one that matches more than 5,000 messages is refused with too_large, having written nothing, rather than applied to some of them. Narrow the search. A filter is REQUIRED — this route will not patch a whole mailbox by omission.
bearerAuthAuthorizationBearer <token>Two credential kinds share this scheme; GET /api/v1/auth/whoami answers which one you hold and what it reaches.
oek_… API key (minted by POST /api/v1/api-keys; shown once at mint). role: account reaches the full account surface: the directory (domains, routes, patterns, api-keys, accounts, address lists) plus every mailbox the account owns. role: system additionally reaches the operator surface, which is documented separately.
oemp_… mailbox app password (minted by POST /api/v1/identities/{identityId}/credentials). Reaches ONE mailbox: its messages, labels, threads, search, sync, send, filters and PIM collections. Directory routes refuse it with 403 insufficient_scope.
Refusal vocabulary: a missing, malformed, expired, or revoked credential answers 401 unauthorized with a WWW-Authenticate: Bearer challenge; a VALID credential outside its scope answers 403 (insufficient_scope for an app password on a directory route, system_credentials_required where only the operator may call). The public routes (health, branding, token-in-path capabilities) declare security: [].
mailboxId*stringMailbox ULID identifier
application/json- body
after?stringContinuation cursor: pass the previous call's nextAfter.
1 <= lengthexcludeRoles?array<>System label roles whose mail this query must not return — e.g. ["trash", "junk"] for an All-Mail view. A message carrying ANY label with one of these roles is excluded, whatever else it carries. Says in one argument what a filter would need a label lookup and one bound value per label to say; a role no label answers to is simply not excluded.
filter?|An RFC 8621 §4.4 FilterCondition, or a boolean tree of them ({operator: AND|OR|NOT, conditions: [...]}). Bounded, and refused (never truncated) past the bound: the tree at most 32 levels deep and 512 nodes, and at most 64 bound values in total (an inMailboxOtherThan element costs one each).
flagsClear?array<>Flags to remove
flagsSet?array<>Flags to set
idMax?stringULID ceiling — the selection is every match with id <= this. Omitted on a FIRST call; REQUIRED once after is given.
1 <= lengthkeywordsClear?array<string>Custom keywords to remove
keywordsExact?array<string>Exact replacement list of custom keywords
keywordsSet?array<string>Custom keywords to add
labelsAdd?array<string>Labels to add
labelsExact?array<string>Exact replacement list of labels
labelsRemove?array<string>Labels to remove
limit?integerLower the per-call update budget. Never raises it.
1 <= valueorphans?stringWhat to do if this write would leave the message with no label. 'expunge' (the default) sends the message to Recently deleted. 'archive' keeps the message live by attaching the Archive system label (attaching Archive needs no right beyond the grant's 't' for a scoped viewer). If the write removes Archive itself and would leave no labels, Archive is kept without error. A SCOPED viewer whose write lands the message in Archive has moved it out of their own scope: the answer is still updated, and the hydrated message carries an EMPTY labels list rather than naming a label the grant never included. labelsAdded omits Archive for the same reason.
"expunge""archive"Counts for this call, and a cursor if the selection is not drained
application/json- response
archived*integerMessages whose last label this patch removed that fell back to the Archive label instead of being expunged. Unlike expunged, this is a SUBSET of updated, not an alternative to it: an archived message was still updated, so examined equals updated + unchanged + expunged + failed and adding archived to updated double counts. Only present when the patch carried orphans: "archive".
examined*integerMessages the query selected in this call
expunged*integerMessages whose last label this patch removed: moved to the Recently-deleted tier, restorable until purge. Counted here rather than in updated.
failed*integerPer-message refusals. Not an error: the rest of the call applied.
idMax*stringThe ULID ceiling this call worked under. Send it back UNCHANGED on a continuation: it is what keeps the selection the one the search showed, and re-resolving it would let mail that arrived since drift into the gesture.
nextAfter*|Continuation cursor: call again with after set to this, the same idMax, the same filter and the same patch. null means the selection is drained. There is no remaining count — counting what is left of a filter is an aggregate over the whole filtered set on every call. Measure progress against the search's own total.
unchanged*integerSelected messages the patch provably would not have changed, so they were skipped
updated*integerMessages the patch changed
curl -X PATCH 'https://api.open.email/api/v1/mailboxes/string/search/query' \
-H 'Content-Type: application/json' \
-d '{}'{
"archived": 0,
"examined": 0,
"expunged": 0,
"failed": 0,
"idMax": "string",
"nextAfter": "string",
"unchanged": 0,
"updated": 0
}Structured filter and query messages POST
Executes a structured query over mailbox messages with complex filters (combinators, header conditions, date ranges, flag states) and custom sort orders.
Highlighted excerpts for search results POST
Builds the same highlighted <mark> excerpts (subject + body preview) the search surfaces return inline under `snippet`, for up to 50 result ids the caller…