Skip to main content

What analytics gives you

GET /books/:id/analytics returns the fully computed BookAnalytics object. This is the same data that powers the Pathway synopsis dashboard: key metrics, debt positions, per-statement breakdowns, transaction-level data with tags, counterparty clusters, and screening results. Everything here is computed after applying your org’s exclusion config (excluded docs, accounts, positions, and revenue tags).
class BookAnalytics(BaseModel):
    # Per-document statement breakdowns
    statements: List[StatementAnalytics]

    # All transactions keyed by account number, tags + position info attached
    merged_accounts: Any

    # Book-level deposit / withdrawal totals
    total_deposits: float
    total_withdrawals: float
    num_deposits: int = 0
    num_withdrawals: int = 0
    avg_deposit: float = 0.0
    avg_withdrawal: float = 0.0
    largest_deposit: float = 0.0
    largest_withdrawal: float = 0.0
    avg_transaction: float = 0.0

    # Balance summary
    opening_balance: float = 0.0
    closing_balance: float = 0.0
    peak_balance: float = 0.0
    lowest_balance: float = 0.0
    average_daily_balance: Optional[float] = None
    days_negative_balance: int = 0
    total_days: int = 0

    # Revenue
    true_revenue: float = 0.0
    num_true_revenue_transactions: int = 0

    # Debt
    total_loan_disbursements: float = 0.0
    total_loan_payments: float = 0.0
    num_loan_disbursements: int = 0
    num_loan_payments: int = 0
    debt_to_income_ratio: Optional[float] = None
    num_mca_positions: int = 0
    loan_summary: List[LoanSummary] = []
    positions: List[DebtPosition] = []

    # Tag-based aggregates
    nsf_total: float = 0.0
    num_nsf: int = 0
    overdraft_total: float = 0.0
    num_overdraft: int = 0
    owner_transaction_total: float = 0.0
    num_owner_transaction: int = 0
    internal_transfer_total: float = 0.0
    num_internal_transfer: int = 0
    bank_fee_total: float = 0.0
    num_bank_fee: int = 0
    payment_processor_total: float = 0.0
    num_payment_processor: int = 0
    stop_payment_total: float = 0.0
    num_stop_payment: int = 0
    reversal_total: float = 0.0
    num_reversal: int = 0

    # Summary row (same shape as AccountStatementMetrics)
    average_statement_metrics: Optional[AccountStatementMetrics] = None

    # Counterparty clusters (top credits = revenue sources, top debits = expenses)
    counterparty_clusters: List[CounterpartyCluster] = []

    # Day-of-week patterns
    deposits_by_weekday: dict[str, float] = {}
    withdrawals_by_weekday: dict[str, float] = {}
    deposit_count_by_weekday: dict[str, int] = {}
    withdrawal_count_by_weekday: dict[str, int] = {}

    # Bank holidays in the statement period
    bank_holidays: List[dict[str, str]] = []

    # Reconciliation
    reconciliation_results: List[ReconciliationResult] = []

    # Autodeny screening result (null if not configured)
    screening_result: Any = None

    # Exclusion config applied when computing this analytics
    revenue_exclusion_tags: List[str] = []
    excluded_position_ids: List[str] = []
    excluded_document_ids: List[str] = []

Key metrics

The most commonly needed fields are at the top level:
a = requests.get(f"{API_BASE}/books/{book_id}/analytics", headers=headers).json()

