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).
BookAnalytics — full class
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] = []
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.
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:
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_collectionloan_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']}")
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 creditsrevenue_txns = [ txn for acct in a["merged_accounts"].values() for txn in acct["transactions"] if "true_revenue" in (txn.get("tag") or [])]# All MCA paymentsmca_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"]
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]
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")