How do I pull send data using the API?

Applies To:   ■ PoliteMail Desktop      ■ PoliteMail Online     ■ PoliteMail M365


Version:  4.9    5.0    5.1+


Pulling Send Data using the API

The sample below uses Python to pull sent message stats, and the sample output is also provided.  Note that you will need to get your own Personal Access Token, and substitute 'prerelease.pmail2.com' with your own PoliteMail hostname.



import os
import json
import argparse
import datetime as dt
import requests
import logging
import time

def iso(s: str) -> str:
    return dt.datetime.fromisoformat(s).isoformat()

def build_base(host: str) -> str:
    host = host.strip()
    host = host.replace("https://", "").replace("http://", "")
    return f"https://{host}/api/odata"

def odata_get(session: requests.Session, url: str, params: dict) -> dict:
    logging.info("Starting HTTP GET %s", url)
    logging.debug("Params: %s", params)
    start = time.time()
    try:
        r = session.get(url, params=params, timeout=30)
    except requests.exceptions.RequestException:
        logging.exception("Request failed for %s", url)
        raise
    elapsed = time.time() - start
    logging.info("HTTP %s %s in %.2fs", r.status_code, url, elapsed)
    if not r.ok:
        logging.error("Response body: %s", r.text)
        raise RuntimeError(f"HTTP {r.status_code}: {r.text}")
    try:
        return r.json()
    except ValueError:
        logging.exception("Failed to decode JSON from %s", url)
        raise

def fetch_sentmessages(base: str, session: requests.Session, since: str | None, until: str | None, top: int, max_requests: int = 1000) -> list[dict]:
    url = f"{base}/SentMessages"
    select_fields = ",".join([
        "ID","Subject","SentDate","OwnerID","RecipientCount",
        "OpenCount","ClickCount","ReadCount","EngagementRate",
        "AvgReadTime","MostRecentOpenDate"
    ])
    filters = []
    if since:
        filters.append(f"SentDate ge {iso(since)}")
    if until:
        filters.append(f"SentDate lt {iso(until)}")
    params = {
        "$select": select_fields,
        "$orderby": "SentDate desc",
        "$top": top
    }
    if filters:
        params["$filter"] = " and ".join(filters)

    out = []
    skip = 0
    max_pages = int(os.getenv("POLITEMAIL_MAX_PAGES", "1000"))
    page_count = 0
    request_count = 0
    while True:
        page_count += 1
        if page_count > max_pages:
            logging.warning("Reached max_pages=%s, stopping to avoid infinite loop", max_pages)
            break
        params["$skip"] = skip
        logging.info("Fetching page skip=%s top=%s", skip, top)
        if request_count >= max_requests:
            logging.info("Reached max_requests=%s, stopping further requests", max_requests)
            break
        data = odata_get(session, url, params)
        request_count += 1
        rows = data.get("value", [])
        logging.info("Received %d rows", len(rows))
        out.extend(rows)
        if len(rows) < top:
            break
        skip += top
    return out

def main():
    p = argparse.ArgumentParser()
    p.add_argument("--host", default=os.getenv("POLITEMAIL_HOST"))
    p.add_argument("--pat", default=os.getenv("POLITEMAIL_PAT"))
    p.add_argument("--since", default=None)
    p.add_argument("--until", default=None)
    p.add_argument("--top", type=int, default=1)
    p.add_argument("--max-requests", type=int, default=int(os.getenv("POLITEMAIL_MAX_REQUESTS", "1")),
                   help="Maximum number of HTTP requests to make (default 1 or POLITEMAIL_MAX_REQUESTS)")
    args = p.parse_args()

    if not args.host or not args.pat:
        raise SystemExit("Set POLITEMAIL_HOST and POLITEMAIL_PAT, or pass --host and --pat")

    base = build_base(args.host)

    # configure logging: send logs to stderr so stdout remains usable for JSON
    log_level = logging.DEBUG if os.getenv("POLITEMAIL_DEBUG") else logging.INFO
    logging.basicConfig(level=log_level, format="%(asctime)s %(levelname)s %(message)s")
    logging.info("Using base URL: %s", base)

    s = requests.Session()
    s.auth = ("pat", args.pat)
    s.headers.update({"Accept": "application/json"})
    logging.info("Starting fetch_sentmessages (top=%s since=%s until=%s max_requests=%s)", args.top, args.since, args.until, args.max_requests)
    rows = fetch_sentmessages(base, s, args.since, args.until, args.top, args.max_requests)
    # print JSON to stdout and flush immediately
    print(json.dumps(rows, indent=2, default=str), flush=True)

if __name__ == "__main__":
    main()


The sample result is shown below:
PS C:\code\api-samples\python> python politemail_sentmessage_stats.py --host prerelease.pmail2.com --pat REDACTED
2026-01-19 08:46:47,107 INFO Using base URL: https://prerelease.pmail2.com/api/odata
2026-01-19 08:46:47,108 INFO Starting fetch_sentmessages (top=1 since=None until=None max_requests=1)
2026-01-19 08:46:47,108 INFO Fetching page skip=0 top=1
2026-01-19 08:46:47,108 INFO Starting HTTP GET https://prerelease.pmail2.com/api/odata/SentMessages
2026-01-19 08:46:54,430 INFO HTTP 200 https://prerelease.pmail2.com/api/odata/SentMessages in 7.32s
2026-01-19 08:46:54,430 INFO Received 1 rows
2026-01-19 08:46:54,430 INFO Fetching page skip=1 top=1
2026-01-19 08:46:54,431 INFO Reached max_requests=1, stopping further requests
[
  {
    "ID": 25937,
    "Subject": "Message (Prerelease)",
    "SentDate": "2026-01-19T06:58:11.363Z",
    "OwnerID": 7,
    "RecipientCount": 1,
    "AvgReadTime": 30000.0,
    "ClickCount": 0.0,
    "MostRecentOpenDate": "2026-01-19T07:37:15.837Z",
    "OpenCount": 1.0,
    "ReadCount": 1.0,
    "EngagementRate": 1.0
  }
]
PS C:\code\api-samples\python>

Note the result returns some basic information on the operation, followed by the specific details about the message in a JSON format, including the subject, send date, recipient count, average read time, click count, and more.