Get all messages with a specific tag
Resolve a tag name to its tagId, run the Received Message Detail report filtered to that tag, and page through every matching message.
“Give me every message tagged Priority” is a one-filter query, but it takes two endpoints to answer: the Received Message Detail report filters by tag ID, not tag name. So the flow is: resolve the name to an ID with List mailboxes, start the report with a tags filter, then poll and paginate.
Prerequisites
- An API key whose scopes include both
reportingandmessages:read, with the target mailbox in its allowlist. - The tag’s display name as it appears in Emailgistics — for example
Priority.
The flow
Resolve the tag name to a tagId
Call List mailboxes. The response includes every mailbox the key authorizes, and each mailbox carries its defined tags — tagId, tagName, and an active flag.
Match on tagName (case-insensitively — tag names are display strings) and keep only active tags. A deleted tag still appears in the list with active: false, and historical messages can still carry it, so decide deliberately whether you want it.
import requests
response = requests.get(
"https://c1.emailgistics.com/api/v1/admin/mailboxes",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=30,
)
response.raise_for_status()
mailbox = next(
m for m in response.json()["mailboxes"]
if m["mailboxEmail"] == "sales@company.com"
)
tag = next(
t for t in mailbox["tags"]
if t["tagName"].lower() == "priority" and t["active"]
)
# mailbox["mailboxId"] and tag["tagId"] feed the next stepThis same response gives you the mailboxId the report needs, so one call resolves both inputs.
Start the report, filtered by tag
POST to Received Message Detail with the tags body parameter set to the resolved ID. The filter returns messages that carry at least one of the given tag IDs — with a single ID, that’s exactly “has this tag.”
Two query parameters matter here:
fieldGroup=basic,tagsadds thetagsfield group so each row shows which tags it carries.includeDetails=trueputstagNameon each tag object — without it you only get IDs back.
Bound the query with activityStart and activityEnd. Omitting them is allowed but the defaults span effectively all history — an unbounded run over a busy mailbox is slower and returns rows you probably don’t want. Both take full ISO 8601 datetimes (YYYY-MM-DDTHH:MM:SS[Z]); a bare date like 2026-04-01 is rejected with 400.
envelope = requests.post(
"https://c1.emailgistics.com/api/v1/reports/receivedMessageDetail"
"?fieldGroup=basic,tags&includeDetails=true&max=500",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"mailboxId": mailbox["mailboxId"],
"activityType": "received",
"activityStart": "2026-04-01T00:00:00Z",
"activityEnd": "2026-05-01T00:00:00Z",
"tags": ["644ff4d68905860a966b0464"], # tagId resolved in step 1
},
timeout=30,
).json()With activityType: "received", the window filters on when each message was received; switch to replied or closed to window on those events instead — see the endpoint reference.
Poll until done, then follow next
The reports endpoints are asynchronous — the POST returns a result envelope immediately, usually with status: "running". Poll _links.self with exponential backoff (start at 2 seconds, cap at 30) until status is done, then collect result.data and follow _links.next while it’s present.
import time
messages, delay = [], 2
while True:
if envelope["status"] == "error":
raise RuntimeError(envelope["error"])
if envelope["status"] == "done":
messages.extend(envelope["result"]["data"])
next_link = envelope["_links"].get("next")
if next_link is None:
break
url = next_link["href"]
else:
time.sleep(delay)
delay = min(delay * 2, 30)
url = envelope["_links"]["self"]["href"]
envelope = requests.get(
url, headers={"Authorization": f"Bearer {API_KEY}"}, timeout=30
).json()Each row is a message with the basic fields (sender, subject, status, owner, messageId, …) plus its tags array.
End-to-end Python sketch
A complete script — tag name and mailbox email in, message rows out. Defaults to the last 30 days; pass explicit ISO datetimes as the optional third and fourth arguments to change the window.
import os
import sys
import time
from datetime import datetime, timedelta, timezone
import requests
BASE_URL = "https://c1.emailgistics.com/api/v1"
API_KEY = os.environ["EMAILGISTICS_API_KEY"]
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
def resolve_tag(tag_name: str, mailbox_email: str) -> tuple[str, str]:
"""One call resolves both report inputs: mailboxId and tagId."""
response = requests.get(f"{BASE_URL}/admin/mailboxes", headers=HEADERS, timeout=30)
response.raise_for_status()
mailbox = next(
(m for m in response.json()["mailboxes"]
if m["mailboxEmail"].lower() == mailbox_email.lower()),
None,
)
if mailbox is None:
sys.exit(f"Mailbox {mailbox_email!r} not found (or not in this key's allowlist).")
tag = next(
(t for t in mailbox["tags"]
if t["tagName"].lower() == tag_name.lower() and t["active"]),
None,
)
if tag is None:
names = ", ".join(t["tagName"] for t in mailbox["tags"] if t["active"])
sys.exit(f"Tag {tag_name!r} not found in {mailbox_email}. Active tags: {names}")
return mailbox["mailboxId"], tag["tagId"]
def messages_with_tag(
tag_name: str,
mailbox_email: str,
activity_start: str,
activity_end: str,
) -> list[dict]:
mailbox_id, tag_id = resolve_tag(tag_name, mailbox_email)
envelope = requests.post(
f"{BASE_URL}/reports/receivedMessageDetail"
"?fieldGroup=basic,tags&includeDetails=true&max=500",
headers=HEADERS,
json={
"mailboxId": mailbox_id,
"activityType": "received",
"activityStart": activity_start,
"activityEnd": activity_end,
"tags": [tag_id],
},
timeout=30,
).json()
messages, delay = [], 2
while True:
if envelope["status"] == "error":
raise RuntimeError(envelope["error"])
if envelope["status"] == "done":
messages.extend(envelope["result"]["data"])
next_link = envelope["_links"].get("next")
if next_link is None:
return messages
url = next_link["href"]
else:
time.sleep(delay)
delay = min(delay * 2, 30)
url = envelope["_links"]["self"]["href"]
envelope = requests.get(url, headers=HEADERS, timeout=30).json()
def iso(dt: datetime) -> str:
"""The API requires full ISO 8601 datetimes; a bare date is rejected with 400."""
return dt.strftime("%Y-%m-%dT%H:%M:%SZ")
if __name__ == "__main__":
if len(sys.argv) not in (3, 5):
sys.exit(f"Usage: {sys.argv[0]} <tag-name> <mailbox-email> [<start> <end>]")
now = datetime.now(timezone.utc)
start = sys.argv[3] if len(sys.argv) == 5 else iso(now - timedelta(days=30))
end = sys.argv[4] if len(sys.argv) == 5 else iso(now)
rows = messages_with_tag(sys.argv[1], sys.argv[2], start, end)
print(f"{len(rows)} message(s) tagged {sys.argv[1]!r}:")
for m in rows:
tags = ", ".join(t.get("tagName", t["tagId"]) for t in m.get("tags", []))
print(f" [{m['status']:>7}] {m['receivedTimestamp']} {m['fromEmail']} {m['subject']}")
print(f" tags: {tags}")Things to watch for
-
The filter is OR, not AND.
tagsmatches messages carrying at least one of the given IDs. To find messages that carry bothPriorityandNew Client, filter on one tag in the request and check the returnedtagsarray for the other in your own code. -
Tag names aren’t unique identifiers. Nothing prevents a deleted tag and a newer active tag from sharing a name. The
activefilter in step 1 picks the live one; if you find zero or multiple matches, surface that rather than guessing. -
Full datetimes, not dates.
activityStartandactivityEndmust beYYYY-MM-DDTHH:MM:SS[Z]— a bare date is rejected with400. The response’stimeZonefield tells you the mailbox’s IANA zone; results are reported in that zone regardless of the offset you send. -
includeDetails=trueis what returns tag names. Without it, thetagsfield group returns bare{ "tagId": ... }objects and you’d need your step-1 lookup table to translate them back. -
Result IDs expire after 24 hours. If you cache the
_links.selfURL for later, it stops working the next day. Re-issue_links.originto run the query again — see Result envelope and async polling. -
Rate limits and concurrency. All calls here share the 100 requests-per-minute cap, and at most 3 async report runs can be in flight per key — see Rate limits. Polling with the recommended backoff stays well inside both.
-
Region base URL. The examples use
c1(Canada). US-region accounts usehttps://us1.emailgistics.com— see Regions and base URLs.