metrics = {
    "total_deposits":      a["total_deposits"],
    "total_withdrawals":   a["total_withdrawals"],
    "true_revenue":        a["true_revenue"],
    "avg_daily_balance":   a["average_daily_balance"],
    "days_negative":       a["days_negative_balance"],
    "dti":                 a.get("debt_to_income_ratio"),   # null if no loan payments
    "opening_balance":     a["opening_balance"],
    "closing_balance":     a["closing_balance"],
    "peak_balance":        a["peak_balance"],
    "lowest_balance":      a["lowest_balance"],
    "total_loan_in":       a["total_loan_disbursements"],
    "total_loan_out":      a["total_loan_payments"],
    "num_mca_positions":   a["num_mca_positions"],
    "total_days":          a["total_days"],
}
True revenue excludes transactions tagged as loans, transfers, NSF, owner draws, etc. It’s the best signal for actual business cash flow. total_deposits - true_revenue gives you the excluded amount. DTI is (total_loan_payments / true_revenue) x 100. It’s null if true_revenue is 0.

Per-statement breakdown

analytics.statements[] gives you per-month, per-account metrics. Each statement covers one PDF document. Multi-account statements include one entry per account plus a account_id=0 combined aggregate.
for stmt in a["statements"]:
    print(f"\n{stmt['statement_period']} - {stmt['document_name']}")
    for acct in stmt["accounts"]:
        if acct["account_id"] == 0:
            print(f"  Combined: deposits=${acct['total_deposits']:,.2f}, "
                  f"revenue=${acct['true_revenue']:,.2f}, "
                  f"dti={acct.get('debt_to_income_ratio', 'N/A')}%")
        else:
            print(f"  {acct['account_name']}: "
                  f"${acct['starting_balance']:,.2f} to ${acct['ending_balance']:,.2f}")
is_reconciled tells you whether the parser’s transaction sum matches the bank-reported ending balance. When false, check discrepancy for the dollar difference. average_statement_metrics is a pre-computed average row across all statements with the same shape as an account object:
avg = a.get("average_statement_metrics", {})
print(f"Avg monthly deposits: ${avg.get('total_deposits', 0):,.2f}")
print(f"Avg monthly revenue:  ${avg.get('true_revenue', 0):,.2f}")
print(f"Avg daily balance:    ${avg.get('average_daily_balance', 0):,.2f}")

Debt positions

analytics.positions[] lists each detected loan or MCA as a named position. These are groups of transactions the parser identified as belonging to the same lender.
for pos in a["positions"]:
    print(f"\n{pos['name']} ({pos['loan_type']})")
    print(f"  Funded:       ${pos['total_disbursements']:,.2f} ({pos['disbursement_count']} draws)")
    print(f"  Paid back:    ${pos['total_payments']:,.2f} ({pos['payment_count']} payments)")
    print(f"  Avg payment:  ${pos.get('avg_payment', 0):,.2f}")
    print(f"  Active:       {pos['is_active']}")
    print(f"  Last payment: {pos.get('last_payment_date', 'N/A')}")

    if pos.get("funder_title"):
        print(f"  Funder: {pos['funder_title']} - {pos.get('funder_link', '')}")

    for sched in pos.get("payment_schedules", []):
        print(f"  Schedule: {sched.get('frequency')} @ ${sched.get('avg_amount', 0):,.2f}")
Loan type values: merchant_cash_advance, bank_loan, factoring, credit, lease, auto, mortgage, buy_now_pay_later, debt_collection loan_summary[] rolls up positions by loan type for a quick debt composition view:
for summary in a["loan_summary"]:
    if not summary["is_excluded"]:
        print(f"{summary['loan_type']}: "
              f"in=${summary['total_disbursements']:,.2f}, "
              f"out={summary['total_payments']:,.2f}, "
              f"count={summary['transaction_count']}")

Transaction-level data

analytics.merged_accounts is an object keyed by account ID. Each account has a flat list of all transactions across the full statement period, with tags and position info attached.
for acct_id, account in a["merged_accounts"].items():
    print(f"\nAccount {acct_id}: {account['account_name']}")

    for txn in account["transactions"]:
        tags = txn.get("tag") or []
        position = txn.get("position")

        print(f"  {txn['transaction_date']}  "
              f"{'+'if txn['transaction_type']=='credit' else '-'}"
              f"${txn['amount']:,.2f}  "
              f"{txn['description'][:50]}")

        if tags:
            print(f"    tags: {', '.join(tags)}")
        if position:
            print(f"    position: {position['position_name']} ({position['loan_type']})")
