EmailgisticsAPI
Guides

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 reporting and messages: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 step

This 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,tags adds the tags field group so each row shows which tags it carries.
  • includeDetails=true puts tagName on 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. tags matches messages carrying at least one of the given IDs. To find messages that carry both Priority and New Client, filter on one tag in the request and check the returned tags array 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 active filter in step 1 picks the live one; if you find zero or multiple matches, surface that rather than guessing.

  • Full datetimes, not dates. activityStart and activityEnd must be YYYY-MM-DDTHH:MM:SS[Z] — a bare date is rejected with 400. The response’s timeZone field tells you the mailbox’s IANA zone; results are reported in that zone regardless of the offset you send.

  • includeDetails=true is what returns tag names. Without it, the tags field 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.self URL for later, it stops working the next day. Re-issue _links.origin to 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 use https://us1.emailgistics.com — see Regions and base URLs.

On this page