Search mail
GET /v1/search searches received and sent mail together: subjects, body text, addresses and, on received mail, the sender’s name, newest first. Only the first 100,000 characters of each message are searched. Each result says whether it was received or sent and carries a snippet of text around the match.
A search never leaves your workspace. A key tied to one agent searches only that agent’s mail, a test key searches only test mail, and a live key only live mail.
How words match
- Every word in
qmust appear somewhere in the message. - A word of three or more characters matches the start of a word:
invfinds “invoice”, andacmefindsbilling@acme.com. - Shorter words must match a whole word, so
indoes not find every word that starts with it. - A word never matches inside another word:
voicedoes not find “invoice”.
Examples
import { createClient } from "@mailsai/sdk";
const client = createClient(); // reads MAILS_API_KEY
// Invoices received since September with a file attached
const invoices = await client.search({
q: "invoice",
after: "2026-09-01",
has_attachment: true,
type: "received",
});
// "refund" in mail from acme.com, in every folder
const refunds = await client.search({
q: "refund",
from: "acme.com",
folder: "all",
});
console.log(invoices.data.length, refunds.data.length);from mailsai import create_client
client = create_client() # reads MAILS_API_KEY
# Invoices received since September with a file attached
invoices = client.search(
"invoice",
after="2026-09-01",
has_attachment=True,
type="received",
)
# "refund" in mail from acme.com, in every folder
refunds = client.search("refund", from_="acme.com", folder="all")
print(len(invoices["data"]), len(refunds["data"]))# Invoices received since September with a file attached
curl -G "https://api.mails.ai/v1/search" \
-H "Authorization: Bearer $MAILS_API_KEY" \
--data-urlencode "q=invoice" \
--data-urlencode "after=2026-09-01" \
--data-urlencode "has_attachment=true" \
--data-urlencode "type=received"
# "refund" in mail from acme.com, in every folder
curl -G "https://api.mails.ai/v1/search" \
-H "Authorization: Bearer $MAILS_API_KEY" \
--data-urlencode "q=refund" \
--data-urlencode "from=acme.com" \
--data-urlencode "folder=all"# Invoices received since September with a file attached
mails search invoice --after 2026-09-01 --has-attachment \
--type received
# "refund" in mail from acme.com, in every folder
mails search refund --from acme.com --folder allTo read a whole message, pass a received hit’s id to GET /v1/messages/received/{id} and a sent hit’s id to GET /v1/messages/{id}.
Filters
Every filter is optional and they combine: a result must match all of them.
| Parameter | Description |
|---|---|
q | string Full-text search: every word must appear in the subject, body or addresses, as a whole word or, from three letters up, the start of one (inv finds invoice). At most 16 different words (more is a 400). |
agent_id | string Filter by agent id or name. A key tied to one agent always reads that agent's mail, and naming another agent is a 403. |
from | string The sender's address contains this text (the display name, which the sender chooses, is not matched). |
to | string A recipient address contains this text. |
after | string Only mail at or after this time (ISO 8601, or YYYY-MM-DD). |
before | string Only mail before this time (ISO 8601, or YYYY-MM-DD). |
has_attachment | boolean true for mail with a file attachment, false for mail without. |
folder | string Which folder's mail to return, or all for every folder. Without it: every folder except trash. One of: inbox, archive, spam, trash, sent, all. |
label | string Only mail in threads carrying this label (a name, or a label id lbl_…). |
type | string Only received or only sent mail. One of: received, sent. |
limit | integer Page size (1–100). Default: 25. |
cursor | string Opaque cursor from a previous page's next_cursor. |
Results
The response is a page of hits, { "data": [...], "has_more": …, "next_cursor": … }. Pass next_cursor back as cursor for the next page. Each hit:
| Field | Description |
|---|---|
type | string Required. received for a message an agent received, sent for one it sent; it says which kind of id id is. One of: received, sent. |
id | string Required. The message id (rcv_… or msg_…). |
thread_id | string | null Required. The thread (thrd_…) the message is in. Null for a message in no thread. |
agent_id | string Required. Id (agt_…) of the agent that received or sent the message. |
subject | string | null Required. The message's subject. Null when it has none. |
from | object Required. Who the message is from: the sender for received mail, the agent itself for sent mail. |
from.address | string Required. The sender's address for received mail, as on the received message's from.address; the sending agent's address for sent mail. |
from.name | string | null Required. The sender's display name on received mail, null when there is none. Always null on sent mail. |
to | string[] Required. For received mail, the one agent address it was delivered to. For sent mail, its to recipients; cc and bcc are not listed. |
date | string Required. Received time, or sent time for sent mail. |
has_attachment | boolean Required. For received mail, true when it carries at least one file attachment (images embedded in the HTML do not count); for sent mail, true when it was sent with any. |
folder | string | null Required. The folder of the message's thread. A message in no thread counts as inbox; null only when its thread cannot be found. One of: inbox, archive, spam, trash, sent. |
labels | string[] Required. The label names on the message's thread. Empty when the thread has none or the message is in no thread. |
snippet | string Required. Plain text from the message's body around the match (the start of the body when the match is only in the subject or an address, or when q is absent). |
Limits
- 120 searches a minute per workspace, shared by every key and the dashboard. Past that, the API answers 429
rate_limit_exceededwith aRetry-Afterheader. - At most 16 different words in
q, and at most 200 characters. More words is a 400 rather than a quietly shortened search. - A search that runs too long is a 400 that asks for another word or a filter to narrow it.
Next steps
Every parameter is also in the API reference.
Was this page helpful?