Filter by tag:
# All true revenue credits
revenue_txns = [
    txn
    for acct in a["merged_accounts"].values()
    for txn in acct["transactions"]
    if "true_revenue" in (txn.get("tag") or [])
]

# All MCA payments
mca_payments = [
    txn
    for acct in a["merged_accounts"].values()
    for txn in acct["transactions"]
    if "merchant_cash_advance" in (txn.get("tag") or [])
    and txn["transaction_type"] == "debit"
]

Counterparty clusters

analytics.counterparty_clusters[] groups transactions by counterparty (normalized description). Top credits are revenue sources. Top debits are expense categories.
credits = sorted(
    [c for c in a["counterparty_clusters"] if c["direction"] == "credit"],
    key=lambda x: x["total"],
    reverse=True
)[:10]

for c in credits:
    print(f"  {c['counterparty']}: ${c['total']:,.2f} ({c['count']} transactions)")

debits = sorted(
    [c for c in a["counterparty_clusters"] if c["direction"] == "debit"],
    key=lambda x: x["total"],
    reverse=True
)[:10]

Tag-based metrics

Every transaction is tagged. Aggregates are pre-computed on BookAnalytics:
red_flags = {
    "nsf_fees":      (a["nsf_total"], a["num_nsf"]),
    "overdrafts":    (a["overdraft_total"], a["num_overdraft"]),
    "stop_payments": (a["stop_payment_total"], a["num_stop_payment"]),
}

for label, (total, count) in red_flags.items():
    if count > 0:
        print(f"{label}: ${total:,.2f} ({count} occurrences)")

print(f"\nOwner transactions: ${a['owner_transaction_total']:,.2f}")
print(f"Internal transfers: ${a['internal_transfer_total']:,.2f}")
print(f"Bank fees:          ${a['bank_fee_total']:,.2f}")
print(f"Payment processors: ${a['payment_processor_total']:,.2f}")

Weekday cash flow patterns

deposits_by_day = a.get("deposits_by_weekday", {})
top_day = max(deposits_by_day, key=deposits_by_day.get) if deposits_by_day else None
print(f"Highest deposit day: {top_day} (${deposits_by_day.get(top_day, 0):,.2f})")

Revenue exclusion config

analytics.revenue_exclusion_tags shows which tags were excluded from true_revenue when this analytics was computed. Either book-level (custom) or falls back to org-level default.
print("Revenue exclusion tags:", a["revenue_exclusion_tags"])
# e.g. ["merchant_cash_advance", "bank_loan", "factoring", "internal_transfer", ...]

excluded_amount = a["total_deposits"] - a["true_revenue"]
excluded_count = a["num_deposits"] - a["num_true_revenue_transactions"]
print(f"Excluded from revenue: ${excluded_amount:,.2f} across {excluded_count} transactions")

Build a simple underwriting summary

def uw_summary(analytics):
    a = analytics
    avg = a.get("average_statement_metrics", {})

    return {
        "period": f"{a['statements'][0]['statement_start_date']} to "
                  f"{a['statements'][-1]['statement_end_date']}",
        "months": len(a["statements"]),
        "total_deposits": a["total_deposits"],
        "true_revenue": a["true_revenue"],
        "avg_monthly_revenue": avg.get("true_revenue", 0),
        "avg_daily_balance": a["average_daily_balance"],
        "days_negative": a["days_negative_balance"],
        "dti": a.get("debt_to_income_ratio"),
        "num_positions": len(a["positions"]),
        "active_positions": sum(1 for p in a["positions"] if p["is_active"] and not p["is_excluded"]),
        "total_mca_payments": a["total_loan_payments"],
        "nsf_count": a["num_nsf"] + a["num_overdraft"],
    }