Api v4 does not accept your existing X-API-KEY.
It uses HTTP Basic Auth instead.
#!/usr/bin/env python3
"""
Fetch a calculated Profit and Loss Statement from Manager.io Cloud via /api4.
Fill in the configuration values below, then run:
pip install requests
python manager_pl_report.py
This uses HTTP Basic Auth, not /api2's X-API-KEY. Use a dedicated,
permission-restricted user (not your personal/admin login) scoped to only
this business, with "View" + "Create" permission on Reports > Profit and
Loss Statement (Settings > Users in Manager).
Once you've filled in real credentials below, keep this file private —
don't commit it or share it with anyone.
"""
import sys
from datetime import date
import requests
# ── Configuration — fill these in ────────────────────────────────────────
SUBDOMAIN = "" # from https://<SUBDOMAIN>.manager.io
USERNAME = "" # dedicated restricted user — not your admin login
PASSWORD = ""
BUSINESS = "" # exact business name as shown in Manager
FROM_DATE = date(2026, 1, 1)
TO_DATE = date(2026, 12, 31)
ACCOUNTING_METHOD = 0 # 0 = Accrual Basis, 1 = Cash Basis
# ──────────────────────────────────────────────────────────────────────────
BASE_URL = f"https://{SUBDOMAIN}.manager.io"
def create_profit_and_loss_report(session: requests.Session, from_date: date, to_date: date, accounting_method: int) -> str:
"""Creates a report definition and returns its key (GUID string)."""
payload = {
"business": BUSINESS,
"value": {
"periods": [
{"fromDate": from_date.isoformat(), "toDate": to_date.isoformat()}
],
"accountingMethod": accounting_method,
"accountCodes": False,
"excludeZeroBalances": False,
},
}
response = session.post(f"{BASE_URL}/api4/profit-and-loss-statement", json=payload)
response.raise_for_status()
return response.json() # the endpoint returns the new key as a raw JSON string
def get_profit_and_loss_view(session: requests.Session, key: str) -> dict:
"""Returns the calculated report: title, columns, hierarchical rows, cells."""
params = {"business": BUSINESS, "key": key}
response = session.get(f"{BASE_URL}/api4/profit-and-loss-statement-view", params=params)
response.raise_for_status()
return response.json()
def delete_profit_and_loss_report(session: requests.Session, key: str) -> None:
"""Cleans up the throwaway report definition created above."""
params = {"business": BUSINESS, "key": key}
response = session.delete(f"{BASE_URL}/api4/profit-and-loss-statement", params=params)
response.raise_for_status()
def print_report(report: dict) -> None:
print(report.get("title", "Profit and Loss Statement"))
for subtitle in report.get("subtitles", []):
print(f" {subtitle}")
print()
columns = report.get("columns", [])
header = "Account".ljust(40) + "".join(c["name"].rjust(15) for c in columns)
print(header)
print("-" * len(header))
def print_rows(rows, indent=0):
for row in rows:
label = (" " * indent + row["name"])[:40].ljust(40)
if row.get("rows"):
print(label)
print_rows(row["rows"], indent + 1)
else:
cells = row.get("cells") or []
values = "".join((c.get("text") or "").rjust(15) for c in cells)
print(label + values)
print_rows(report.get("rows", []))
def main() -> None:
if not USERNAME or not PASSWORD:
sys.exit("Fill in USERNAME and PASSWORD at the top of this file before running.")
session = requests.Session()
session.auth = (USERNAME, PASSWORD)
print(f"Creating P&L report definition for '{BUSINESS}' ({FROM_DATE} to {TO_DATE})...")
key = create_profit_and_loss_report(session, FROM_DATE, TO_DATE, ACCOUNTING_METHOD)
print(f"Report key: {key}")
print("Fetching calculated report...")
report = get_profit_and_loss_view(session, key)
print()
print_report(report)
delete_profit_and_loss_report(session, key)
if __name__ == "__main__":
try:
main()
except requests.HTTPError as exc:
status = exc.response.status_code if exc.response is not None else "?"
body = exc.response.text if exc.response is not None else str(exc)
print(f"Request failed: {status} {body}", file=sys.stderr)
sys.exit(1)