# Analytics Source: https://docs.lendpathway.com/api-reference/endpoint/analytics Read normalized bank metrics, transactions, positions, screening, and tax cash flow. Analytics are computed from the current Book every time you request them. The response reflects transaction tags, excluded documents and accounts, excluded positions, organization revenue rules, business-day settings, and screening policy. ## Bank analytics ```http theme={null} GET /books/{book_id}/analytics ``` Call this after the Book reaches `parse_status: "completed"` and contains bank statement or Plaid data. ```python Python theme={null} import requests API_BASE = "https://api.lendpathway.com/api" TOKEN = "pat_your_token_here" BOOK_ID = "99cc93e6-1f3f-42b1-9fe4-ba5a95be9c78" response = requests.get( f"{API_BASE}/books/{BOOK_ID}/analytics", headers={"Authorization": f"Bearer {TOKEN}"}, timeout=60, ) response.raise_for_status() analytics = response.json() print("True revenue:", analytics["true_revenue"]) print("Average daily balance:", analytics["average_daily_balance"]) print("Debt positions:", len(analytics["positions"])) ``` ```javascript JavaScript theme={null} const API_BASE = "https://api.lendpathway.com/api"; const TOKEN = "pat_your_token_here"; const BOOK_ID = "99cc93e6-1f3f-42b1-9fe4-ba5a95be9c78"; const response = await fetch(`${API_BASE}/books/${BOOK_ID}/analytics`, { headers: { Authorization: `Bearer ${TOKEN}` }, }); if (!response.ok) { throw new Error(`${response.status}: ${await response.text()}`); } const analytics = await response.json(); console.log({ trueRevenue: analytics.true_revenue, averageDailyBalance: analytics.average_daily_balance, positions: analytics.positions.length, }); ``` ```bash cURL theme={null} curl --fail-with-body \ "https://api.lendpathway.com/api/books/99cc93e6-1f3f-42b1-9fe4-ba5a95be9c78/analytics" \ -H "Authorization: Bearer pat_your_token_here" ``` ### How to read the response The response has several useful levels: * Book totals such as `true_revenue`, `average_daily_balance`, `total_loan_payments`, and `debt_to_income_ratio` * `statements`, organized by statement period with an account breakdown inside each period * `merged_accounts`, containing the transaction history enriched with cleaned tags and position information * `positions`, containing detected debt relationships and their payment schedules * `screening_metrics` and `screening_result`, containing the fact sheet and current organization policy decision All monetary values are dollar amounts. Ratios such as `debt_to_income_ratio` and `holdback_pct` are percentage values, so `12.5` means 12.5%. ## Response models These models mirror the current public response. Default values are included because older Books can have sparse data. ```python theme={null} from typing import Any from pydantic import BaseModel, Field class BookAnalytics(BaseModel): statements: list[StatementAnalytics] merged_accounts: dict[str, MergedAccount] | None = None loan_summary: list[LoanSummary] = Field(default_factory=list) positions: list[DebtPosition] = Field(default_factory=list) total_deposits: float total_withdrawals: float total_loan_disbursements: float = 0.0 total_loan_payments: float = 0.0 opening_balance: float = 0.0 closing_balance: float = 0.0 peak_balance: float = 0.0 lowest_balance: float = 0.0 largest_deposit: float = 0.0 avg_deposit: float = 0.0 largest_withdrawal: float = 0.0 avg_withdrawal: float = 0.0 num_deposits: int = 0 num_withdrawals: int = 0 avg_transaction: float = 0.0 total_days: int = 0 average_daily_balance: float | None = None days_negative_balance: int = 0 days_under_threshold: int = 0 low_balance_threshold: float | None = None true_revenue: float = 0.0 num_true_revenue_transactions: int = 0 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 num_loan_disbursements: int = 0 num_loan_payments: int = 0 debt_to_income_ratio: float | None = None num_mca_positions: int = 0 num_active_mca_positions: int = 0 num_factoring_positions: int = 0 num_reversal_credits: int = 0 num_missed_payments: int = 0 num_modified_payments: int = 0 total_mca_daily_remit: float = 0.0 total_mca_monthly_remit: float = 0.0 total_mca_paid_net: float = 0.0 total_mca_holdback_pct: float | None = None reconciliation_results: list[ReconciliationResult] = Field(default_factory=list) revenue_exclusion_tags: list[str] = Field(default_factory=list) excluded_position_ids: list[str] = Field(default_factory=list) excluded_document_ids: list[str] = Field(default_factory=list) average_statement_metrics: AccountStatementMetrics | None = None counterparty_clusters: list[CounterpartyCluster] = Field(default_factory=list) deposits_by_weekday: dict[str, float] = Field(default_factory=dict) withdrawals_by_weekday: dict[str, float] = Field(default_factory=dict) deposit_count_by_weekday: dict[str, int] = Field(default_factory=dict) withdrawal_count_by_weekday: dict[str, int] = Field(default_factory=dict) bank_holidays: list[dict[str, str]] = Field(default_factory=list) most_recent_transaction_date: str | None = None most_recent_statement_end_date: str | None = None most_recent_mca_disbursement_date: str | None = None most_recent_mca_payment_date: str | None = None screening_metrics: ScreeningMetrics | None = None screening_result: ScreeningResult | None = None ``` ```python theme={null} from typing import Literal from pydantic import BaseModel, Field class AccountStatementMetrics(BaseModel): account_id: int account_name: str account_number: str document_id: str | None = None starting_balance: float ending_balance: float num_deposits: int total_deposits: float num_withdrawals: int total_withdrawals: float is_reconciled: bool reconciliation_skipped: bool = False discrepancy: float | None = None reconciliation_message: str | None = None computed_ending_balance: float | None = None expected_ending_balance: float | None = None loan_disbursements: float = 0.0 loan_payments: float = 0.0 average_daily_balance: float | None = None days_negative_balance: int = 0 days_under_threshold: int = 0 days_in_period: int = 0 true_revenue: float = 0.0 num_true_revenue_transactions: int = 0 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 debt_to_income_ratio: float | None = None class StatementAnalytics(BaseModel): document_id: str document_ids: list[str] = Field(default_factory=list) document_name: str document_type: str | None = None statement_start_date: str statement_end_date: str statement_period: str accounts: list[AccountStatementMetrics] class LoanSummary(BaseModel): loan_type: str total_disbursements: float total_payments: float disbursement_count: int payment_count: int is_excluded: bool = False class CounterpartyCluster(BaseModel): cluster_id: str counterparty: str direction: Literal["credit", "debit"] total: float count: int transaction_ids: list[int] class ReconciliationResult(BaseModel): month: str account_name: str reconciled: bool reconciliation_skipped: bool = False discrepancy: float attempts_made: int reason: str | None = None ``` Each statement normally contains individual account rows plus an `account_id: 0` row named `COMBINED`. The combined row represents the period-wide cash line across included accounts. ```python theme={null} from typing import Literal from pydantic import BaseModel, Field class TransactionPosition(BaseModel): position_id: str position_name: str loan_type: str funder_title: str | None = None class EnrichedTransaction(BaseModel): transaction_id: int document_id: str | None = None transaction_date: str description: str amount: float transaction_type: Literal["credit", "debit"] ledger_balance: float | None = None tag: list[str] = Field(default_factory=list) position: TransactionPosition | None = None class MergedAccount(BaseModel): account_number: str account_name: str | None = None transactions: list[EnrichedTransaction] = Field(default_factory=list) ``` `merged_accounts` is keyed by the account ID serialized as a string. Transaction tags have already been cleaned for credit/debit direction. Qualifying credits also receive the synthetic `true_revenue` tag, and transactions without another tag receive `untagged`. ```python theme={null} from typing import Literal from pydantic import BaseModel, Field Frequency = Literal["daily", "weekly", "monthly", "irregular"] ScheduleState = Literal["active", "closed"] EpisodeState = Literal["pending", "active", "closed"] EpisodeRole = Literal["initial", "renewal", "stack", "orphan"] PositionStatus = Literal["just_funded", "active", "closed"] class Disbursement(BaseModel): disbursement_id: str transaction_ids: list[int] = Field(default_factory=list) date: str amount: float is_merged: bool = False class ReversalCredit(BaseModel): transaction_id: int date: str amount: float description: str = "" attributed_schedule_id: str | None = None class Miss(BaseModel): evidence: Literal["reversal", "gap"] date: str | None = None date_window: tuple[str, str] | None = None expected_amount: float count_estimate: int = 1 reversal_txn_id: int | None = None severity: Literal["single", "multi", "long"] = "single" schedule_id: str | None = None class Modification(BaseModel): date: str before_amount: float after_amount: float delta_pct: float type: Literal["cross_stream", "within_stream"] schedule_id: str class PaymentSchedule(BaseModel): schedule_id: str transaction_ids: list[int] = Field(default_factory=list) avg_amount: float = 0.0 frequency: Frequency = "irregular" pull_day: str | None = None remit_daily: float | None = None amount_variance: float = 0.0 is_holdback_style: bool = False first_payment: str = "" last_payment: str = "" payment_count: int = 0 total_paid: float = 0.0 total_paid_net: float = 0.0 term_est_days: int | None = None state: ScheduleState = "active" misses: list[Miss] = Field(default_factory=list) modifications: list[Modification] = Field(default_factory=list) class Episode(BaseModel): episode_id: str advance: Disbursement | None = None schedules: list[PaymentSchedule] = Field(default_factory=list) role: EpisodeRole = "initial" state: EpisodeState = "pending" first_payment: str | None = None last_payment: str | None = None class PositionTransaction(BaseModel): transaction_id: str document_id: str | None = None date: str description: str amount: float type: str class DebtPosition(BaseModel): position_id: str name: str loan_type: str is_excluded: bool = False funder_uuid: str | None = None funder_title: str | None = None funder_link: str | None = None funder_contact: str | None = None funder_email: str | None = None transactions: list[PositionTransaction] = Field(default_factory=list) total_disbursements: float = 0.0 total_payments: float = 0.0 transaction_count: int = 0 disbursement_count: int = 0 payment_count: int = 0 avg_disbursement: float = 0.0 avg_payment: float = 0.0 first_disbursement_date: str | None = None last_disbursement_date: str | None = None last_payment_date: str | None = None returns_count: int = 0 returns_total: float = 0.0 episodes: list[Episode] = Field(default_factory=list) reversal_credits: list[ReversalCredit] = Field(default_factory=list) status: PositionStatus = "closed" daily_remit_burden: float = 0.0 monthly_remit_burden: float = 0.0 holdback_pct: float | None = None n_active_schedules: int = 0 n_active_episodes: int = 0 est_total_payback: float | None = None total_paid_net: float = 0.0 progress_pct: float | None = None has_renewal: bool = False has_stack: bool = False potential_missed_payments: list[Miss] = Field(default_factory=list) potential_modified_payments: list[Modification] = Field(default_factory=list) ``` An episode represents one observed advance and its payment streams. `renewal` and `stack` describe how a later advance relates to earlier active schedules. `orphan` means payments are visible but the original advance predates the statement window. ```python theme={null} from typing import Any, Literal from pydantic import BaseModel, Field class ScreeningMetrics(BaseModel): avg_daily_balance: float | None = None negative_days: float | None = None days_under_threshold: float | None = None true_revenue: float | None = None avg_monthly_revenue: float | None = None avg_monthly_deposits: float | None = None total_loan_payments: float | None = None debt_to_income_ratio: float | None = None closing_balance: float | None = None opening_balance: float | None = None total_days: float | None = None lowest_balance: float | None = None num_deposits: float | None = None num_withdrawals: float | None = None num_true_revenue_transactions: float | None = None num_mca_positions: float | None = None time_in_business_in_days: float | None = None account_holder_ownership_percentage: float | None = None requested_loan_amount: float | None = None most_recent_month_revenue: float | None = None most_recent_month_negative_days: float | None = None most_recent_month_avg_daily_balance: float | None = None most_recent_month_deposits: float | None = None most_recent_month_loan_payments: float | None = None num_nsf: float | None = None num_overdraft: float | None = None num_active_mca_positions: float | None = None num_factoring_positions: float | None = None days_since_last_transaction: float | None = None days_since_last_statement_end: float | None = None days_since_last_mca_disbursement: float | None = None days_since_last_mca_payment: float | None = None total_deposits: float | None = None total_withdrawals: float | None = None avg_monthly_withdrawals: float | None = None avg_monthly_deposit_count: float | None = None avg_monthly_negative_days: float | None = None avg_monthly_loan_payments: float | None = None nsf_total: float | None = None overdraft_total: float | None = None num_reversal_credits: float | None = None num_missed_payments: float | None = None num_modified_payments: float | None = None business_name: str | None = None state_code: str | None = None industry: str | None = None class ResolvedScreeningRule(BaseModel): target_type: Literal["state", "industry", "all"] target_value: str equation: dict[str, Any] | None = None deny_all: bool = False lendsaas_decline_reason_ids: list[str] | None = None result: Literal["PASS", "REJECT"] reason: str rule_as_variables: str rule_after_substituting: str class ScreeningResult(BaseModel): result: Literal["PASS", "REJECT"] num_passed: int num_total: int resolved_rules: list[ResolvedScreeningRule] = Field(default_factory=list) ``` `screening_result` can be absent when screening is disabled or when a fact sheet cannot be built. A missing metric causes the individual rule that needs it to pass. ## Account-first statement view ```http theme={null} GET /books/{book_id}/statements ``` This view groups data by account and then by month. It includes daily balance maps and monthly lender activity. It does not apply organization-level document, account, or revenue exclusions. ```python theme={null} from typing import Any from pydantic import BaseModel, Field class SimpleFunder(BaseModel): position_id: str name: str loan_type: str funder_uuid: str | None = None funder_title: str | None = None funder_link: str | None = None funder_contact: str | None = None funder_email: str | None = None transaction_count: int total_disbursements: float total_payments: float funded_date: str | None = None first_payment_date: str | None = None last_payment_date: str | None = None payment_count: int = 0 avg_payment_amount: float | None = None payment_frequency: str | None = None class SimpleStatement(BaseModel): document_id: str document_name: str statement_start_date: str statement_end_date: str starting_balance: float ending_balance: float min_balance: float = 0.0 max_balance: float = 0.0 sum_credits: float sum_debits: float net_deposits: float num_deposits: int num_withdrawals: int average_daily_balance: float | None = None days_negative_balance: int = 0 days_in_period: int = 0 revenue_credits: float = 0.0 loan_disbursements: float = 0.0 total_mca_disbursements: float = 0.0 loan_payments: float = 0.0 debt_to_income_ratio: float | None = None transactions: list[dict[str, Any]] daily_balances: dict[str, float] = Field(default_factory=dict) funders: list[SimpleFunder] = Field(default_factory=list) class SimpleAccount(BaseModel): account_id: int account_name: str account_number: str bank_name: str routing_number: str | None = None account_type: str business_name: str business_address: dict[str, Any] | None = None statements: list[SimpleStatement] ``` ## Tax cash-flow analysis ```http theme={null} GET /books/{book_id}/tax-analytics ``` Returns qualifying-income analysis derived from the tax forms stored on the Book. ```python theme={null} from typing import Any, Literal from pydantic import BaseModel, Field class TaxEntity(BaseModel): name: str tin: str | None = None address: str | None = None class IncomeSource(BaseModel): source_type: Literal["wages", "schedule_c", "partnership", "s_corp"] source_name: str entity: TaxEntity | None = None ownership_pct: float | None = None ordinary_income: float = 0 rental_income: float = 0 guaranteed_payments: float = 0 depreciation_addback: float = 0 other_income: float | None = None other_deductions: float | None = None subtotal: float = 0 has_k1: bool = False has_return: bool = False k1_return_match: bool | None = None class ReconciliationWarning(BaseModel): entity_name: str warning_type: Literal[ "missing_k1", "missing_return", "income_mismatch", "missing_from_schedule_e", ] message: str class ParsedFormField(BaseModel): key: str label: str value: Any format: Literal["currency", "percent", "text", "count"] class ParsedFormCard(BaseModel): title: str entity_name: str | None = None entity_tin: str | None = None badge: str | None = None fields: list[ParsedFormField] = Field(default_factory=list) class TaxYearAnalysis(BaseModel): tax_year: int borrower: TaxEntity | None = None wage_sources: list[IncomeSource] = Field(default_factory=list) schedule_c_sources: list[IncomeSource] = Field(default_factory=list) partnership_sources: list[IncomeSource] = Field(default_factory=list) s_corp_sources: list[IncomeSource] = Field(default_factory=list) schedule_e_entity_count: int = 0 k1_count: int = 0 warnings: list[ReconciliationWarning] = Field(default_factory=list) parsed_forms: list[ParsedFormCard] = Field(default_factory=list) total_wages: float = 0 total_schedule_c: float = 0 total_partnership: float = 0 total_s_corp: float = 0 total_depreciation_addback: float = 0 total_qualifying_income: float = 0 reported_agi: float | None = None class TaxCashFlowAnalysis(BaseModel): years: list[TaxYearAnalysis] = Field(default_factory=list) most_recent_year: int | None = None avg_qualifying_income: float | None = None ``` ## Response conditions | Status | Meaning | | ------ | ------------------------------------------------------------------------ | | `400` | The Book has not completed parsing or does not contain the required data | | `401` | Missing or invalid PAT | | `403` | The token cannot access this Book | | `404` | The Book does not exist in the token's organization | For practical selection and aggregation patterns, continue with [Working with analytics data](/cookbook/working-with-analytics). # Authentication Source: https://docs.lendpathway.com/api-reference/endpoint/authentication Create, use, verify, and revoke a Pathway API token. Pathway uses Personal Access Tokens for API authentication. A PAT belongs to one user inside one organization and begins with `pat_`. ```http theme={null} Authorization: Bearer pat_your_token_here ``` ## Create a token 1. Open the Pathway app. 2. Click your organization name in the lower-left corner. 3. Open **Settings** and select **Account**. 4. Scroll to **API Access Tokens** and click **New Token**. 5. Name the token and choose whether it should be read-only. 6. Copy it before closing the dialog. The raw token is displayed once. Pathway stores its SHA-256 hash and cannot show the original value again. ## Choose the access level | Token | Use it for | Behavior | | ---------- | ------------------------------------------------------- | ------------------------------------------------------------------------------- | | Read-write | Server integrations that submit or modify Books | GET and mutating requests are allowed according to the user's organization role | | Read-only | Reporting, internal analysis, and narrow data consumers | GET requests work; POST, PATCH, PUT, and DELETE return `403` | Submitting documents requires a read-write token. ## Send the token Include the token in the `Authorization` header on every request. ```python Python theme={null} import requests API_BASE = "https://api.lendpathway.com/api" TOKEN = "pat_your_token_here" headers = { "Authorization": f"Bearer {TOKEN}", } response = requests.get( f"{API_BASE}/auth/me", headers=headers, timeout=30, ) response.raise_for_status() print(response.json()) ``` ```javascript JavaScript theme={null} const API_BASE = "https://api.lendpathway.com/api"; const TOKEN = "pat_your_token_here"; const headers = { Authorization: `Bearer ${TOKEN}`, }; const response = await fetch(`${API_BASE}/auth/me`, { headers }); if (!response.ok) { throw new Error(`${response.status}: ${await response.text()}`); } console.log(await response.json()); ``` ```bash cURL theme={null} curl --fail-with-body \ "https://api.lendpathway.com/api/auth/me" \ -H "Authorization: Bearer pat_your_token_here" ``` ## Verify the token `GET /auth/me` returns the identity attached to the credential. ```python theme={null} class AuthMeResponse(BaseModel): org_id: str org_name: str user_id: str user_name: str ``` ```json theme={null} { "org_id": "c4f9dda9-7875-4115-961f-0ac4b9630526", "org_name": "Acme Funding", "user_id": "fa576914-9590-40af-bbb8-c3af6f500859", "user_name": "John Doe" } ``` The token always acts inside the organization shown here. You do not need to send `X-Org-Id` with PAT requests. If you send it anyway, it must match the token's organization. ## Store it safely Keep the token on your server. Do not place it in browser code, mobile application bundles, public repositories, logs, or screenshots. For deployed applications, read it from a secret manager or environment variable: ```python theme={null} import os TOKEN = os.environ["PATHWAY_API_TOKEN"] ``` ## Revoke a token Return to **Settings → Account → API Access Tokens**, find the token by name or prefix, and click the delete icon. Revocation takes effect immediately. PATs do not expire on a timer. They remain valid until revoked, the user is removed, or the organization is deleted. ## Authentication responses | Status | Meaning | | ------ | ---------------------------------------------------------------------------------- | | `401` | The token is missing, malformed, invalid, or revoked | | `403` | The token is valid but cannot perform this action or access the requested resource | | `429` | The token exceeded the 30 requests-per-minute limit | Give each integration its own named token. Usage remains easier to identify and one integration can be revoked without interrupting the others. # Books Source: https://docs.lendpathway.com/api-reference/endpoint/books Check parse status and read the raw output for a deal. A Book is the complete stored deal. It carries the deal's identity, parse status, source, user edits, exclusions, and raw output from every parser that ran across its documents. Bank-statement data has several representations. Choosing one should be deliberate: | Data | Where it lives | What it contains | | ---------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------- | | Complete Book | `GET /books/{book_id}` | Book fields and the entire `book_meta` object | | Canonical raw bank result | Inside `book_meta` | Extracted business, owners, accounts, statement ledgers, transactions, and stored positions | | Computed underwriting result | `GET /books/{book_id}/analytics` | Current metrics, normalized transactions, enriched positions, counterparties, and screening | | Simple statement history | `GET /books/{book_id}/statements` | Accounts first, with their statements and daily balances nested underneath | Credit reports, tax forms, loan applications, photo IDs, and voided checks are also returned inside the full Book's `book_meta`. ## Get a Book ```http theme={null} GET /books/{book_id} ``` ```python Python theme={null} import requests API_BASE = "https://api.lendpathway.com/api" TOKEN = "pat_your_token_here" BOOK_ID = "99cc93e6-1f3f-42b1-9fe4-ba5a95be9c78" response = requests.get( f"{API_BASE}/books/{BOOK_ID}", headers={"Authorization": f"Bearer {TOKEN}"}, timeout=30, ) response.raise_for_status() book = response.json() print(book["parse_status"], book["parse_status_message"]) ``` ```javascript JavaScript theme={null} const API_BASE = "https://api.lendpathway.com/api"; const TOKEN = "pat_your_token_here"; const BOOK_ID = "99cc93e6-1f3f-42b1-9fe4-ba5a95be9c78"; const response = await fetch(`${API_BASE}/books/${BOOK_ID}`, { headers: { Authorization: `Bearer ${TOKEN}` }, }); if (!response.ok) { throw new Error(`${response.status}: ${await response.text()}`); } const book = await response.json(); console.log(book.parse_status, book.parse_status_message); ``` ```bash cURL theme={null} curl --fail-with-body \ "https://api.lendpathway.com/api/books/99cc93e6-1f3f-42b1-9fe4-ba5a95be9c78" \ -H "Authorization: Bearer pat_your_token_here" ``` ## Parse status `parse_status` follows this state flow: ```mermaid theme={null} flowchart LR N["new"] --> P["processing"] P --> C["completed"] P --> F["failed"] P --> X["cancelled"] ``` | Status | Meaning | | ------------ | ------------------------------------------------------------------------------ | | `new` | The Book exists and has not started parsing | | `processing` | Files are being uploaded, classified, or parsed | | `completed` | Parsing finished. Available parser outputs are attached to `book_meta` | | `failed` | Parsing could not finish. Inspect `parse_status_message` and document statuses | | `cancelled` | A running parse was stopped | The API can complete a Book when one document-specific pipeline fails and another succeeds. Inspect the parser keys you need rather than assuming every uploaded document produced output. ## Complete Book response The following models describe the JSON returned by `GET /books/{book_id}`. This call returns the complete stored Book, including `book_meta`. ```python theme={null} from datetime import datetime from typing import Any, Literal from uuid import UUID from pydantic import BaseModel, Field ParseStatus = Literal["new", "processing", "completed", "failed", "cancelled"] BookOrigin = Literal["email", "manual", "api", "lendsaas", "salesforce"] class UserSummary(BaseModel): id: UUID name: str | None = None email: str | None = None picture: str | None = None class EmailAddress(BaseModel): email: str name: str | None = None class BookLineageMeta(BaseModel): origin: BookOrigin = "manual" sender_email: str | None = None sender_name: str | None = None originator_group_id: UUID | None = None email_thread_id: str | None = None additional_addresses: list[EmailAddress] = Field(default_factory=list) class Book(BaseModel): id: UUID org_id: UUID | None = None parse_status: ParseStatus | None = "new" parse_status_message: str | None = None name: str description: str | None = None book_tag: str | None = None is_starred: bool = False is_deleted: bool = False created_by: UUID | None = None created_by_user: UserSummary | None = None created_at: datetime updated_at: datetime document_count: int | None = None # deprecated book_meta: BookMeta = Field(default_factory=BookMeta) last_parsed_at: datetime | None = None parse_job_id: UUID | None = None email_thread_id: str | None = None salesforce_opportunity_id: str | None = None lendsaas_lead_id: str | None = None orgmeter_lead_id: str | None = None lineage_meta: BookLineageMeta = Field(default_factory=BookLineageMeta) is_legacy: bool = False notes: str | None = None eval_baseline: dict[str, Any] | None = None web_research_running: bool = False ``` ```python theme={null} from typing import Any from pydantic import BaseModel, Field class BookMeta(BaseModel): # Raw output written by the document-specific parsers parser_v2_mca_result: dict[str, Any] | None = None parser_v2_credit_report: dict[str, Any] | None = None parser_v2_tax_forms: dict[str, Any] | None = None parser_v2_loan_application: dict[str, Any] | None = None parser_v2_identity_documents: dict[str, Any] | None = None parser_v2_plaid_result: dict[str, Any] | None = None # Explicit user and organization configuration applied at analytics time excluded_document_ids: list[str] = Field(default_factory=list) excluded_account_ids: list[int] = Field(default_factory=list) excluded_position_ids: list[str] = Field(default_factory=list) revenue_exclusion_tags: list[str] | None = None selected_credit_bureau: str | None = None ``` ### Raw parser keys | Key | Content | | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | | `parser_v2_mca_result` | Uploaded bank statement extraction with businesses, accounts, statement ledgers, transactions, and stored debt positions | | `parser_v2_plaid_result` | Plaid Asset Report data and its normalized bank-statement representation. When present, it is the canonical bank source | | `parser_v2_credit_report` | Subject identity, bureau data, FICO scores, inquiries, and tradelines | | `parser_v2_tax_forms` | Extracted 1040, Schedule C, 1065, 1120-S, and K-1 forms | | `parser_v2_loan_application` | Fields extracted from the application, including requested amount and applicant information | | `parser_v2_identity_documents` | Extracted photo ID and voided-check data | `book_meta` stores parser output and explicit edits. Computed metrics are generated when you call `/analytics` and are not stored here. ## Canonical raw bank result Bank data can arrive from uploaded statements or a Plaid Asset Report. Pathway uses the normalized Plaid result when it exists. Uploaded-statement output is the fallback. ```python theme={null} book = requests.get( f"{API_BASE}/books/{BOOK_ID}", headers={"Authorization": f"Bearer {TOKEN}"}, timeout=30, ).json() meta = book["book_meta"] plaid = meta.get("parser_v2_plaid_result") or {} canonical_mca = plaid.get("holy_mca") or meta.get("parser_v2_mca_result") if canonical_mca: print(canonical_mca["business"]) print(canonical_mca["merged_accounts"]) ``` `merged_accounts` is the most direct raw transaction view. It combines transactions from every statement by account number. `bank_statements` preserves the source-document and statement-period hierarchy. ```python theme={null} from typing import Any, Literal from pydantic import BaseModel, Field class PhoneNumber(BaseModel): country_code: str = "+1" area_code: str phone_number: str extension: str | None = None class Address(BaseModel): country: str | None = None street_address: str | None = None street_address_line_2: str | None = None city: str | None = None state_province: str | None = None postal_code: str | None = None class Human(BaseModel): full_name: str phone_number: PhoneNumber | None = None human_address: Address | None = None human_relationship_to_business: str | None = None class Business(BaseModel): business_name: str legal_business_name: str | None = None business_phone_number: PhoneNumber | None = None business_address: Address | None = None class AccountLedger(BaseModel): account_id: int account_name: str account_number: str account_type: str bank_name: str routing_number: str | None = None class HolyTransaction(BaseModel): transaction_id: int | None = None document_id: str | None = None transaction_date: str | None = None description: str | None = None amount: float | None = None transaction_type: Literal["credit", "debit"] | str | None = None ledger_balance: float | None = None tag: list[str] | None = None position: dict[str, Any] | None = None class ReconciliationResult(BaseModel): reconciled: bool = False reconciliation_skipped: bool = False attempts_made: int = 0 sum_transactions: float = 0.0 computed_ending_balance: float = 0.0 expected_ending_balance: float = 0.0 discrepancy: float = 0.0 last_error: str | None = None class HolyLedger(BaseModel): account_id: int | None = None account_name: str | None = None account_number: str | None = None account_type: str | None = None bank_name: str | None = None routing_number: str | None = None statement_starting_balance: float | None = None statement_ending_balance: float | None = None statement_num_credits: int | None = None statement_num_debits: int | None = None transactions: list[HolyTransaction] | None = None reconciliation_result: ReconciliationResult | None = None class HolyBankStatement(BaseModel): document_id: str | None = None document_name: str | None = None document_type: str | None = None statement_start_date: str | None = None statement_end_date: str | None = None ledgers: list[HolyLedger] | None = None class MergedMCAAccount(BaseModel): account_number: str | None = None account_name: str | None = None transactions: list[HolyTransaction] | None = None class StoredPosition(BaseModel): position_id: str name: str | None = None loan_type: str = "merchant_cash_advance" transaction_ids: list[int] = Field(default_factory=list) funder_uuid: str | None = None funder_title: str | None = None funder_link: str | None = None funder_contact: str | None = None funder_email: str | None = None class HolyMCAResult(BaseModel): business: Business | None = None humans: list[Human] | None = None account_ledgers: list[AccountLedger] | None = None bank_statements: list[HolyBankStatement] | None = None merged_accounts: dict[str, MergedMCAAccount] | None = None positions: list[StoredPosition] = Field(default_factory=list) total_disbursements: float = 0.0 total_payments: float = 0.0 transaction_count: int = 0 web_research: str | None = None tampering_analysis: Any | None = None ``` These values reflect parser output and stored edits. For metrics such as true revenue, average daily balance, debt-to-income ratio, normalized position schedules, and screening decisions, request the full [`BookAnalytics`](/api-reference/endpoint/analytics#response-models). A reparse replaces `book_meta`. Any manual transaction tags, debt-position edits, and exclusions on the existing parse are discarded. ## List Books ```http theme={null} GET /books/ ``` Returns every active Book in the token's organization, newest first. ```python theme={null} response = requests.get( f"{API_BASE}/books/", headers={"Authorization": f"Bearer {TOKEN}"}, timeout=30, ) response.raise_for_status() for book in response.json(): print(book["id"], book["name"], book["parse_status"]) ``` The list endpoint deliberately returns `book_meta` as an empty object because raw parser output can be large. Fetch a specific Book when you need its full metadata. ## Update Book fields ```http theme={null} PATCH /books/{book_id} Content-Type: application/json ``` Send only the fields you want to change. The supported content fields are `name`, `description`, `book_tag`, and `notes`. ```python theme={null} response = requests.patch( f"{API_BASE}/books/{BOOK_ID}", headers={"Authorization": f"Bearer {TOKEN}"}, json={ "name": "Acme Coffee Renewal", "book_tag": "priority", }, timeout=30, ) response.raise_for_status() ``` The default response omits the large parser payload. Add `?include_meta=true` when the updated response also needs the complete `book_meta`. `DELETE /books/{book_id}` soft-deletes the Book and cancels an active parse. `POST /books/{book_id}/toggle-star` toggles the starred state without sending a request body. ## Read document-oriented results The Book endpoint is the entry point for raw credit, tax, application, and identity output. Computed endpoints are available when you want normalized results: | Request | Use it for | | ----------------------------------------- | --------------------------------------------------------------------------------- | | `GET /books/{book_id}/analytics` | Bank metrics, enriched transactions, positions, cash-flow patterns, and screening | | `GET /books/{book_id}/statements` | Account-first bank statement history with daily balances | | `GET /books/{book_id}/tax-analytics` | Qualifying-income analysis across parsed tax years | | `GET /books/{book_id}/csv-export` | Bank-underwriting CSV | | `GET /books/{book_id}/spreadsheet-export` | Template-generated Excel or PDF | For transaction tags, debt positions, and underwriting exclusions, continue with [Underwriting edits](/api-reference/endpoint/underwriting-edits). ## Response conditions | Status | Meaning | | ------ | --------------------------------------------------- | | `401` | Missing or invalid PAT | | `403` | The token cannot access this organization or Book | | `404` | The Book does not exist in the token's organization | # Documents and parsing Source: https://docs.lendpathway.com/api-reference/endpoint/documents-and-parsing Create a Book, upload its files, and control parsing as separate API calls. Use these endpoints when your application needs to control each part of the Book lifecycle. You can create an empty Book, add files over time, inspect the uploaded documents, and start parsing when the package is ready. For a single request that creates, uploads, and starts parsing, use [`POST /submit-book`](/api-reference/endpoint/submit-book). ## Complete manual flow ```python theme={null} import time from pathlib import Path import requests API_BASE = "https://api.lendpathway.com/api" TOKEN = "pat_your_token_here" HEADERS = {"Authorization": f"Bearer {TOKEN}"} # 1. Create the Book. response = requests.post( f"{API_BASE}/books/", headers=HEADERS, json={"name": "Acme Coffee", "description": "March renewal review"}, timeout=30, ) response.raise_for_status() book_id = response.json()["id"] # 2. Upload one or more documents. paths = [Path("january.pdf"), Path("february.pdf")] handles = [path.open("rb") for path in paths] try: response = requests.post( f"{API_BASE}/documents/", headers=HEADERS, params={"book_id": book_id}, files=[("files", (path.name, handle, "application/pdf")) for path, handle in zip(paths, handles)], timeout=120, ) response.raise_for_status() documents = response.json() finally: for handle in handles: handle.close() # 3. Start parsing. response = requests.post( f"{API_BASE}/parse/{book_id}/parse-book", headers=HEADERS, timeout=30, ) response.raise_for_status() parse_job_id = response.json()["parse_job_id"] # 4. Wait for a terminal status. while True: response = requests.get( f"{API_BASE}/books/{book_id}", headers=HEADERS, timeout=30, ) response.raise_for_status() book = response.json() if book["parse_status"] in {"completed", "failed", "cancelled"}: break time.sleep(3) print(book["parse_status"], book.get("parse_status_message")) ``` ## Create an empty Book ```http theme={null} POST /books/ Content-Type: application/json ``` ```json theme={null} { "name": "Acme Coffee", "description": "March renewal review" } ``` The response is the newly created [`Book`](/api-reference/endpoint/books#complete-book-response). Its initial `parse_status` is `new`. ## Upload documents ```http theme={null} POST /documents/?book_id={book_id} Content-Type: multipart/form-data ``` Repeat the `files` form field for every file. A ZIP file is expanded during upload. The response contains every Document currently attached to the Book. ```python Python theme={null} with open("statement.pdf", "rb") as statement: response = requests.post( f"{API_BASE}/documents/", headers=HEADERS, params={"book_id": BOOK_ID}, files={"files": ("statement.pdf", statement, "application/pdf")}, timeout=120, ) response.raise_for_status() print(response.json()) ``` ```javascript JavaScript theme={null} import { openAsBlob } from "node:fs"; const form = new FormData(); form.append("files", await openAsBlob("statement.pdf"), "statement.pdf"); const response = await fetch(`${API_BASE}/documents/?book_id=${BOOK_ID}`, { method: "POST", headers: { Authorization: `Bearer ${TOKEN}` }, body: form, }); if (!response.ok) { throw new Error(`${response.status}: ${await response.text()}`); } console.log(await response.json()); ``` ```bash cURL theme={null} curl --fail-with-body \ -X POST \ "https://api.lendpathway.com/api/documents/?book_id=99cc93e6-1f3f-42b1-9fe4-ba5a95be9c78" \ -H "Authorization: Bearer pat_your_token_here" \ -F "files=@statement.pdf" ``` ```python theme={null} from datetime import datetime from typing import Literal from uuid import UUID from pydantic import BaseModel DocumentType = Literal[ "pending", "bank_statement", "month_to_date", "plaid_asset_report", "loan_application_form", "receipt", "decisionlogic", "credit_report", "tax_form", "ar_report", "photo_id", "voided_check", "unsupported_file", ] DocumentStatus = Literal["pending", "new", "processing", "completed", "failed"] class DocumentMeta(BaseModel): failure_reason: str | None = None class Document(BaseModel): id: UUID org_id: UUID | None = None book_id: UUID | None = None document_type: DocumentType = "pending" document_status: DocumentStatus = "pending" uploaded_file_name: str original_filename: str file_size: int | None = None document_meta: DocumentMeta | None = None is_deleted: bool = False created_by: UUID | None = None created_at: datetime updated_at: datetime presigned_url: str | None = None presigned_url_expires: datetime | None = None mime_type: str | None = None page_count: int | None = None ``` ## Inspect and download documents | Request | Result | | --------------------------------------- | ------------------------------------------------------ | | `GET /documents/?book_id={book_id}` | Every active document attached to the Book | | `GET /documents/{document_id}` | One Document with its latest classification and status | | `GET /documents/{document_id}/download` | A fresh one-hour `download_url` for the source file | Document classification happens during parsing. Before that work runs, `document_type` can be `pending` and `document_status` can be `new`. ## Start parsing ```http theme={null} POST /parse/{book_id}/parse-book ``` ```json theme={null} { "parse_job_id": "d1f05046-b5ba-45a2-8f3b-ae3acc08fd11" } ``` The request atomically moves the Book into `processing` and returns immediately. A Book already processing returns `400`. Organization usage limits can return `403`. Poll either resource: | Request | Use | | ---------------------------------------- | ---------------------------------------------------------------------------------------- | | `GET /books/{book_id}` | Status plus the complete Book. Fetch this when you will read the result after completion | | `GET /parse/{book_id}/parse-book-status` | Lightweight status for one Book | | `GET /parse/books/statuses` | Lightweight status for every Book in the organization | The terminal states are `completed`, `failed`, and `cancelled`. Once completed, choose the response that fits the job: * [`GET /books/{book_id}`](/api-reference/endpoint/books) for the entire stored Book and raw parser results * [`GET /books/{book_id}/analytics`](/api-reference/endpoint/analytics) for complete computed underwriting data * `GET /books/{book_id}/statements` for the smaller account-first bank view ## Stop an active parse ```http theme={null} POST /parse/{book_id}/stop-parse ``` The response is `{"message": "Parse cancelled"}` when a running parse was cancelled, or `{"message": "No active parse"}` when there was nothing to stop. Starting a new parse replaces the Book's existing `book_meta`. Transaction tags, position edits, and exclusions stored with the previous parse are discarded. # Embed Source: https://docs.lendpathway.com/api-reference/endpoint/embed Create a read-only Book URL for your application. The embed flow renders Pathway's Book interface inside your application. The person viewing it does not need a Pathway login or API token. Your server creates a book-scoped `emb_...` token with its PAT. Your frontend places that embed token in an iframe URL. Create embed tokens on your server. The PAT used to create them must never be shipped to the browser. ## Create an embed token ```http theme={null} POST /embed/token Content-Type: application/json ``` UUID of the Book the viewer can access. Temporary tokens expire after 24 hours. Permanent tokens have no scheduled expiration. ```python Python theme={null} import requests API_BASE = "https://api.lendpathway.com/api" APP_BASE = "https://app.lendpathway.com" TOKEN = "pat_your_token_here" BOOK_ID = "99cc93e6-1f3f-42b1-9fe4-ba5a95be9c78" response = requests.post( f"{API_BASE}/embed/token", headers={"Authorization": f"Bearer {TOKEN}"}, json={"book_id": BOOK_ID, "permanent": False}, timeout=30, ) response.raise_for_status() embed = response.json() embed_url = f"{APP_BASE}/embed/book/{embed['embed_token']}?theme=light" print(embed_url) ``` ```javascript JavaScript theme={null} const API_BASE = "https://api.lendpathway.com/api"; const APP_BASE = "https://app.lendpathway.com"; const TOKEN = "pat_your_token_here"; const BOOK_ID = "99cc93e6-1f3f-42b1-9fe4-ba5a95be9c78"; const response = await fetch(`${API_BASE}/embed/token`, { method: "POST", headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ book_id: BOOK_ID, permanent: false }), }); if (!response.ok) { throw new Error(`${response.status}: ${await response.text()}`); } const embed = await response.json(); const embedUrl = `${APP_BASE}/embed/book/${embed.embed_token}?theme=light`; console.log(embedUrl); ``` ```bash cURL theme={null} curl --fail-with-body \ "https://api.lendpathway.com/api/embed/token" \ -H "Authorization: Bearer pat_your_token_here" \ -H "Content-Type: application/json" \ --data '{"book_id":"99cc93e6-1f3f-42b1-9fe4-ba5a95be9c78","permanent":false}' ``` ### Response ```python theme={null} class EmbedTokenResponse(BaseModel): embed_token: str book_id: str expires_at: str | None = None ``` ```json theme={null} { "embed_token": "emb_VWUCi3ml0L05CSkgZNBli4hKs8ObZrzaO9XGRUySJ8A", "book_id": "99cc93e6-1f3f-42b1-9fe4-ba5a95be9c78", "expires_at": "2026-07-22T18:30:00+00:00" } ``` `expires_at` is `null` for a permanent token. Pathway keeps one active embed token per Book. When a valid token already exists, the endpoint returns that token and its current expiration. ## Render the Book ```html theme={null} ``` ### URL parameters Set `light` or `dark`. Set this explicitly when the embed should match the surrounding application. Opens the Book on a specific available tab. Omit it to use the Book's normal default. The embedded Book reuses the same synopsis, transactions, credit, tax, application, file, and contract surfaces as the Pathway app. Available tabs depend on the parsed document types. ## Render the funder directory The same token can render the organization funder directory: ```html theme={null} ``` The directory is read-only under an embed token. ## Fetch the embed payload directly ```http theme={null} GET /embed/book/{embed_token} ``` The embed token in the URL is the credential. Do not send the PAT. ```python theme={null} from typing import Any from pydantic import BaseModel class EmbedBookResponse(BaseModel): book: Book analytics: BookAnalytics | None tagConfig: dict[str, Any] documents: list[Document] ``` ```python theme={null} response = requests.get( f"{API_BASE}/embed/book/{embed_token}", timeout=60, ) response.raise_for_status() payload = response.json() print(payload["book"]["name"]) print(payload["analytics"]) ``` `analytics` is `null` when the Book does not contain canonical bank data. Document objects include temporary presigned URLs used by the embedded viewer. ## Validate a token ```http theme={null} GET /embed/validate/{embed_token} ``` ```python theme={null} class EmbedValidateResponse(BaseModel): valid: bool org_id: str book_id: str ``` An invalid or expired token returns `401`. A valid token returns the Book and organization context used by the iframe. ## Security model * An embed token can read one Book and organization-level read surfaces used by the embed. * Mutating requests made with `X-Embed-Token` return `403`. * Temporary tokens live for 24 hours. * Permanent tokens persist until removed from Pathway's storage. * Anyone holding the embed URL can open it, so handle the URL as a secret share link. Prefer temporary tokens for short sessions. Use permanent tokens when the same embedded report must remain available across visits. # Exports Source: https://docs.lendpathway.com/api-reference/endpoint/exports Download underwriting CSVs and generate files from the spreadsheet gallery. Pathway exposes a stable bank-underwriting CSV and a gallery of generated Excel and PDF templates. Gallery availability can vary by organization, so integrations should discover templates before requesting one by ID. ## List available templates ```http theme={null} GET /parse/spreadsheet-templates ``` ```python theme={null} import requests API_BASE = "https://api.lendpathway.com/api" TOKEN = "pat_your_token_here" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get( f"{API_BASE}/parse/spreadsheet-templates", headers=headers, timeout=30, ) response.raise_for_status() for template in response.json()["templates"]: print(template["id"], template["type"], template["format"]) ``` ```python theme={null} from typing import Literal from pydantic import BaseModel, Field class SpreadsheetTemplate(BaseModel): id: str name: str type: Literal["mca", "credit_report", "tax"] format: Literal["excel", "pdf"] description: str class SpreadsheetTemplatesConfig(BaseModel): templates: list[SpreadsheetTemplate] = Field(default_factory=list) ``` The response excludes private templates that do not belong to the token's organization. ### Public gallery | ID | Name | Data | Format | | ---------------------------------- | ---------------------------- | ----------------------------------------------------------------- | ------ | | `mca_stack_view` | MCA Stack View | Bank statements and detected debt positions | Excel | | `mca_monthly_columns` | MCA Monthly Columns | Monthly deposits, true revenue, DTI, and payment analysis | Excel | | `mca_stacking_pro` | Debt Stacking Drilldown | Advances, schedules, remittance burden, misses, and modifications | Excel | | `mca_sample_bank_statement_report` | Sample Bank Statement Report | Rendered bank statement analysis | PDF | | `vlad_full` | Credit Report Full | Bureau tabs, FICO scores, accounts, utilization, and inquiries | Excel | | `vlad_summary` | Credit Report Summary | Compact score and account summary | Excel | | `vlad_loc_scrub` | Credit Report LOC Scrub | Revolving utilization, unsecured loans, and derogatory history | Excel | | `tax_qualifying_income` | Tax Qualifying Income | Wages, Schedule C, pass-through entities, and qualifying income | Excel | The discovery endpoint is authoritative. New public templates can appear without a client release. ## Download bank analytics as CSV ```http theme={null} GET /books/{book_id}/csv-export ``` `month_as_row` writes one row per statement period and account. `month_as_col` writes metrics as rows and statement periods as columns. ```python Python theme={null} from pathlib import Path import requests url = f"{API_BASE}/books/{BOOK_ID}/csv-export" response = requests.get( url, headers=headers, params={"table_format": "month_as_row"}, timeout=60, ) response.raise_for_status() Path("underwriting.csv").write_bytes(response.content) ``` ```javascript JavaScript theme={null} import { writeFile } from "node:fs/promises"; const url = new URL(`${API_BASE}/books/${BOOK_ID}/csv-export`); url.searchParams.set("table_format", "month_as_row"); const response = await fetch(url, { headers: { Authorization: `Bearer ${TOKEN}` }, }); if (!response.ok) { throw new Error(`${response.status}: ${await response.text()}`); } await writeFile("underwriting.csv", Buffer.from(await response.arrayBuffer())); ``` ```bash cURL theme={null} curl --fail-with-body \ "https://api.lendpathway.com/api/books/{book_id}/csv-export?table_format=month_as_row" \ -H "Authorization: Bearer pat_your_token_here" \ --output underwriting.csv ``` ### `month_as_row` The file contains individual account rows followed by `NET` and `Average` summary rows. ```text theme={null} Period,Document,Account,Starting Balance,Deposits,Deposit Count,Withdrawals,Withdrawal Count,Ending Balance,Loan In,Loan Out,Avg Daily Balance,Days Negative,True Revenue,DTI % Jan 2026,january.pdf,Business Checking,8149.06,101019.45,83,94510.22,106,14658.29,20000.00,12440.00,11281.33,0,81587.45,15.2% NET,,1 Account,8149.06,101019.45,83,94510.22,106,14658.29,20000.00,12440.00,11281.33,0,81587.45,15.2% Average,,1 Account,8149.06,101019.45,83,94510.22,106,14658.29,20000.00,12440.00,11281.33,0,81587.45,15.2% ``` ### `month_as_col` The file uses the combined cash row for each statement period. ```text theme={null} Metric,Jan 2026,Feb 2026,Mar 2026 Starting Balance,8149.06,14658.29,12211.08 Deposits,101019.45,97004.12,108440.90 True Revenue,81587.45,79220.83,90118.70 DTI %,15.2%,13.8%,17.1% ``` Both formats use the same exclusions and analytics configuration as `GET /books/{book_id}/analytics`. ## Generate a gallery export ```http theme={null} GET /books/{book_id}/spreadsheet-export ``` `mca` for bank statement templates, `vlad` for credit report templates, or `tax` for tax templates. A template returned by `GET /parse/spreadsheet-templates`. When omitted, Pathway uses the organization's default for that sheet type. ```python theme={null} from pathlib import Path import requests response = requests.get( f"{API_BASE}/books/{BOOK_ID}/spreadsheet-export", headers=headers, params={ "sheet_type": "mca", "template_id": "mca_stacking_pro", }, timeout=120, ) response.raise_for_status() content_type = response.headers.get("content-type", "") suffix = ".pdf" if "pdf" in content_type else ".xlsx" Path(f"deal{suffix}").write_bytes(response.content) ``` The chosen template must match `sheet_type` and must be visible to the token's organization. Omit `template_id` when you want the file to follow the default selected in Pathway settings. Pass an explicit ID when another system depends on a fixed layout. ## Get a browser-ready export ```http theme={null} GET /books/{book_id}/spreadsheet-embed ``` This endpoint accepts the same `sheet_type` and `template_id` parameters. It generates the file, uploads a temporary copy, and returns URLs suitable for an embedded preview or download. ```python theme={null} class SpreadsheetEmbedResponse(BaseModel): embed_url: str download_url: str drive_file_url: str | None = None drive_embed_url: str | None = None drive_synced_at: str | None = None ``` ```python theme={null} response = requests.get( f"{API_BASE}/books/{BOOK_ID}/spreadsheet-embed", headers=headers, params={"sheet_type": "vlad", "template_id": "vlad_full"}, timeout=120, ) response.raise_for_status() print(response.json()["embed_url"]) ``` `download_url` is temporary. Generate a new response when the old URL expires or when the Book changes. ## Response conditions | Status | Meaning | | ------ | -------------------------------------------------------------------------------------------------- | | `400` | The Book lacks the required parsed data, the sheet type is invalid, or the template does not match | | `401` | Missing or invalid PAT | | `403` | The token cannot access the Book or the requested template | | `404` | The Book or template does not exist | | `500` | The selected template could not be rendered | For multi-file packaging and filename handling, see [Export data](/cookbook/export-data). # Submit a Book Source: https://docs.lendpathway.com/api-reference/endpoint/submit-book Upload a deal package and start parsing in one request. ```http theme={null} POST /submit-book Content-Type: multipart/form-data ``` This endpoint creates a Book, attaches every uploaded file, and starts parsing in the background. The response arrives before parsing finishes. Use one request per deal. Files submitted together are classified and parsed as one package. ## Request One or more financial documents. Send multiple files by repeating the `files` field. A recognizable name for the deal. The parser may later replace it with the extracted business name. Optional context stored on the Book. Optional HTTPS URL that receives the final parse status. Omit it when you plan to poll. PDF is the normal format for bank statements, credit reports, applications, and tax forms. Supported document images can also be classified. Files that cannot be classified remain visible on the Book with a failed or unsupported status. ## Example ```python Python theme={null} from pathlib import Path import requests API_BASE = "https://api.lendpathway.com/api" TOKEN = "pat_your_token_here" paths = [Path("jan.pdf"), Path("feb.pdf"), Path("application.pdf")] with requests.Session() as session: session.headers["Authorization"] = f"Bearer {TOKEN}" opened = [path.open("rb") for path in paths] try: files = [ ("files", (path.name, handle, "application/pdf")) for path, handle in zip(paths, opened) ] response = session.post( f"{API_BASE}/submit-book", files=files, data={ "book_name": "Smith Auto Body", "webhook_url": "https://example.com/webhooks/pathway", }, timeout=120, ) response.raise_for_status() print(response.json()) finally: for handle in opened: handle.close() ``` ```javascript JavaScript theme={null} import { openAsBlob } from "node:fs"; const API_BASE = "https://api.lendpathway.com/api"; const TOKEN = "pat_your_token_here"; const form = new FormData(); form.set("book_name", "Smith Auto Body"); form.set("webhook_url", "https://example.com/webhooks/pathway"); for (const filename of ["jan.pdf", "feb.pdf", "application.pdf"]) { const file = await openAsBlob(filename, { type: "application/pdf" }); form.append("files", file, filename); } const response = await fetch(`${API_BASE}/submit-book`, { method: "POST", headers: { Authorization: `Bearer ${TOKEN}`, }, body: form, }); if (!response.ok) { throw new Error(`${response.status}: ${await response.text()}`); } console.log(await response.json()); ``` ```bash cURL theme={null} curl --fail-with-body \ "https://api.lendpathway.com/api/submit-book" \ -H "Authorization: Bearer pat_your_token_here" \ -F "book_name=Smith Auto Body" \ -F "webhook_url=https://example.com/webhooks/pathway" \ -F "files=@jan.pdf;type=application/pdf" \ -F "files=@feb.pdf;type=application/pdf" \ -F "files=@application.pdf;type=application/pdf" ``` Do not set `Content-Type` manually in Python or JavaScript. The HTTP client adds the multipart boundary when it builds the body. ## Response ```python theme={null} class SubmitBookResponse(BaseModel): book_id: str status: str message: str ``` ```json theme={null} { "book_id": "99cc93e6-1f3f-42b1-9fe4-ba5a95be9c78", "status": "processing", "message": "Processing started" } ``` Save `book_id`. You use it to read status, analytics, exports, and embeds. `status: "processing"` confirms that the asynchronous submission flow was accepted. Continue checking the Book until its stored `parse_status` becomes `completed`, `failed`, or `cancelled`. ## Completion webhook When `webhook_url` is present, Pathway sends one JSON `POST` after the parse reaches its final database status. ```python theme={null} class ParseWebhook(BaseModel): book_id: str status: Literal["completed", "failed", "cancelled"] book_url: str error: str | None = None ``` ```json theme={null} { "book_id": "99cc93e6-1f3f-42b1-9fe4-ba5a95be9c78", "status": "completed", "book_url": "https://app.lendpathway.com/books/99cc93e6-1f3f-42b1-9fe4-ba5a95be9c78" } ``` Return a `2xx` response quickly and move any result fetching into your own background job. Completion webhooks are currently unsigned and are sent once. Treat the Book ID as a lookup key, then call the authenticated Book endpoint to confirm the current status before moving a deal forward. ## Poll instead Call `GET /books/{book_id}` every few seconds and stop on a terminal status. The full implementation, including timeouts and failure handling, is in [Parse documents and get results](/cookbook/parse-and-poll). ## Common failures | Status | Cause | | ------ | -------------------------------------------------- | | `401` | Missing or invalid PAT | | `403` | Read-only token or insufficient access | | `422` | Required multipart fields are missing or malformed | | `500` | Pathway could not stage the Book or its documents | Individual file failures and usage-limit checks occur after this request returns. Read the Book and its documents for the final outcome. A usage limit moves the Book to `failed` and places the reason in `parse_status_message`. # Underwriting edits Source: https://docs.lendpathway.com/api-reference/endpoint/underwriting-edits Update transaction tags, debt positions, funder matches, and analytics exclusions. A read-write PAT can make the same stored underwriting edits available in the Pathway app. These routes update the canonical bank result inside the Book. The next `GET /books/{book_id}/analytics` call calculates from the new state immediately. Fetch the current analytics after a mutation when your application needs the resulting true revenue, debt metrics, schedules, or screening decision. ## Update transaction tags Get the current tag registry from: ```http theme={null} GET /parse/tags ``` Then apply additions, removals, and replacements to any set of transaction IDs. ```http theme={null} PATCH /books/{book_id}/transactions-v2/tags Content-Type: application/json ``` ```python Python theme={null} response = requests.patch( f"{API_BASE}/books/{BOOK_ID}/transactions-v2/tags", headers={"Authorization": f"Bearer {TOKEN}"}, json={ "transaction_ids": [1021, 1022], "operation": { "add": ["payment_processor"], "remove": ["internal_transfer"], "replace": [], }, }, timeout=30, ) response.raise_for_status() print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch( `${API_BASE}/books/${BOOK_ID}/transactions-v2/tags`, { method: "PATCH", headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ transaction_ids: [1021, 1022], operation: { add: ["payment_processor"], remove: ["internal_transfer"], replace: [], }, }), }, ); if (!response.ok) { throw new Error(`${response.status}: ${await response.text()}`); } console.log(await response.json()); ``` ```bash cURL theme={null} curl --fail-with-body \ -X PATCH \ "https://api.lendpathway.com/api/books/{book_id}/transactions-v2/tags" \ -H "Authorization: Bearer pat_your_token_here" \ -H "Content-Type: application/json" \ --data '{ "transaction_ids": [1021, 1022], "operation": { "add": ["payment_processor"], "remove": ["internal_transfer"], "replace": [] } }' ``` ```json theme={null} { "message": "Tags updated", "transactions_updated": 2 } ``` Invalid values in `add` are ignored. A replacement only removes `old_tag` when `new_tag` is valid. Use the registry response instead of hard-coding the available set. ## Manage debt positions A stored position groups transactions that belong to one debt relationship. Analytics enrich that stored group with payment schedules, episodes, remittance burden, misses, modifications, and status. | Operation | Request | | ------------------------------- | ---------------------------------------------------------- | | Create a position | `POST /books/{book_id}/positions` | | Assign or unassign transactions | `PATCH /books/{book_id}/transactions-v2/position` | | Merge positions | `POST /books/{book_id}/positions/merge` | | Match or clear a funder | `PATCH /books/{book_id}/positions/{position_id}/funder` | | Change the loan type | `PATCH /books/{book_id}/positions/{position_id}/loan-type` | | Delete positions | `DELETE /books/{book_id}/positions` | ### Create and populate a position ```python theme={null} # Create the stored position. response = requests.post( f"{API_BASE}/books/{BOOK_ID}/positions", headers={"Authorization": f"Bearer {TOKEN}"}, json={ "name": "OnDeck", "loan_type": "merchant_cash_advance", "funder_id": "a6f59ae3-f423-4acc-841d-1c73297f35e2", }, timeout=30, ) response.raise_for_status() # Read the Book to obtain the new position_id. book = requests.get( f"{API_BASE}/books/{BOOK_ID}", headers={"Authorization": f"Bearer {TOKEN}"}, timeout=30, ).json() meta = book["book_meta"] plaid = meta.get("parser_v2_plaid_result") or {} mca = plaid.get("holy_mca") or meta["parser_v2_mca_result"] position_id = next(p["position_id"] for p in mca["positions"] if p["name"] == "OnDeck") # Attach the funding and payment transactions. response = requests.patch( f"{API_BASE}/books/{BOOK_ID}/transactions-v2/position", headers={"Authorization": f"Bearer {TOKEN}"}, json={"position_id": position_id, "transaction_ids": [118, 142, 165]}, timeout=30, ) response.raise_for_status() ``` Send `position_id: null` to the assignment endpoint to remove the listed transactions from every position. ```python theme={null} from pydantic import BaseModel, Field class CreatePosition(BaseModel): name: str loan_type: str = "merchant_cash_advance" funder_id: str | None = None class PositionAssignment(BaseModel): transaction_ids: list[int] position_id: str | None = None class MergePositions(BaseModel): position_ids: list[str] merged_name: str class UpdatePositionFunder(BaseModel): funder_id: str | None = None class MovePositionLoanType(BaseModel): new_loan_type: str class DeletePositions(BaseModel): position_ids: list[str] remove_tag: bool = True ``` Changing a position's loan type also replaces the old loan tag on its assigned transactions. Deleting with `remove_tag: true` removes that position's loan tag from its transactions. ## Control analytics exclusions ```http theme={null} PATCH /books/{book_id}/exclusions Content-Type: application/json ``` Each supplied field replaces the complete stored list for that field. Omitted fields remain unchanged. Send an empty list to clear one. ```python theme={null} response = requests.patch( f"{API_BASE}/books/{BOOK_ID}/exclusions", headers={"Authorization": f"Bearer {TOKEN}"}, json={ "excluded_document_ids": ["edb70371-5d2c-45f7-b3c1-c19cbf97d5e8"], "excluded_account_ids": [], "excluded_position_ids": ["74ce93ee-0127-4831-95ed-810333e41e34"], "revenue_exclusion_tags": ["internal_transfer", "merchant_cash_advance"], }, timeout=30, ) response.raise_for_status() analytics = requests.get( f"{API_BASE}/books/{BOOK_ID}/analytics", headers={"Authorization": f"Bearer {TOKEN}"}, timeout=60, ) analytics.raise_for_status() print(analytics.json()["true_revenue"]) ``` ```python theme={null} class UpdateExclusions(BaseModel): excluded_document_ids: list[str] | None = None excluded_account_ids: list[int] | None = None excluded_position_ids: list[str] | None = None revenue_exclusion_tags: list[str] | None = None class ExclusionsAck(BaseModel): message: str excluded_document_ids: list[str] excluded_account_ids: list[int] excluded_position_ids: list[str] revenue_exclusion_tags: list[str] | None = None ``` A reparse replaces `book_meta`, including transaction tags, stored positions, and these exclusion lists. Apply edits after the parse you intend to keep. # API overview Source: https://docs.lendpathway.com/api-reference/introduction Use the full Pathway product from your own application or agent. The Pathway API can manage the full underwriting workspace. Create Books, upload documents, run parses, read and edit extracted data, calculate analytics, apply screening rules, match funders, generate exports, and connect deal data to the rest of your system. Pathway's web app uses this API. The built-in chat agent also uses it to work across Books, documents, analytics, funders, screening settings, intake email, and organization data. The web app authenticates with a signed-in user session. Your application or agent authenticates with a Personal Access Token and receives the same organization-scoped access allowed by that user and token. ```text Copy into your AI agent wrap theme={null} Read the Pathway API documentation: https://docs.lendpathway.com/api-reference/introduction.md Use the live OpenAPI schema for exact API details: https://api.lendpathway.com/openapi.json ``` ## What the API covers | Surface | What you can do | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | Books and documents | Create and manage deals, upload files, inspect source documents, and track where each deal came from | | Parsing and extracted data | Start or stop a parse, follow its status, read bank statements, credit reports, tax forms, applications, IDs, and checks | | Underwriting | Work with transactions, account history, debt positions, cash-flow analytics, screening results, and exports | | Workspace | Read and manage funders, matching rules, inbox data, organization settings, CRM records, and connected product integrations | The [live OpenAPI explorer](https://api.lendpathway.com/docs) contains every route and generated request schema. This guide explains the main objects and gives you complete working paths through the product. ## Base URL All API requests begin with: ```text theme={null} https://api.lendpathway.com/api ``` Keep the final `/api`. Endpoint paths in this reference begin with `/`, so the authentication check resolves to: ```text theme={null} https://api.lendpathway.com/api/auth/me ``` ## Create an API key Create a Personal Access Token in **Settings → Account → API Access Tokens**. Tokens begin with `pat_` and are shown once. Use a read-write token for uploads, parsing, edits, and other mutations. A read-only token can safely power an agent, reporting job, or data warehouse sync. The UI calls this an API Access Token. The credential is a Personal Access Token, or PAT. It is the API key used in every example here. ## Make your first request Call `GET /auth/me` before doing any work. It returns the user and organization attached to the token, confirming the host, final `/api`, authorization header, and organization scope in one request. ```python Python theme={null} import requests API_BASE = "https://api.lendpathway.com/api" TOKEN = "pat_your_token_here" response = requests.get( f"{API_BASE}/auth/me", headers={"Authorization": f"Bearer {TOKEN}"}, timeout=30, ) response.raise_for_status() print(response.json()) ``` ```javascript JavaScript theme={null} const API_BASE = "https://api.lendpathway.com/api"; const TOKEN = "pat_your_token_here"; const response = await fetch(`${API_BASE}/auth/me`, { headers: { Authorization: `Bearer ${TOKEN}`, }, }); if (!response.ok) { throw new Error(`${response.status}: ${await response.text()}`); } console.log(await response.json()); ``` ```bash cURL theme={null} curl --fail-with-body \ "https://api.lendpathway.com/api/auth/me" \ -H "Authorization: Bearer pat_your_token_here" ``` **Response** ```json theme={null} { "org_id": "c4f9dda9-7875-4115-961f-0ac4b9630526", "org_name": "Acme Funding", "user_id": "fa576914-9590-40af-bbb8-c3af6f500859", "user_name": "John Doe" } ``` ## Get one deal through the API `POST /submit-book` is the shortest path from files to a parsed Book. It creates the Book, uploads every file in the multipart request, and starts parsing. This Python example waits for completion and reads the simple account-first bank statement result. ```python theme={null} import time from pathlib import Path import requests API_BASE = "https://api.lendpathway.com/api" TOKEN = "pat_your_token_here" HEADERS = {"Authorization": f"Bearer {TOKEN}"} paths = [Path("january.pdf"), Path("february.pdf")] handles = [path.open("rb") for path in paths] try: response = requests.post( f"{API_BASE}/submit-book", headers=HEADERS, data={"book_name": "Acme Coffee"}, files=[("files", (path.name, handle, "application/pdf")) for path, handle in zip(paths, handles)], timeout=120, ) response.raise_for_status() book_id = response.json()["book_id"] finally: for handle in handles: handle.close() while True: response = requests.get( f"{API_BASE}/books/{book_id}", headers=HEADERS, timeout=30, ) response.raise_for_status() book = response.json() if book["parse_status"] in {"completed", "failed", "cancelled"}: break time.sleep(3) if book["parse_status"] != "completed": raise RuntimeError(book.get("parse_status_message") or "Parse did not complete") response = requests.get( f"{API_BASE}/books/{book_id}/statements", headers=HEADERS, timeout=60, ) response.raise_for_status() for account in response.json(): print(account["bank_name"], account["account_number"]) for statement in account["statements"]: print(statement["statement_end_date"], statement["ending_balance"]) ``` Use [Documents and parsing](/api-reference/endpoint/documents-and-parsing) when your application needs to control Book creation, file uploads, and parsing separately. The complete [parse and retrieve cookbook](/cookbook/parse-and-poll) includes Python and webhook flows. ## Understand the result There are four useful views of a parsed bank-statement Book. They serve different jobs. | Request or field | Shape | Best use | | --------------------------------------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------------- | | `GET /books/{book_id}` | Complete Book, including `book_meta` | Status, provenance, user edits, and every raw parser result | | Canonical MCA result inside `book_meta` | Raw business, owners, accounts, statements, transactions, and stored positions | Exact extracted data and transaction-level work | | `GET /books/{book_id}/analytics` | Full computed `BookAnalytics` | Underwriting metrics, enriched positions, cash-flow patterns, and screening | | `GET /books/{book_id}/statements` | Compact account-first statement history | A simple account and balance view with very little transformation code | The [Books reference](/api-reference/endpoint/books) defines the complete Book and canonical raw MCA models. The [Analytics reference](/api-reference/endpoint/analytics) defines the full computed response, including its nested transaction, position, and screening models. ## Main references Upload a complete deal package and start parsing in one request. Create a Book, upload documents, and control parsing as separate calls. Read status, provenance, raw parser output, and the canonical bank result. Read computed bank metrics, transactions, debt positions, and screening results. Download CSV, choose an Excel template, or fetch an embeddable spreadsheet URL. Render one read-only Book inside your application. ## Request conventions * Send the PAT in `Authorization: Bearer `. * Send JSON for normal request bodies and multipart form data for file uploads. * Dates are ISO strings. Parser dates use `YYYY-MM-DD`. Timestamps include a timezone. * Currency values are JSON numbers in dollars. * JSON endpoints return the response body directly, without an outer `data` envelope. * Error responses use `{"detail": "..."}`. Validation failures return a structured list in `detail`. PAT traffic is limited to 30 requests per minute per token. A rate-limited request returns `429`. Parsing runs asynchronously, so submission and parse-start requests return before the work is complete. # Product API map Source: https://docs.lendpathway.com/api-reference/product-map The main route families behind the Pathway application. Pathway's application and built-in agent use the same product API described here. A Personal Access Token is scoped to its user and organization. Read-only tokens can inspect the workspace. Read-write tokens can use the mutations allowed by that user's role. This page is the wide map. The rest of the guide goes deeper on the underwriting paths most integrations need. Use the [live OpenAPI explorer](https://api.lendpathway.com/docs) for every operation and generated schema within a family. ## Deals and documents | Route family | Product surface | | -------------- | --------------------------------------------------------------------------------------- | | `/books/` | Books, status, metadata, provenance, tags, raw parser results, and Book-level mutations | | `/documents/` | Uploaded documents, classifications, source-file downloads, and bulk downloads | | `/parse/` | Parse start, stop, status, supported document types, tags, and spreadsheet templates | | `/parse-jobs` | Parse history and job-level status across the organization | | `/submit-book` | Public integration shortcut that creates, uploads, and starts parsing | ## Underwriting data | Route | Result | | ------------------------------------- | ---------------------------------------------- | | `/books/{book_id}/analytics` | Complete computed bank-statement analytics | | `/books/{book_id}/statements` | Compact account-first bank history | | `/books/{book_id}/tax-analytics` | Tax cash-flow and qualifying-income analysis | | `/books/{book_id}/csv-export` | Underwriting summary CSV | | `/books/{book_id}/spreadsheet-export` | Template-generated Excel or PDF | | `/books/{book_id}/spreadsheet-embed` | Temporary URL for viewing a generated workbook | Transaction tags, debt positions, exclusions, screening, and web research are also managed through Book routes. Those mutations feed the next analytics request, which always calculates from the Book's current stored state and organization settings. ## Underwriting configuration | Route family | Product surface | | ------------------------- | ------------------------------------------------------------------------------- | | `/screening/` | Screening settings, thresholds, and available rule fields | | `/funders` | Funder directory and transaction-description aliases used for position matching | | `/funding-partners` | Downstream funding relationships and deal matching | | `/financial-institutions` | Financial-institution registry | | `/tag-rules/` | Organization transaction-tag rules and rule previews | | `/parser-settings` | Organization parser behavior | | `/spreadsheet-defaults` | Organization export-template defaults | ## Intake and organization data | Route family | Product surface | | --------------------- | ---------------------------------------------------------------------------------- | | `/intake-inbox/` | Intake settings, email threads, messages, attachments, originators, and forwarders | | `/email-originators/` | Managed sender records | | `/originator-groups/` | Sender groups and their settings | | `/orgs/` | Organization metadata, members, invitations, and role management | | `/usage` | Current organization usage | | `/auth/me` | The user and organization attached to the current PAT | ## CRM and connected systems | Route family | Product surface | | --------------------- | ----------------------------------------------------------------------- | | `/crm/` | Borrowers, contacts, deals, deal stages, events, and Book relationships | | `/contracts` | Contract templates and generated contract data | | `/salesforce/` | Salesforce connection, schema, queries, and synchronization | | `/lendsaas/` | LendSaaS connection and lead workflows | | `/google-drive/` | Google Drive connection and file workflows | | `/google-inbox/` | Connected Gmail workflows | | `/slack-integration/` | Slack connection and notifications | | `/chats` | Pathway chat history, Book-linked chats, and message search | Some connection setup routes require an interactive user session because they complete OAuth or an administrator action. PAT access is enforced per route, token mode, organization membership, and user role. A `403` means the credential is valid and lacks permission for that operation. # Bank Statements & ISO App Source: https://docs.lendpathway.com/bank-statements What you see after parsing bank statements and loan applications. > When you parse a book consisting of bank statments and (optionally) a loan application you will be presented with an output consisting of the following tabs # Synopsis Tab The Synopsis tab is the main view after parsing bank statements. It's a single scrollable page made up of **draggable, reorderable sections** (you can grab the grip icon on the left edge of any section and drag it to rearrange the layout). A sticky sidebar navigation appears on the right to jump between sections. There are also **info badges** that sit in the top-right corner of the page, next to the tab bar. These are covered at the end of this section. Screenshot2026 03 02at3 30 07PM Screenshot2026 03 02at3 30 07PM *** ### **Debt Summary** At the top of the Synopsis. Shows total **funded** and **paid** amounts across all detected debt positions. Positions are organized into columns by **loan type** (e.g. "Merchant Cash Advance", "Lease"). Each column shows total funded/paid for that type, and contains **position cards**: one per detected lender/funder. Each position card shows: * Funder name * Amount funded (green) and amount paid (red) * Active/Inactive badge * Last funded and last paid dates * Payment pattern summary (e.g. "33 weekly (Wed) \$2,206.00, 30 daily \$450.00") You can: * **Click a position card** to see all its transactions in a modal * **Click a loan type header** to see all transactions for that loan type * **Drag positions** between loan type sections to reclassify them * **+ Add** new positions manually * **Select multiple positions** to merge or delete them * **Exclude/include** individual positions or entire loan types from revenue calculations using the eye icon () * **Sort** positions by most recent payment, most recent funding, largest funded, etc. * Toggle between **expanded** (detailed cards ) and **sparse** (compact ) view Screenshot 2026 02 26 At 6 03 57 PM Screenshot 2026 02 26 At 6 03 57 PM *** ### **Metrics** A grid of clickable metric cards showing aggregate numbers across all statements. **Clicking any card** opens a modal with the underlying transactions for that metric. **Row 1 (primary):** | Card | Shows | | :-------------- | :---------------------------------------------- | | **Deposits** | Total deposit amount, number of transactions | | **Withdrawals** | Total withdrawal amount, number of transactions | | **Loan In** | Total loan-in (disbursements) amount and count | | **Loan Out** | Total loan-out (payments) amount and count | **Row 2:** | Card | Shows | | :-------------------- | :---------------------------------------------------------------- | | **Avg Daily Balance** | Average daily balance (click to see daily balance chart) | | **Days Negative** | Number of days the balance was negative | | **True Revenue** | Revenue after excluding loan activity (click to see transactions) | | **Excluded Revenue** | Revenue excluded from True Revenue calculation | The True Revenue card also has an **abacus icon** () in the top right that opens the **True Revenue Configuration** modal, where you can choose which loan types are excluded from revenue. *(see screenshot below)* **Row 3 (flagged activity):** | Card | Shows | | :--------------------- | :----------------------------------- | | **NSF** | Non-sufficient funds count and total | | **Overdraft** | Overdraft count and total | | **Owner Transactions** | Owner draw/deposit count and total | | **Internal Transfers** | Internal transfer count and total | **Row 4:** | Card | Shows | | :-------------------- | :--------------------------------- | | **Bank Fees** | Bank fee count and total | | **Payment Processor** | Payment processor volume and count | | **Stop Payments** | Stop payment count and total | | **Reversals** | Reversal count and total | Hero Dark Hero Dark Screenshot2026 02 26at6 16 30PM Screenshot2026 02 26at6 16 30PM *** ### **Bank Statements** A table with **one row per statement period per account**. Shows the month-by-month breakdown plus an **Average** row at the bottom. **Default visible columns:** Period, Acct, Start, Deposits, Withdrawals, End, Loan In, Loan Out, Avg Daily, Neg, Revenue, DTI, Status (reconciliation check mark). **Additional columns** (toggle via the column picker gear icon): Days, Discrepancy, NSF, Overdraft, Owner Txn, Internal Xfer, Bank Fees, Payment Processor, Stop Payments, Reversals. You can: * **Sort** by period (newest or oldest first) * **Toggle between Rows and Pivoted** view (icon toggle in top-right) * **Export CSV** of the table * **Click a row** to open the corresponding document in the side panel * **Expand a row** to see a per-account breakdown (if multiple accounts exist) Screenshot2026 02 26at8 10 55PM Screenshot2026 02 26at8 10 55PM Create Org Dark Create Org Dark *** ### **Book Summary** Three-column summary card: | Balance Flow | Transaction Insights | Activity Summary | | :-------------- | :------------------- | :------------------------ | | Opening balance | Largest Deposit | Total Deposits (count) | | Closing balance | Avg Deposit | Total Withdrawals (count) | | Peak Balance | Largest Withdrawal | Avg Transaction | | Lowest Balance | Avg Withdrawal | Daily Avg Txns | This section is display only: no clickable elements. Screenshot2026 02 26at8 14 40PM Screenshot2026 02 26at8 14 40PM *** ### **Top Counterparties** Shows the most significant counterparties (entities you transact with), split into **Credits** and **Debits** columns. Each counterparty shows name, total dollar amount, and transaction count. You can: * Toggle between **grid** and **list** view * **Click a counterparty** to open a modal with all transactions for that counterparty Screenshot2026 02 26at8 16 33PM 1 Screenshot2026 02 26at8 16 33PM 1 *** ### **Cash Flow by Day of Week** Bar chart showing deposit and withdrawal activity broken down by day of week (Mon–Sun). Credits on the left, Debits on the right. You can toggle between **Amount** and **Count** using the dropdown. Screenshot2026 02 26at8 16 33PM Screenshot2026 02 26at8 16 33PM *** ### **Daily Balance Calendar** A month-by-month calendar view showing the **end-of-day balance** for each day. Navigate between months using the left/right arrows. Bank holidays are labeled (e.g. "New Year's Day"). MCA disbursement & repayment events will be labeled on days they occur. Screenshot2026 02 26at8 23 05PM Screenshot2026 02 26at8 23 05PM *** ### **AI Deep Research** An AI-generated report that cross-references the extracted business identity (name, address, phone) against web sources to verify legitimacy. Shows match/mismatch findings for each identifier. Screenshot2026 02 26at8 24 58PM Screenshot2026 02 26at8 24 58PM *** ### **Tampering Analysis** Analyzes the uploaded PDF files for signs of fabrication or tampering. Shows: * A **risk score** (e.g. "6/7 - High Risk") in the top-right * A written summary of findings (e.g. identical metadata, programmatic PDF generation signals, temporal impossibilities) * **PDF Metadata** section listing each document with expandable details (producer, creator, creation dates) Screenshot2026 02 26at8 28 54PM Screenshot2026 02 26at8 28 54PM *** ### **Loan Application** Only appears if a **loan application** was uploaded alongside the bank statements. Displays extracted fields in three groups: * **Business Information**: Business Name, DBA, EIN, Entity Type, Industry, Business Started, Employees, Address, Phone, Email, Website * **Financial Information**: Requested Amount, Loan Purpose, Annual Revenue, Monthly Revenue, Current MCA Balance, Avg Daily Balance, Monthly CC Sales * **Owner Information**: Owner Name, Ownership %, Owner SSN, Owner Address, Credit Score Screenshot2026 02 26at8 30 48PM Screenshot2026 02 26at8 30 48PM *** ### **Info Badges (top-right corner)** These popover badges sit next to the tab bar and are always visible on the Synopsis page: **1. Identity (****)** — Business name, tax ID, address, phone. Principal(s) with name, role, address, phone. Screenshot2026 02 26at8 41 39PM 1 Screenshot2026 02 26at8 41 39PM 1 **2. Accounts** **(****)** — Number of bank accounts detected. Account name, bank name. Eye icon to exclude/include an account from analytics. Expandable "Full details" for account and routing numbers. Screenshot2026 02 26at8 46 32PM Screenshot2026 02 26at8 46 32PM **3. Documents (****)** — Count of documents and reconciliation status (e.g. "12 Documents - All Reconciled"). Each document listed with period, filename, status, and eye icon to exclude/include from analytics. Screenshot2026 02 26at8 50 20PM Screenshot2026 02 26at8 50 20PM **4. Pass / Autodeny** **(****)**— Screening result badge (green "Pass" or red "Autodeny"). Shows reason and target. Based on auto-screening org settings. See the Autodeny Section for more information Screenshot2026 02 26at8 50 20PM 1 Screenshot2026 02 26at8 50 20PM 1 **5. Salesforce** *(if linked)* — Push-to-Salesforce button with mapped field preview. > See the Integrations page for more information # Transactions Tab > The Transactions tab shows every individual transaction extracted from the bank statements in a searchable, filterable, sortable table. Screenshot2026 02 27at12 00 15AM Screenshot2026 02 27at12 00 15AM *** ### **Table Columns** Each row is a single transaction. The columns are: | Column | What it shows | | :-------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Checkbox** | Select individual transactions (or select all/ multiple with the header checkbox) | | **Date** | Transaction date (e.g. "Jan 15, 2024") | | **Account** | Account name (shown when multiple accounts exist; hidden by default for single-account books) | | **Description** | Full transaction description — search matches are highlighted | | **Amount** | Dollar amount, color-coded: green for credits, red (with minus sign) for debits | | **Balance** | Running ledger balance after this transaction (red if negative) | | **Tags** | Color-coded badges showing the tags assigned to this transaction (e.g. "Merchant Cash Advance", "NSF", "Owner Draw"). Loan-type tags appear first, followed by other tags. | | **Position** | If the transaction is assigned to a debt position, shows the funder name (with favicon if available) | | **Doc ID** | First 8 characters of the source document ID (hidden by default) | **Clicking a row** opens the source PDF document in the side panel so you can see exactly where the transaction came from. Screenshot2026 02 27at12 09 45AM Screenshot2026 02 27at12 09 45AM *** ### **Toolbar (Filtering & Search)** The toolbar sits above the table and lets you narrow down transactions: | Filter | What it does | | :------------ | :------------------------------------------------------------------------------------------------ | | **Search** | Free-text filter on the Description column (type to search, e.g. "stripe" or "prime funding") | | **Date** | Date range picker — only show transactions within a specific window | | **Type** | Filter by Credits or Debits | | **Account** | Filter by account (only appears when there are multiple bank accounts) | | **Tags** | Filter by one or more tags — shows a list of all tags present in the data with transaction counts | | **Positions** | Filter by debt position — shows all detected positions with transaction counts | Filters stack — you can combine date + type + tag + search at the same time. Screenshot2026 02 27at12 11 34AM Screenshot2026 02 27at12 11 34AM *** ### **Actions (Right Side of Toolbar)** Three buttons appear on the right side of the toolbar: **1. Edit Tags (****)** — Select one or more transactions using the checkboxes, then click **Edit Tags**. This opens a modal titled **"Edit Tags (****)"** with three sections: **Replace Current Tags** Lists every unique tag across your selected transactions with a count (e.g. "Merchant Cash Advance (12)"). For each tag you can: * Click the **arrow slot** to replace it with a different tag (searchable dropdown, grouped into "Loan Types" and "Other Tags") * Click the **trash icon** to remove it entirely **Add Tags** A multi-select dropdown to add one or more new tags to all selected transactions. Tags are searchable. **Assign to Position** A dropdown listing all detected debt positions. Select a position to assign all selected transactions to it, or choose **"Unassign from all positions"** to remove the position assignment. Click **Save Changes** to apply. All changes (tag replacements, additions, removals, and position reassignment) are saved in one action. Screenshot2026 02 27at12 16 29AM Screenshot2026 02 27at12 16 29AM Screenshot2026 02 27at12 19 13AM Screenshot2026 02 27at12 19 13AM **2. Summary (****)** — opens the Filtered Transactions Summary modal showing a quick breakdown of whatever transactions are currently visible (after filters). Displays: * **Total Credits** — sum and count * **Total Debits** — sum and count * **Net Amount** — credits minus debits * **Date Range** — first to last date and number of days The number in the button reflects how many transactions match your current filters. **3. Download (****)** — exports the currently filtered transactions as a **CSV** file with columns: Date, Description, Type, Amount, Balance, Tags, Account, and Document ID. The number shows how many rows will be exported. Screenshot2026 02 27at12 31 26AM Screenshot2026 02 27at12 31 26AM *** ### **Tag Reference (Global)** In the **top-right corner of the app header** (visible on every page, not just Transactions), there's a **tag icon** button. Clicking it opens the **Tag Reference** modal — a full gallery of every tag in your org's configuration. Each tag card shows: * **Tag name** with its color * **Description** — what the tag means and the rules for when it's applied * **Examples** — sample transaction descriptions that would receive this tag You can **search** tags by name, description, or example text. Tags are organized with loan types first, then other tags. This is useful when you're reviewing transactions and want to understand what a specific tag means or why a transaction was tagged a certain way. Screenshot2026 02 27at12 39 49AM Screenshot2026 02 27at12 39 49AM # Bank Spreadsheet Tab > The Bank Spreadsheet tab shows an auto-generated Excel workbook built from the parsed bank statement data. It opens directly in the browser — no download required. This tab only appears if your org has the Bank Spreadsheet enabled in settings. *** ### **Template Selector** A **template selector** will appear in the floating toolbar at the bottom of the screen. Click it to switch between templates. For bank statements, the available templates are: **1. MCA Stack View** — One row per payment schedule across all MCA positions: funder, amount funded, frequency, number of payments, returns, remit daily, remittance %, and active/renewal status. Hero Dark Hero Dark **2. Monthly Columns** — Monthly deposit totals, true revenue, DTI metrics, ending balance, NSF, negative days, CC deposits, number of credits, average ledger balance — broken out by month. Also includes business identity fields (legal name, DBA, address, Tax ID, owner name), an MCA Stacking Analysis section, and an Offer Calculator. Screenshot2026 03 02at1 12 20AM 2 Screenshot2026 03 02at1 12 20AM 2 When you select a template, it becomes your **default** for bank spreadsheets going forward. The spreadsheet regenerates automatically with the new layout. *** ### **Toolbar Actions** The floating toolbar sits at the bottom center of the spreadsheet view and has three controls: **1. Template selector** — Switch between spreadsheet layouts (described above). **2. Download** — Downloads the spreadsheet as an **.xlsx** file to your computer. Ready to send to a funder or attach to a file. **3. Open in Drive** — Opens the spreadsheet in **Google Sheets** in a new tab. Requires the Google Drive integration to be connected (see Integrations). Creates a copy in your Google Drive that you can edit, share, or collaborate on. Screenshot2026 03 02at1 22 01AM Screenshot2026 03 02at1 22 01AM # Files Tab > The Files tab shows all uploaded documents in a visual grid with thumbnails. This is where you manage, preview, download, and exclude/include individual files. The Files tab is always available, regardless of what document types were uploaded. Screenshot2026 03 02at1 33 45AM Screenshot2026 03 02at1 33 45AM *** ### **Document Cards** Each file is a card with a **thumbnail preview**, **filename**, **status dot** (green = parsed, red = failed), **file size**, and **upload time**. * **Click** to select (for bulk actions). **Shift+click** to select a range. * **Double-click** to open a full PDF preview modal. Once parsed, files are automatically **grouped by category** (Bank Statements, Credit Reports, Tax Forms, Loan Applications, etc.) with counts. *** ### **Toolbar** **1. Search** — Filter by filename. **2. File Type** — Filter by extension (PDF, XLSX, etc.). Only shows when multiple types exist. **3. Category** — Filter by document type (Bank Statement, Credit Report, etc.). Only shows after classification. **4. Status** — Filter by Parsed, Failed, Processing, New. *** ### **Selecting & Bulk Actions** Select one or more cards and a toolbar appears: **1. Download** — Two options: plain zip or **watermarked** zip (applies a watermark to each file). **2. Show / Hide** — Exclude or re-include selected documents from analytics. Excluded documents appear dimmed with an X overlay. Same as toggling the eye icon from the Documents info badge. **3. Delete** — Remove selected files from the book. Press **Escape** to clear selection. Screenshot2026 03 02at1 38 40AM Screenshot2026 03 02at1 38 40AM # Changelog Source: https://docs.lendpathway.com/changelog/changelog Everything we ship, week by week. ## March 30 – April 1, 2026 latest ### Screening rules Screening rules now evaluate as an equation: **metric → operator → value**. You pick a metric on the left, an operator in the middle (greater than, less than, etc.), and a value on the right. If the condition is true, the application is rejected. The right side can be a fixed number or another metric. Click "Use metric instead" to switch. When comparing two metrics, a multiplier is applied to the right side. For example: avg daily balance \< 0.3 × avg monthly revenue — this rejects any book where the average daily balance is less than 30% of average monthly revenue. When a book is screened, every rule is evaluated individually. Each one shows whether it passed or failed, the equation that was checked, and the actual numbers that were plugged in. The overall result is pass only if every rule passes. Screening is now computed live — updating a rule takes effect immediately on all books without re-parsing. ### Email inbox reply control New "Send Replies" toggle in inbox settings. When disabled, incoming emails are still parsed and screened, but no completion or failure emails are sent back. *** ## March 16–20, 2026 ### Email synopsis Rebuilt the completion email to mirror the synopsis page. Overview with business info and account chips, cash flow cards (avg monthly revenue, avg daily balance, true revenue, days negative, NSF, overdraft), debt positions table with funded/paid totals, monthly bank statement breakdown with negative days and DTI, book summary, and a web research preview. All metrics respect exclusions. Preview at `/api/books/{id}/email-preview`. *** ## March 9–13, 2026 ### Screening rules Screening rules let you set conditions against the metrics the parser extracts. They run on every scrub — the deal passes or gets flagged with the exact reason. Supported metrics: avg daily balance, negative days, true revenue, avg monthly revenue, avg monthly deposits, total loan payments, DTI ratio, closing balance, opening balance, total days, lowest balance, deposit count, withdrawal count, true revenue transaction count, MCA position count, state code. Operators: greater than, less than, equal, not equal, greater or equal, less or equal. Rules can target all states or specific states. A deal is rejected if any rule's condition is met. Changing a rule re-screens every book the org has ever parsed — the metrics were already stored from parsing. **Import from document.** Drag in a PDF, spreadsheet, screenshot, or Google Doc containing a funder's criteria. A constrained agent reads the document, maps requirements onto the supported metrics and operators, and inverts each requirement into its denial form ("minimum \$25k monthly revenue" becomes `avg_monthly_revenue < 25000`). Anything that doesn't map to a real metric is dropped — structurally impossible to express. You review checkboxes and approve what applies. ### Credit report parser * **Authorized user detection.** The parser now only sets `is_authorized_user=true` when the report explicitly says "Authorized User" or "AU." Joint, co-signer, and individual accounts are no longer misclassified. * **Collateral type on revolving accounts.** `collateral_type=unsecured` is now only assigned to installment personal/term loans. Credit lines, LOCs, and HELOCs get `None` collateral, keeping them out of the Unsecured Loans section. * **Last Reported date accuracy.** Field description clarified to point at the "Last Reported" / "Date Reported" label specifically, reducing misreads. * **Creditor name extraction.** The extraction prompt now uses realistic creditor name examples (`SYNCB/AMAZON`, `BK OF AMER`, `THD/CBNA`) and explicitly tells the parser that abbreviated/slash-delimited names are valid accounts. * **Fraud alert and security freeze detection.** New optional fields `has_fraud_alert` and `has_security_freeze` on the entity model. Existing templates are unaffected. * **Bureau extraction model upgrade.** Bureau extraction was running on an older model. Upgraded to match the rest of the parsing pipeline for better accuracy and reliability. * **Late payment history field.** New `worst_delinquency_days` field captures the worst number of days late (30, 60, 90, etc.) independently of account status. Accounts that are closed but have late history now surface in the delinquencies section of both templates. ### LOC Scrub template * **Charge cards in revolving section.** Charge accounts (e.g. Amex) were not showing up. The revolving filter now includes both `revolving` and `charge` account types. * **Revolving account classification guard.** The unsecured loans filter now rejects revolving accounts as a safeguard against parser mislabeling. * **Closed revolving accounts with balances.** Closed cards that still carry a balance now appear in the Revolving Accounts table, marked with `(CLOSED)` prefix and red highlight. Paydown columns show the full balance (pay to zero) instead of 25% utilization. * **Fraud and security freeze alerts.** If the credit report contains a fraud alert or security freeze, a red bold warning appears in the header area under the logo. ### Credit Full template * **AU accounts excluded from DTI by default.** Authorized user accounts now default to `In DTI? = No`, matching the LOC Scrub behavior. The toggle remains editable. * **Inquiries filtered to last 12 months.** The inquiries section now only shows hard pulls from the last 12 months, sorted newest first. *** ## February 23–27, 2026 ### NSF and Overdraft tag split The combined `nsf_overdraft` tag has been split into two separate tags: `nsf` and `overdraft`. * **NSF** = the payment was rejected. It did not go through. The money never left the account. * **Overdraft** = the payment went through, but pushed the account balance negative. The bank covered it. Both tags are still deterministic (regex, no LLM), debit-only, and have the same $0.01–$200 amount guard. Both remain in the revenue exclusion set. A transaction like "NSF OD FEE" can match both tags simultaneously. Analytics, metric cards, the bank statements table, and the pivoted deposits view now report NSF and Overdraft as separate columns and metrics. The monthly spreadsheet export has two rows instead of one. Old books that were parsed before this change still have `nsf_overdraft` in their tag arrays. The backend analytics layer treats legacy `nsf_overdraft` tags as matching both `nsf` and `overdraft` so existing books do not lose their data. The frontend renders the old tag with its original "NSF/Overdraft" label and color. On reparse, the old tag is replaced with the split tags automatically. ### French-Canadian bank support All five deterministic tagging functions (check, wire, NSF, overdraft, stop payment) now recognize French-language transaction descriptions from Quebec credit unions and Canadian banks. **Check patterns:** * `Chèque #740`, `Chèque, NO.740`, `Dépôt chèque`, `Chèque certifié` **Wire patterns:** * `Virement entrant` (incoming wire), `Virement sortant` (outgoing wire), `Virement électronique` (electronic wire), `Virement interbancaire` (interbank wire) **NSF patterns:** * `Fonds manquants` (missing funds), `Fonds insuffisants` (insufficient funds), `Sans provision` (without provision), `Chèque retourné` (returned cheque), `Effet retourné` (returned item) **Overdraft patterns:** * `Découvert` (overdraft), `À découvert` (overdrawn), `Solde négatif` (negative balance) **Stop payment patterns:** * `Arrêt de paiement` (stop payment), `Opposition` (European French banking term for stop payment) ### Monthly spreadsheet restructure The monthly bank statement spreadsheet layout was restructured. The stacking table and offer calculator now flow vertically below the pivoted deposits table instead of being placed side-by-side. Stacking rows are grouped and sorted by loan type. Conditional formatting highlights active vs. inactive positions. NSF and Overdraft are separate rows in the pivoted deposits table. ### File management New file list view replaces the old files tab. Documents have a dedicated panel component with PDF, CSV, and image viewing, plus a toggle to the parsed transaction sidebar. File uploads, downloads, and document exclusion are handled inline. ### Book page layout The book page tab bar collapses into a popover on narrow screens. BookInfoBadges supports a compact mode. The document viewer was refactored into a standalone DocumentPanel component. Debt board defaults to expanded view and is responsive on mobile. ### MIME detection File type detection switched from manual magic-byte checks to libmagic with filename extension as fallback. Fixes misclassification of edge-case file types during document upload and parsing. ### Analytics: payment processor and stop payment metrics Payment processor totals/counts and stop payment totals/counts are now tracked at the statement and book level alongside existing tag-based metrics (NSF, overdraft, owner transaction, internal transfer, bank fee). *** ## February 16–20, 2026 ### LOC Scrub template rebuild The Credit Report LOC Scrub spreadsheet template was rewritten from scratch. * Each bureau tab has a structured header block with customer info, underwriter, and estimated BLOC summary * Delinquencies surface in their own section before the revolving accounts table. Charged off, collection, bankruptcy, and delinquent accounts are separated * Revolving accounts table highlights closed accounts in light red and authorized-user accounts in light blue. 50%/25% utilization columns are live Excel formulas * DTI panel in columns K-L pulls total monthly debts from the hidden `_RawAccounts` sheet via SUMIFS. Annual income is a user-input cell; monthly income and DTI ratio calculate automatically * Logo positioning fixed with pixel-level offsets so it renders correctly across Excel versions and zoom levels *** ## February 9–13, 2026 ### Open in Google Sheets Spreadsheets can now be opened directly in Google Sheets. One click uploads the generated workbook to Drive and opens it in a new tab. ### Chat overhaul Chat UI rebuilt. The input bar supports file attachments and multi-line messages. The message list handles streaming and non-streaming responses more reliably. The chat panel layout was reworked with better status indicators, cleaner thread separation, and improved scroll behavior. ### Funder directory embed The funder directory can be embedded as a public, token-authenticated iframe. Partners can drop an embed route directly into their own portals. # Known Issues Source: https://docs.lendpathway.com/changelog/known-issues Current bugs and limitations we're tracking. Things we know about and are working on. If you're hitting something not listed here, email [support@lendpathway.com](mailto:support@lendpathway.com). ## UI **Mobile and responsive breakpoints.** The app is designed for desktop. We've done a pass on responsive layouts for the book page, debt board, and tab bar, but there are still places where spacing, column visibility, and component sizing break down on smaller screens. Ongoing. ## CRM **Lender matching and screening per funder.** The autodeny/screening system currently applies a single set of screening settings at the org level. We are working on moving this to per-funder screening, where each funder can have its own criteria, its own industry list, and its own exclusions. This requires a mapping layer that canonicalizes our internal industry types to each funder's industry enums so we can support their specific underwriting workflows. **Programmatic lender submission.** Working with multiple lender portals and APIs to configure programmatic deal submission to funders through a single API layer. **Book deduplication and ingestion.** Documents and parsed results that come through ingestion (email, Drive, manual upload) need to be deduplicated and routed cleanly into the businesses tab. Merging logic for when the same merchant appears across multiple books is in progress. # Our Stack Source: https://docs.lendpathway.com/changelog/stack What we're building and thinking about. ## In Progress **NSF/Overdraft source tracing.** When an NSF or overdraft occurs, find the corresponding credit that was declined or the reversal where the bounced money came back. Link the return credit to the original debit (e.g., RETURN NSF CHEQUE 131.19 traced to ACH DEBIT FORWARD FUNDING 131.19). Dual-tag the linked transaction with both `nsf` and the position tag (e.g., `merchant_cash_advance`). Shows which positions are getting hit. **Configurable book-level metric cards.** The bank statements table already supports configurable columns in both standard and pivoted views. Same pattern applied to the deposits/withdrawals metric cards at the book level. Users pick which tag-based or computed metrics show as cards in the overview. **CRM: per-funder screening and industry mapping.** Screening settings move from org-level to per-funder. Each funder gets its own criteria, industry list, and exclusions. Requires a mapping layer to canonicalize internal industry types to funder-specific industry enums. **CRM: programmatic lender submission.** Integrating with lender portals and APIs for programmatic deal submission. **CRM: book deduplication and ingestion routing.** Merging duplicate merchants across books and routing ingested documents into the businesses tab. **Responsive UI.** Continued work on mobile and tablet breakpoints. ## Up Next Things committed but not yet started. ## Ideas Not committed. Just thinking. # Work with credit reports Source: https://docs.lendpathway.com/cookbook/credit-reports Read identity, FICO scores, inquiries, and tradelines without double-counting bureaus. Credit report output is stored on the Book at `book_meta.parser_v2_credit_report`. It preserves each bureau as its own view of the subject's credit file. ```python theme={null} import os import requests API_BASE = "https://api.lendpathway.com/api" TOKEN = os.environ["PATHWAY_API_TOKEN"] BOOK_ID = "99cc93e6-1f3f-42b1-9fe4-ba5a95be9c78" response = requests.get( f"{API_BASE}/books/{BOOK_ID}", headers={"Authorization": f"Bearer {TOKEN}"}, timeout=30, ) response.raise_for_status() book = response.json() credit = book["book_meta"].get("parser_v2_credit_report") if credit is None: raise RuntimeError("This Book has no parsed credit report") ``` ## Response model The following models describe the complete credit report payload. ```python theme={null} from typing import Literal from pydantic import BaseModel, Field BureauName = Literal[ "equifax", "experian", "transunion", "innovis", "lexisnexis", "other", ] AccountType = Literal["revolving", "installment", "mortgage", "charge", "other"] CollateralType = Literal[ "unsecured", "real_estate", "vehicle", "cash_deposit", "education", "misc_asset", ] AccountStatus = Literal[ "current", "delinquent", "charged_off", "collection", "bankruptcy", "foreclosure", "repossession", "settled", "closed", "unknown", ] class CRAddress(BaseModel): address_line_1: str | None = None city: str | None = None state: str | None = None zip_code: str | None = None country: str | None = None is_primary: bool | None = None class CREntity(BaseModel): full_name: str | None = None address: list[CRAddress] | None = None date_of_birth: str | None = None has_fraud_alert: bool | None = None has_security_freeze: bool | None = None class CreditInquiry(BaseModel): creditor_name: str | None = None inquiry_date: str | None = None class CreditBureau(BaseModel): name: BureauName display_name: str | None = None inquiries: list[CreditInquiry] | None = None class CreditAccount(BaseModel): account_name: str | None = None account_number: str | None = None is_open: bool | None = None is_authorized_user: bool | None = None is_self_reported: bool | None = None account_type_normalized: AccountType | None = None account_status_normalized: AccountStatus | None = None collateral_type: CollateralType | None = None date_opened: str | None = None date_closed: str | None = None recent_balance: float | None = None recent_balance_date: str | None = None credit_limit: float | None = None date_of_most_recent_report: str | None = None date_of_first_report: str | None = None date_last_active: str | None = None worst_delinquency_days: int | None = None payment_status: str | None = None monthly_payment: float | None = None high_credit_balance: float | None = None class FicoScore(BaseModel): score: int | None = None date_of_score: str | None = None class BureauReport(BaseModel): credit_bureau: CreditBureau underwritten_accounts: list[CreditAccount] = Field(default_factory=list) fico_score: FicoScore | None = None class CRMeta(BaseModel): primary_entity: CREntity | None = None credit_report_body: list[BureauReport] | None = None ``` All extracted fields are optional because report formats vary. Check for missing values at the boundary of your workflow. ## Read the subject ```python theme={null} entity = credit.get("primary_entity") or {} print("Subject", entity.get("full_name")) print("Date of birth", entity.get("date_of_birth")) print("Fraud alert", entity.get("has_fraud_alert")) print("Security freeze", entity.get("has_security_freeze")) primary_address = next( ( address for address in entity.get("address") or [] if address.get("is_primary") ), None, ) ``` The fraud-alert and security-freeze fields are `null` when the report does not provide enough information to decide. ## Read FICO scores ```python theme={null} for report in credit.get("credit_report_body") or []: bureau = report["credit_bureau"] fico = report.get("fico_score") or {} print( bureau.get("display_name") or bureau["name"], fico.get("score"), fico.get("date_of_score"), ) ``` A multi-bureau report can return several scores. Keep the bureau name attached to every score instead of collapsing them into one unexplained number. ## Select one bureau for account totals The same tradeline commonly appears under Experian, Equifax, and TransUnion. Summing every bureau together can count one debt several times. Pathway stores the bureau chosen in the UI at `book_meta.selected_credit_bureau`. Your integration can use that value or apply its own explicit bureau policy. ```python theme={null} reports = credit.get("credit_report_body") or [] selected_name = book["book_meta"].get("selected_credit_bureau") def choose_bureau(reports: list[dict], preferred: str | None) -> dict | None: if preferred: match = next( ( report for report in reports if report["credit_bureau"]["name"] == preferred ), None, ) if match: return match priority = ["experian", "equifax", "transunion"] for bureau_name in priority: match = next( ( report for report in reports if report["credit_bureau"]["name"] == bureau_name ), None, ) if match: return match return reports[0] if reports else None selected = choose_bureau(reports, selected_name) accounts = selected["underwritten_accounts"] if selected else [] ``` This keeps account balances, payments, and counts internally consistent. You can still display every bureau side by side. ## Work with tradelines ```python theme={null} for account in accounts: print({ "name": account.get("account_name"), "type": account.get("account_type_normalized"), "status": account.get("account_status_normalized"), "open": account.get("is_open"), "balance": account.get("recent_balance"), "limit": account.get("credit_limit"), "monthly_payment": account.get("monthly_payment"), "worst_delinquency_days": account.get("worst_delinquency_days"), }) ``` ### Derogatory history `account_status_normalized` describes the current account state. `worst_delinquency_days` preserves late-payment history even when the account is currently closed or paid. ```python theme={null} derogatory_statuses = { "delinquent", "charged_off", "collection", "bankruptcy", "foreclosure", "repossession", "settled", } accounts_with_risk = [ account for account in accounts if account.get("account_status_normalized") in derogatory_statuses or (account.get("worst_delinquency_days") or 0) > 0 ] ``` ### Authorized-user and self-reported accounts Pathway flags accounts that should often be handled separately in underwriting. ```python theme={null} borrower_accounts = [ account for account in accounts if not account.get("is_authorized_user") and not account.get("is_self_reported") ] ``` The flags preserve the report's account role. Your credit policy decides whether to exclude them from a particular calculation. ### Monthly obligations ```python theme={null} monthly_obligations = sum( account.get("monthly_payment") or 0 for account in borrower_accounts if account.get("is_open") ) print("Monthly obligations", round(monthly_obligations, 2)) ``` This is a selected-bureau total. Avoid combining it with the same accounts from other bureaus. ### Revolving utilization ```python theme={null} revolving = [ account for account in borrower_accounts if account.get("is_open") and account.get("account_type_normalized") in {"revolving", "charge"} ] balance = sum(account.get("recent_balance") or 0 for account in revolving) limit = sum(account.get("credit_limit") or 0 for account in revolving) utilization = (balance / limit * 100) if limit else None ``` Charge accounts can have no conventional credit limit. Keep utilization nullable when a denominator is unavailable. ## Read hard inquiries Inquiries live inside each `credit_bureau` object. ```python theme={null} if selected: inquiries = selected["credit_bureau"].get("inquiries") or [] for inquiry in inquiries: print(inquiry.get("inquiry_date"), inquiry.get("creditor_name")) ``` The API returns the inquiries visible in the source report. Apply any recency window required by your own credit policy. ## Produce a selected-bureau summary ```python theme={null} def summarize_credit(book: dict) -> dict | None: credit = book["book_meta"].get("parser_v2_credit_report") if not credit: return None reports = credit.get("credit_report_body") or [] selected = choose_bureau( reports, book["book_meta"].get("selected_credit_bureau"), ) if not selected: return None accounts = [ account for account in selected["underwritten_accounts"] if not account.get("is_authorized_user") and not account.get("is_self_reported") ] derogatory = { "delinquent", "charged_off", "collection", "bankruptcy", "foreclosure", "repossession", "settled", } fico = selected.get("fico_score") or {} return { "subject": (credit.get("primary_entity") or {}).get("full_name"), "bureau": selected["credit_bureau"]["name"], "fico": fico.get("score"), "open_accounts": sum(bool(account.get("is_open")) for account in accounts), "monthly_obligations": sum( account.get("monthly_payment") or 0 for account in accounts if account.get("is_open") ), "derogatory_accounts": sum( account.get("account_status_normalized") in derogatory or (account.get("worst_delinquency_days") or 0) > 0 for account in accounts ), "fraud_alert": (credit.get("primary_entity") or {}).get("has_fraud_alert"), "security_freeze": (credit.get("primary_entity") or {}).get("has_security_freeze"), } ``` For a formatted workbook, use the `vlad_full`, `vlad_summary`, or `vlad_loc_scrub` templates described in [Exports](/api-reference/endpoint/exports). # Export data Source: https://docs.lendpathway.com/cookbook/export-data Move Book data into pandas, CSV, Excel, PDF, or your own storage. Pathway exposes the same Book through several export shapes. Pick the shape your next system can consume directly. | Need | Request | | ------------------------------------------ | ----------------------------------------- | | Complete stored deal and raw parser output | `GET /books/{book_id}` | | Computed underwriting data as JSON | `GET /books/{book_id}/analytics` | | Rows ready for a database or dataframe | `GET /books/{book_id}/csv-export` | | An underwriter-facing workbook or report | `GET /books/{book_id}/spreadsheet-export` | | Original uploaded files | `GET /documents/{document_id}/download` | The [Exports reference](/api-reference/endpoint/exports) lists every public template and the full request parameters. ## Load the underwriting table into pandas The CSV endpoint can go directly into a dataframe. `month_as_row` gives you one row per statement period and account, followed by the summary rows `NET` and `Average`. ```python theme={null} from io import BytesIO import pandas as pd import requests API_BASE = "https://api.lendpathway.com/api" TOKEN = "pat_your_token_here" BOOK_ID = "99cc93e6-1f3f-42b1-9fe4-ba5a95be9c78" HEADERS = {"Authorization": f"Bearer {TOKEN}"} response = requests.get( f"{API_BASE}/books/{BOOK_ID}/csv-export", headers=HEADERS, params={"table_format": "month_as_row"}, timeout=60, ) response.raise_for_status() underwriting = pd.read_csv(BytesIO(response.content)) # Keep statement/account rows for analysis. The final two rows are rollups. detail = underwriting[~underwriting["Period"].isin(["NET", "Average"])] print(detail[["Period", "Account", "Deposits", "True Revenue", "DTI %"]]) ``` Use `month_as_col` when a human wants periods across columns. The row-oriented file is easier to load into a database or combine across Books. ## Save a gallery export Discover templates at runtime. Organizations can have private templates, and new public templates can be added without changing the API. ```python theme={null} from pathlib import Path import requests def get_template(template_id: str) -> dict: response = requests.get( f"{API_BASE}/parse/spreadsheet-templates", headers=HEADERS, timeout=30, ) response.raise_for_status() templates = response.json()["templates"] return next(template for template in templates if template["id"] == template_id) def save_gallery_export(book_id: str, template_id: str, destination: Path) -> Path: template = get_template(template_id) sheet_type = "vlad" if template["type"] == "credit_report" else template["type"] response = requests.get( f"{API_BASE}/books/{book_id}/spreadsheet-export", headers=HEADERS, params={"sheet_type": sheet_type, "template_id": template_id}, timeout=120, ) response.raise_for_status() expected_type = "application/pdf" if template["format"] == "pdf" else "spreadsheet" content_type = response.headers.get("content-type", "").lower() if expected_type not in content_type: raise RuntimeError(f"Unexpected export content type: {content_type}") suffix = ".pdf" if template["format"] == "pdf" else ".xlsx" output = destination.with_suffix(suffix) output.write_bytes(response.content) return output path = save_gallery_export( BOOK_ID, template_id="mca_stacking_pro", destination=Path("acme-coffee-underwriting"), ) print(path) ``` Omit `template_id` when the export should follow the organization's current default. Pass an explicit ID when another process depends on the workbook layout. ## Store a JSON snapshot Save the Book and analytics separately. The Book contains raw parser output and stored edits. Analytics contain the computed state at the time of export. ```python theme={null} import json from datetime import UTC, datetime from pathlib import Path import requests def get_json(path: str) -> dict | list: response = requests.get( f"{API_BASE}{path}", headers=HEADERS, timeout=60, ) response.raise_for_status() return response.json() output = Path("acme-coffee") output.mkdir(exist_ok=True) book = get_json(f"/books/{BOOK_ID}") analytics = get_json(f"/books/{BOOK_ID}/analytics") documents = get_json(f"/documents/?book_id={BOOK_ID}") (output / "book.json").write_text(json.dumps(book, indent=2), encoding="utf-8") (output / "analytics.json").write_text(json.dumps(analytics, indent=2), encoding="utf-8") (output / "documents.json").write_text(json.dumps(documents, indent=2), encoding="utf-8") (output / "manifest.json").write_text( json.dumps( { "book_id": BOOK_ID, "exported_at": datetime.now(UTC).isoformat(), "parse_status": book["parse_status"], "last_parsed_at": book.get("last_parsed_at"), }, indent=2, ), encoding="utf-8", ) ``` The analytics snapshot can change after a user edits a transaction tag, position, exclusion, or organization rule. Keep `last_parsed_at` and your own export timestamp when the files need an audit trail. ## Download the original documents The document download endpoint returns a fresh one-hour URL. Request it shortly before downloading the file. ```python theme={null} from pathlib import Path import requests def safe_filename(name: str) -> str: return Path(name).name.replace("\x00", "") documents = requests.get( f"{API_BASE}/documents/", headers=HEADERS, params={"book_id": BOOK_ID}, timeout=30, ) documents.raise_for_status() source_dir = Path("acme-coffee/source-documents") source_dir.mkdir(parents=True, exist_ok=True) for document in documents.json(): response = requests.get( f"{API_BASE}/documents/{document['id']}/download", headers=HEADERS, timeout=30, ) response.raise_for_status() download_url = response.json()["download_url"] file_response = requests.get(download_url, timeout=120) file_response.raise_for_status() destination = source_dir / safe_filename(document["original_filename"]) destination.write_bytes(file_response.content) ``` For many documents, `POST /documents/bulk-download` can return one ZIP. Its request includes `document_ids`, `book_id`, `book_name`, and an optional `with_watermark` flag. # Parse documents and get results Source: https://docs.lendpathway.com/cookbook/parse-and-poll A complete Python integration for submitting a deal, waiting for parsing, and fetching analytics. This guide builds the smallest useful Pathway integration in Python. It submits a deal package, waits for the asynchronous parse, and retrieves bank analytics. Use a webhook when your application already has a public callback endpoint. Use polling for local scripts, jobs, and the first version of an integration. ## Install the client ```bash theme={null} python -m pip install requests ``` Set the token in your environment: ```bash theme={null} export PATHWAY_API_TOKEN="pat_your_token_here" ``` The examples use this shared setup: ```python theme={null} import os from pathlib import Path import requests API_BASE = "https://api.lendpathway.com/api" TOKEN = os.environ["PATHWAY_API_TOKEN"] session = requests.Session() session.headers["Authorization"] = f"Bearer {TOKEN}" ``` ## Submit the deal Send every file for one deal in the same multipart request. ```python theme={null} def submit_book( paths: list[Path], book_name: str, webhook_url: str | None = None, ) -> str: opened = [path.open("rb") for path in paths] try: files = [ ("files", (path.name, handle, "application/pdf")) for path, handle in zip(paths, opened) ] data = {"book_name": book_name} if webhook_url: data["webhook_url"] = webhook_url response = session.post( f"{API_BASE}/submit-book", files=files, data=data, timeout=120, ) response.raise_for_status() return response.json()["book_id"] finally: for handle in opened: handle.close() book_id = submit_book( paths=[ Path("jan_statement.pdf"), Path("feb_statement.pdf"), Path("credit_report.pdf"), Path("application.pdf"), ], book_name="Smith Auto Body", ) print("Submitted", book_id) ``` The response only confirms that Pathway accepted the asynchronous workflow. The new Book continues through upload, classification, and parsing after the request closes. ## Wait with polling Poll the Book every few seconds. Stop when the status reaches `completed`, `failed`, or `cancelled`, and put a timeout around the whole wait. ```python theme={null} import time class ParseFailed(RuntimeError): pass def wait_for_book( book_id: str, *, poll_every: float = 5, timeout: float = 15 * 60, ) -> dict: deadline = time.monotonic() + timeout while time.monotonic() < deadline: response = session.get( f"{API_BASE}/books/{book_id}", timeout=30, ) response.raise_for_status() book = response.json() status = book["parse_status"] message = book.get("parse_status_message") or "" print(f"[{status}] {message}") if status == "completed": return book if status in {"failed", "cancelled"}: raise ParseFailed(f"{status}: {message}") time.sleep(poll_every) raise TimeoutError(f"Book {book_id} did not finish within {timeout} seconds") book = wait_for_book(book_id) ``` Five seconds is a reasonable polling interval. Faster polling rarely makes the result arrive meaningfully sooner and uses the PAT's request budget. ## Fetch the result After a completed parse, request the output your workflow needs. ```python theme={null} def get_json(path: str) -> dict | list: response = session.get(f"{API_BASE}{path}", timeout=60) response.raise_for_status() return response.json() analytics = get_json(f"/books/{book_id}/analytics") print("True revenue:", analytics["true_revenue"]) print("Average daily balance:", analytics["average_daily_balance"]) print("Debt-to-income:", analytics["debt_to_income_ratio"]) print("Positions:", len(analytics["positions"])) ``` The useful result endpoints are: | Endpoint | Content | | ------------------------------------ | --------------------------------------------------------------------------------------- | | `GET /books/{id}` | Status and raw parser output for credit, tax, application, identity, and bank documents | | `GET /books/{id}/analytics` | Computed bank underwriting data | | `GET /books/{id}/statements` | Account-first statement history and daily balances | | `GET /books/{id}/tax-analytics` | Tax qualifying-income analysis | | `GET /books/{id}/csv-export` | Bank underwriting CSV | | `GET /books/{id}/spreadsheet-export` | Generated Excel or PDF from the gallery | Some Books contain no bank statements. In that case `/analytics` returns `400`, while the relevant raw credit, tax, application, or identity result can still be present in `book_meta`. ## Wait with a webhook A [webhook](https://en.wikipedia.org/wiki/Webhook) removes the polling loop. Add `webhook_url` to the submit request and expose a callback that accepts Pathway's completion payload. ```python theme={null} book_id = submit_book( paths=[Path("statement.pdf"), Path("application.pdf")], book_name="Smith Auto Body", webhook_url="https://example.com/webhooks/pathway", ) ``` The receiver can remain small. Confirm the stored status with an authenticated API request before starting downstream work. ```python theme={null} import os from typing import Literal import requests from fastapi import BackgroundTasks, FastAPI from pydantic import BaseModel app = FastAPI() API_BASE = "https://api.lendpathway.com/api" TOKEN = os.environ["PATHWAY_API_TOKEN"] class ParseWebhook(BaseModel): book_id: str status: Literal["completed", "failed", "cancelled"] book_url: str error: str | None = None def process_completed_book(book_id: str) -> None: headers = {"Authorization": f"Bearer {TOKEN}"} book_response = requests.get( f"{API_BASE}/books/{book_id}", headers=headers, timeout=30, ) book_response.raise_for_status() book = book_response.json() if book["parse_status"] != "completed": return analytics_response = requests.get( f"{API_BASE}/books/{book_id}/analytics", headers=headers, timeout=60, ) if analytics_response.status_code == 400: # A completed Book can contain only credit, tax, or application data. return analytics_response.raise_for_status() analytics = analytics_response.json() # Persist analytics or enqueue the next step in your own system. print(book["name"], analytics["true_revenue"]) @app.post("/webhooks/pathway", status_code=202) def receive_pathway_webhook( payload: ParseWebhook, background_tasks: BackgroundTasks, ): if payload.status == "completed": background_tasks.add_task(process_completed_book, payload.book_id) return {"accepted": True} ``` Pathway currently sends one unsigned webhook delivery. Keep the receiver idempotent and use the authenticated Book response as the final status. ## Complete polling script This version can be copied into one file and run directly. ```python theme={null} import os import time from pathlib import Path import requests API_BASE = "https://api.lendpathway.com/api" TOKEN = os.environ["PATHWAY_API_TOKEN"] session = requests.Session() session.headers["Authorization"] = f"Bearer {TOKEN}" def submit_book(paths: list[Path], name: str) -> str: opened = [path.open("rb") for path in paths] try: response = session.post( f"{API_BASE}/submit-book", files=[ ("files", (path.name, handle, "application/pdf")) for path, handle in zip(paths, opened) ], data={"book_name": name}, timeout=120, ) response.raise_for_status() return response.json()["book_id"] finally: for handle in opened: handle.close() def wait_for_book(book_id: str, timeout: float = 900) -> dict: deadline = time.monotonic() + timeout while time.monotonic() < deadline: response = session.get(f"{API_BASE}/books/{book_id}", timeout=30) response.raise_for_status() book = response.json() print(book["parse_status"], book.get("parse_status_message") or "") if book["parse_status"] == "completed": return book if book["parse_status"] in {"failed", "cancelled"}: raise RuntimeError(book.get("parse_status_message") or book["parse_status"]) time.sleep(5) raise TimeoutError("Pathway parse timed out") book_id = submit_book( [Path("jan.pdf"), Path("feb.pdf"), Path("application.pdf")], "Smith Auto Body", ) wait_for_book(book_id) response = session.get(f"{API_BASE}/books/{book_id}/analytics", timeout=60) response.raise_for_status() analytics = response.json() print({ "book_id": book_id, "true_revenue": analytics["true_revenue"], "average_daily_balance": analytics["average_daily_balance"], "debt_to_income_ratio": analytics["debt_to_income_ratio"], "positions": len(analytics["positions"]), }) ``` Continue with [Working with analytics data](/cookbook/working-with-analytics) once this flow is returning completed Books. # Work with analytics Source: https://docs.lendpathway.com/cookbook/working-with-analytics Read cash flow, transactions, debt positions, and screening without rebuilding Pathway's calculations. `GET /books/{book_id}/analytics` is the main underwriting response for bank data. It contains Book totals, monthly account rows, enriched transactions, detected debt positions, payment schedules, counterparty clusters, and screening. The response is computed when you request it. Changes to tags, exclusions, organization settings, or screening rules appear on the next call. The complete typed contract is on the [Analytics endpoint page](/api-reference/endpoint/analytics#response-models). Keep that model beside your integration rather than recreating response fields from examples. ## Fetch and validate the response ```python theme={null} import os import requests API_BASE = "https://api.lendpathway.com/api" TOKEN = os.environ["PATHWAY_API_TOKEN"] BOOK_ID = "99cc93e6-1f3f-42b1-9fe4-ba5a95be9c78" response = requests.get( f"{API_BASE}/books/{BOOK_ID}/analytics", headers={"Authorization": f"Bearer {TOKEN}"}, timeout=60, ) response.raise_for_status() analytics = response.json() ``` If you copied the Pydantic contracts from the endpoint reference into `pathway_models.py`, validate at the boundary: ```python theme={null} from pathway_models import BookAnalytics analytics = BookAnalytics.model_validate(response.json()) print(analytics.true_revenue) ``` Validation catches a changed or unexpected payload before it moves into a credit decision, spreadsheet, or CRM field. ## Start with the Book totals The most commonly used fields are already calculated. ```python theme={null} summary = { "total_deposits": analytics["total_deposits"], "total_withdrawals": analytics["total_withdrawals"], "true_revenue": analytics["true_revenue"], "average_daily_balance": analytics["average_daily_balance"], "days_negative": analytics["days_negative_balance"], "loan_payments": analytics["total_loan_payments"], "debt_to_income_ratio": analytics["debt_to_income_ratio"], "active_mca_positions": analytics["num_active_mca_positions"], "missed_payments": analytics["num_missed_payments"], } ``` Do not recompute these values from raw `book_meta`. The analytics response has already applied the Book's excluded documents, accounts, positions, and revenue tags, along with organization-level business-day settings. ### Gross deposits and true revenue `total_deposits` includes every credit. It can contain transfers, loan proceeds, owner contributions, reversals, and operating revenue. `true_revenue` includes credits that survive the active revenue-exclusion rules. The exact tag set used for the request appears in `revenue_exclusion_tags`. ```python theme={null} print("Gross deposits", analytics["total_deposits"]) print("True revenue", analytics["true_revenue"]) print("Excluded tags", analytics["revenue_exclusion_tags"]) ``` ### Debt-to-income ratio Pathway returns `debt_to_income_ratio` as a percentage. A value of `18.4` means 18.4%. The value can be `null` when there is no qualifying revenue denominator. ```python theme={null} dti = analytics["debt_to_income_ratio"] if dti is None: print("DTI unavailable") else: print(f"DTI {dti:.1f}%") ``` ## Read statement periods `statements` is chronological bank data organized by statement period. Each period contains one or more account rows. ```python theme={null} for statement in analytics["statements"]: print( statement["statement_period"], statement["statement_start_date"], statement["statement_end_date"], ) for account in statement["accounts"]: print( account["account_id"], account["account_name"], account["true_revenue"], account["average_daily_balance"], ) ``` Banks sometimes issue separate PDFs for each account in the same month. Pathway folds those files into one statement period and lists every source in `document_ids`. ### Use the combined row for monthly trends The row with `account_id == 0` represents the combined cash position across included accounts for that statement period. ```python theme={null} monthly = [] for statement in analytics["statements"]: combined = next( account for account in statement["accounts"] if account["account_id"] == 0 ) monthly.append({ "period": statement["statement_period"], "deposits": combined["total_deposits"], "true_revenue": combined["true_revenue"], "average_daily_balance": combined["average_daily_balance"], "days_negative": combined["days_negative_balance"], "dti": combined["debt_to_income_ratio"], }) ``` Use individual rows when the decision needs account-level detail. Avoid summing the combined row together with its individual account rows because that counts the same activity twice. `average_statement_metrics` contains the average period row used by Pathway's tables and CSV export. ## Read transactions `merged_accounts` contains the normalized transaction history. It is keyed by account ID as a string. ```python theme={null} transactions = [ transaction for account in analytics["merged_accounts"].values() for transaction in account["transactions"] ] transactions.sort(key=lambda transaction: transaction["transaction_date"]) ``` Each transaction includes its bank description, direction, amount, running balance, cleaned tags, source document, and attached debt position when one exists. ### Find true revenue credits ```python theme={null} revenue_transactions = [ transaction for transaction in transactions if "true_revenue" in transaction.get("tag", []) ] for transaction in revenue_transactions[:20]: print( transaction["transaction_date"], transaction["description"], transaction["amount"], ) ``` ### Find loan payments Use the tags returned by `GET /parse/tags` when you need a complete dynamic tag registry. For a single response, position attachment is a clean way to isolate identified debt activity: ```python theme={null} position_payments = [ transaction for transaction in transactions if transaction["transaction_type"] == "debit" and transaction.get("position") is not None ] ``` ### Group by tag A transaction can carry several tags. Decide whether your output is multi-label before aggregating. ```python theme={null} from collections import defaultdict totals_by_tag = defaultdict(float) for transaction in transactions: for tag in transaction.get("tag", []): totals_by_tag[tag] += transaction["amount"] for tag, total in sorted(totals_by_tag.items()): print(tag, round(total, 2)) ``` The totals across tags will usually exceed deposits plus withdrawals because one transaction can contribute to more than one tag. ## Read debt positions Each object in `positions` represents one detected relationship with a lender or funder. ```python theme={null} for position in analytics["positions"]: print({ "name": position["funder_title"] or position["name"], "type": position["loan_type"], "status": position["status"], "funded": position["total_disbursements"], "paid": position["total_payments"], "daily_remit": position["daily_remit_burden"], "monthly_remit": position["monthly_remit_burden"], "holdback_pct": position["holdback_pct"], "misses": len(position["potential_missed_payments"]), "modifications": len(position["potential_modified_payments"]), }) ``` `status` is `just_funded`, `active`, or `closed`. The `episodes` tree gives the history behind that rollup: * An `initial` episode begins the observed relationship. * A `renewal` begins after earlier schedules have closed. * A `stack` begins while an earlier schedule is still active. * An `orphan` contains payments whose advance occurred before the available statement window. Payment schedules inside each episode include cadence, expected daily remittance, active state, inferred misses, and amount modifications. ```python theme={null} for position in analytics["positions"]: for episode in position["episodes"]: for schedule in episode["schedules"]: print( position["name"], episode["role"], schedule["frequency"], schedule["avg_amount"], schedule["state"], ) ``` ## Read counterparties `counterparty_clusters` groups similar transaction descriptions separately for credits and debits. ```python theme={null} top_credit_sources = sorted( ( cluster for cluster in analytics["counterparty_clusters"] if cluster["direction"] == "credit" ), key=lambda cluster: cluster["total"], reverse=True, ) for cluster in top_credit_sources[:10]: print(cluster["counterparty"], cluster["total"], cluster["count"]) ``` Use `transaction_ids` to trace any cluster back to the underlying activity in `merged_accounts`. ## Read screening When organization screening is enabled, the response includes the complete fact sheet and current result. ```python theme={null} result = analytics.get("screening_result") if result: print(result["result"]) for rule in result["resolved_rules"]: print( rule["result"], rule["reason"], rule["rule_after_substituting"], ) ``` `screening_metrics` contains the normalized values available to rules, including recent-month snapshots, recency measures, applicant facts, and payment-risk counts. Missing facts remain `null`. Screening is evaluated during the analytics request. A rule update affects the next response without reparsing the Book. ## Build an integration payload Keep the Pathway response available for audit and map only the fields the receiving system understands. ```python theme={null} def crm_payload(book: dict, analytics: dict) -> dict: screening = analytics.get("screening_result") return { "external_book_id": book["id"], "business_name": book["name"], "parse_status": book["parse_status"], "total_deposits": analytics["total_deposits"], "true_revenue": analytics["true_revenue"], "average_daily_balance": analytics["average_daily_balance"], "days_negative": analytics["days_negative_balance"], "debt_to_income_ratio": analytics["debt_to_income_ratio"], "active_mca_positions": analytics["num_active_mca_positions"], "monthly_mca_remit": analytics["total_mca_monthly_remit"], "screening_result": screening["result"] if screening else None, } ``` Store the Book ID with the destination record. It provides a stable path back to source documents, raw parser output, and refreshed analytics. # Welcome to LendPathway Source: https://docs.lendpathway.com/getting-started/index Document parsing and underwriting infrastructure for lending teams. LendPathway reads bank statements, credit reports, tax forms, and loan applications. It extracts transactions, tags them, reconciles balances, detects debt positions, and returns structured underwriting data. Everything lands in a [Book](/platform/books/books). Pathway Pathway Start here. One deal, one Book. Synopsis, transactions, debt positions, metrics, and spreadsheets. An AI agent with a sandbox, scoped to your data. Ask, analyze, and build. Submit documents, get structured data back. Gmail, Salesforce, Google Drive.
Take a guided tour with your favorite AI
# Quickstart Source: https://docs.lendpathway.com/getting-started/quickstart Get your account set up and parse your first deal. ## Sign in Go to [app.lendpathway.com](https://app.lendpathway.com) to create an account or sign in. Sign in with Google or Microsoft. Pathway login Pathway login Sign in with Google or Microsoft only. Email/password login is not supported. ## Create your organization On first sign-in, enter your company name and phone number. Your organization is the shared workspace your whole team operates from. Invite teammates from **Settings** once you're in. Create organization Create organization If you've been invited to an existing organization, accept the invite link from your email. You'll be added to that org without creating a new one. ## Next steps Create a book and upload your first set of documents. What Pathway can parse and what each document type produces. Integrate Pathway into your pipeline programmatically. # How Parsing Works Source: https://docs.lendpathway.com/how-parsing-works > When you a parse job is initiated, LendPathway runs a multi-step pipeline that turns raw PDFs into structured, analyzed financial data. This page explains exactly what happens at each step. ## **The Pipeline** Every parse follows this sequence: ### **1. Load Documents** All uploaded files are pulled from storage. Only **PDF files** are accepted — any other file type (images, spreadsheets, Word docs, etc.) is immediately marked as failed. ### **2. Classify Each Document** Each PDF is sent to an AI model that reads the document and identifies what kind of financial document it is. Classification runs **in parallel** — all PDFs are classified at the same time. The classifier can identify these document types: | Type | What it looks for | | :--------------- | :---------------------------------------------------------------------- | | Bank Statement | Transactions, deposits, withdrawals, account balances | | Credit Report | Experian / Equifax / TransUnion, credit scores, tradelines | | Tax Form | IRS forms (1040, 1065, 1120, 1120-S, Schedule C, Schedule E, K-1, etc.) | | Loan Application | Application form for financing | | Receipt | Purchase receipt, business expense | | DecisionLogic | Report from [decisionlogic.com](http://decisionlogic.com) | | AR Report | Accounts receivable report | | Photo ID | Government-issued ID (driver's license, passport) | | Voided Check | Bank check showing account details | Documents that don't match any type are marked **Unsupported**. Of these, four have full parsing pipelines: **Bank Statement**, **Credit Report**, **Tax Form**, and **Loan Application**. The others (Receipt, Photo ID, Voided Check, etc.) are classified and stored but not parsed further. ### **3. Run Type-Specific Pipelines** Based on classification, LendPathway runs the appropriate parser for each document type — **in parallel**. If you upload a mix of bank statements, a credit report, and a loan application, all three pipelines run simultaneously. Screenshot2026 03 08at5 16 16AM Screenshot2026 03 08at5 16 16AM *** ## **Bank Statement Pipeline** The bank statement pipeline is the most complex. Here's what happens inside it, step by step. ### **Step 1 — Account Metadata Extraction** The AI reads **all bank statement PDFs together** in a single call and extracts: * **Business identity** — business name, address, phone, tax ID * **Principals** — owner names, roles, addresses, phone numbers * **Account ledgers** — every distinct bank account across all documents (account name, account number, bank name, routing number) Each account is assigned a unique ID. This step establishes the map of accounts that the rest of the pipeline uses. If no bank accounts are found at all, the parse fails here. After this step, two things happen immediately: * The **book name** is updated to the extracted business name * **AI Deep Research** is kicked off in the background (more on this below) ### **Step 2 — Statement Metadata Extraction** For each individual PDF (in parallel), the AI extracts statement-level metadata: * Statement start and end dates * Starting and ending balances, per account * Which accounts appear in this specific document This is also where LendPathway figures out the date range for the book (e.g. "3 accounts, Jan 2024 to Dec 2024"). ### **Step 3 — Duplicate Detection** Only runs when there are 2 or more bank statement PDFs. The AI compares all statements and identifies **redundant documents** — complete duplicates or documents that are subsets of another (e.g. someone uploaded both a full 3-page statement and a 1-page summary of the same month). Redundant documents are marked as failed and removed before transaction extraction, preventing double-counted data. ### **Step 4 — Transaction Extraction** For each remaining statement (in parallel), the AI extracts every individual transaction: * Date * Description * Amount * Type (credit or debit) This is the most computationally intensive step. If a document is too large for a single extraction call (hits token limits), LendPathway automatically falls back to **chunked extraction** — pulling transactions in batches of \~100 at a time, up to 20 chunks, and merging the results. Each chunk receives context about where the previous chunk left off to avoid gaps. ### **Step 5 — Assembly** The extracted metadata and transactions are merged together into **ledgers**. A ledger is one account's data within one statement document — its starting balance, ending balance, and list of transactions. A single PDF can produce multiple ledgers if it contains multiple accounts. ### **Step 6 — Reconciliation** After assembly, LendPathway reconciles each ledger independently (all ledgers in parallel). This is the mathematical verification step. **The formula:** Starting Balance + Sum of All Credits − Sum of All Debits = Computed Ending Balance The computed ending balance is compared against the ending balance printed on the statement. If they match, the ledger is **reconciled** — meaning the extracted transactions are a mathematically faithful representation of the bank's own records. **If it doesn't reconcile on the first check**, LendPathway enters a retry loop (up to **3 attempts**). On each attempt, the AI receives: * The original PDF (ground truth) * The current list of extracted transactions as a CSV * The current discrepancy amount and direction (too high or too low) * If the statement has multiple accounts, a note about which account is being reconciled The AI compares the extracted transactions against the PDF and can make three types of corrections: 1. **Flip** a transaction's type — if a credit was mistakenly extracted as a debit (or vice versa), flip it 2. **Remove** a transaction — if a duplicate or nonexistent transaction was extracted 3. **Add** a missing transaction — if a transaction visible in the PDF wasn't extracted The AI is instructed to only make corrections it can clearly verify in the PDF. It will never fabricate transactions to force the math to work. If the extraction looks correct but the math still doesn't add up (e.g. the bank's own statement has an internal discrepancy), the AI gives up and explains why. After corrections are applied, the balance is rechecked. If it's within \$0.05, the ledger is reconciled. If not, the next attempt runs. After 3 failed attempts (or if the AI gives up), the ledger is marked **not reconciled** with an explanation of what went wrong. Reconciliation is **skipped entirely** if the starting or ending balance couldn't be extracted from the statement. Screenshot2026 03 08at4 34 52PM Screenshot2026 03 08at4 34 52PM ### **Step 7 — Tagging** After reconciliation, all transactions across all ledgers are assigned a global sequential ID (1, 2, 3, ...) and then tagged. Tagging runs three parallel processes simultaneously: **AI Loan Tagging** — The AI classifies transaction groups into debt/loan types: | Tag | Display Name | | :---------------------- | :-------------------- | | merchant\_cash\_advance | Merchant Cash Advance | | bank\_loan | Bank Loan | | factoring | Factoring | | credit | Credit Card | | lease | Lease | | auto | Auto Loan | | mortgage | Mortgage | | buy\_now\_pay\_later | Buy Now Pay Later | | debt\_collection | Debt Collection | Each transaction can have at most one loan tag. **AI Core Tagging** — The AI classifies transaction groups into activity categories. The AI receives business identity and account context to make accurate calls (e.g. knowing the business name helps identify internal transfers vs external payments): | Tag | Display Name | | :----------------- | :---------------- | | internal\_transfer | Internal Transfer | | owner\_transaction | Owner Transaction | | payment\_processor | Payment Processor | | bank\_fee | Bank Fee | | bank\_interest | Bank Interest | | reversal | Reversal | | cash | Cash | A transaction can have multiple core tags. **Deterministic Pattern Tagging** — Rule-based regex matching (no AI involved) that identifies: | Tag | Display Name | | :------------- | :----------- | | check | Check | | wire | Wire | | peer\_to\_peer | P2P | | stop\_payment | Stop Payment | | nsf | NSF | | overdraft | Overdraft | NSF and overdraft tags are only applied to debits. A transaction can have multiple deterministic tags. All three tag types are then merged onto each transaction: loan tag first (if any), then core tags, then deterministic tags. ### **Step 8 — Position Detection** Positions are detected from the loan-tagged transactions. There are two methods depending on loan type: **MCA Positions (AI-based)** — For Merchant Cash Advance transactions, an AI model matches transaction groups to known funders from your org's funder registry. Each position gets a funder name, loan type, and the set of transaction IDs that belong to it. Funders from your registry include metadata like favicon, contact info, and website. **Other Loan Positions (algorithmic)** — For all other loan types (Bank Loan, Factoring, Auto, Lease, Mortgage, Debt Collection, Buy Now Pay Later), positions are detected using text similarity clustering. Transaction descriptions are compared using TF-IDF (a text similarity algorithm) and grouped into clusters. Each cluster becomes a position. ### **Step 9 — Background Analysis** Two background tasks run during the pipeline and are collected at the end: **AI Deep Research** — Started immediately after account metadata extraction (Step 1). Uses the extracted business name, address, phone, and principal names to search the web and verify the business's legitimacy. Runs in the background during the entire rest of the pipeline. The result is the "AI Deep Research" card on the Synopsis page. **Tampering Analysis** — Started after reconciliation (Step 6). Examines the PDF metadata of every uploaded document — producer, creator application, creation dates, modification dates — and looks for signs of fabrication or programmatic generation (e.g. all PDFs having identical metadata, timestamps that are impossibly close together, or creation tools not typically used by banks). Runs in the background during tagging and position detection. The result is the "Tampering Analysis" card on the Synopsis page. Both tasks are best-effort. If either one fails, the parse still completes normally. *** ## **Key Concepts** **Book** — A container for one deal or submission. A book holds one or more uploaded documents and the parsed results. When you upload files and click Parse, you're parsing a book. **Document** — A single uploaded PDF file. Gets classified into a document type (bank statement, credit report, etc.) during parsing. **Ledger** — One bank account within one statement period. A single PDF can produce multiple ledgers if it contains data for multiple accounts. Each ledger has a starting balance, ending balance, and a list of transactions. Reconciliation happens at the ledger level — each ledger is independently verified. **Account** — A bank account that spans across statement periods. After parsing, LendPathway merges all ledgers for the same account into a single unified transaction history. If you upload 12 monthly statements for the same checking account, you get 12 ledgers but 1 account. **Position** — A detected debt relationship with a specific lender. For example, if the parser identifies regular payments to "Prime Funding LLC," it creates a position grouping those transactions together with a funder name, loan type, total disbursed, and total paid. **Tag** — A label applied to a transaction that identifies what type of activity it represents. Tags are applied automatically during parsing and can be manually edited afterward. A transaction can have multiple tags (e.g. a wire payment to a lender could be tagged both "Wire" and "Merchant Cash Advance"). **Reconciliation** — The process of mathematically verifying that extracted transactions match the bank's own records. Starting balance plus the sum of all transaction amounts should equal the ending balance. A reconciled ledger means the data is accurate to within \$0.05 of what the bank reported. # Pricing with Conversions Source: https://docs.lendpathway.com/pathway-internals/pricing-tokens Usage-based billing over LLM inference. *By Armaan Kapoor* Pathway runs agentic pipelines on behalf of organizations. A lot of the actions in those pipelines have a real dollar cost: extraction, reconciliation, identity lookups, web research, sandboxed compute. Cost scales with the complexity of the work, not the number of users or features enabled. Pathway does not charge per seat and does not charge per feature. Every plan includes full access to everything. The only variable is usage. We price usage through a single unit called a conversion. A conversion wraps raw action cost into a number the org can plan around. Since cost is proportional to what the pipeline actually does, an org that wants to reduce their cost per document can turn off pipeline stages they don't need. Certain stages are auxiliary to core extraction, and they can be toggled on and off or have their strength controlled, giving the org a cost profile they manage themselves. The [pricing page](https://lendpathway.com/pricing) covers what conversions look like from the org's side. This page covers how they're constructed, the margin guarantees for every party in the chain, and the proofs that let any party at any layer verify that the only markup is the one they agreed to. Bank statement settings showing toggleable pipeline features Bank statement settings showing toggleable pipeline features *** ## What a conversion actually is Each org has a monthly subscription price **Y**, a conversion limit **X**, and a take-profit factor **TP**. The org sees Y and X. TP is internal to the party setting the pricing. Every action that costs money accumulates a dollar cost during execution. When the job finishes, the total cost feeds into one function: $$ \text{conversions} = \frac{\text{cost} \times X \times TP}{Y} $$ The ratio Y/X is what the org pays per conversion. If they're on \$500/month with 2,000 conversions, each conversion costs them \$0.25. That's the number both sides reference. TP is the multiplier between what a conversion costs the org and how much compute it actually represents. That number gets written to the job record. In the codebase, this is `calculate_conversions()`, called at the terminal state of every parse job: ```python theme={null} def calculate_conversions(cost, conversions_monthly_limit, subscription_amount_usd, take_profit_factor): quoted_price = ( subscription_amount_usd / conversions_monthly_limit if conversions_monthly_limit and subscription_amount_usd > 0 else 0.20 ) return cost / quoted_price * take_profit_factor ``` The result is stored in `parse_job_meta.conversions` alongside the action cost, document count, page count, and transaction count. Every completed, failed, or cancelled job writes this record. The org's monthly usage is the sum of those floats across all jobs. Parse history table showing per-job conversion costs Parse history table showing per-job conversion costs A month of bank statements from one account lands around 1 conversion. A 6-month deal package across 3 accounts: 8-12. Credit report: 1-2. Tax return: under 1. The numbers are stable because the work is proportional to document structure, and structure doesn't shift between runs. The unit cost is competitive with dedicated bank statement and credit report parsers despite doing significantly more work per document, which is a function of how the pipeline is built. The [parser white paper](/pathway-internals/white-paper) covers the technical details: transaction compression, parallel extraction, and reconciliation design that keeps token counts low relative to output quality. This is also how we onboard new orgs. Grant 10 conversions on a free trial. They process their first few deal packages, see real conversion costs, get a feel for the unit relative to their volume. When they're ready to scale to full deal flow, we set Y and X to match their throughput and TP to match the economics. The conversion count ramps with them. The unit stays the same. *** ## Why X doesn't control cost X appears in the conversion formula, but it doesn't appear in the cost bound. The org is blocked when total conversions reach X. Setting the sum equal to X and solving for total cost: $$ \sum_{i} \frac{C_i \cdot X \cdot TP}{Y} = X \quad \Rightarrow \quad \frac{TP}{Y} \sum_{i} C_i = 1 \quad \Rightarrow \quad \sum_{i} C_i = \frac{Y}{TP} $$ X cancels. Grant 500 or 50,000 conversions, max spend is Y/TP. At the default TP of 2.5, that's 40% of revenue, 60% margin floor. | TP | Max cost | Margin floor | | --- | -------- | ------------ | | 2.0 | 50% of Y | 50% | | 2.5 | 40% of Y | 60% | | 3.0 | 33% of Y | 67% | X is how the org thinks about their budget. TP is how we think about ours. Raising X doesn't grant more compute, it slices the same compute into finer units. The org sees more conversions, but each one is proportionally smaller. Total spend at cutoff is still Y/TP. Since there are no per-seat or per-feature charges, the conversion is the only thing absorbing the cost of everything we build. New pipeline stages, new integrations, new capabilities all flow through the same unit. The org's incentive is to process more documents. Our incentive is to make processing cheaper so we can either pass savings through to the org or improve margin. Both sides grow from the same usage. X also becomes the natural gate for operational scaling. Custom integration work, dedicated account management, white-label access, SLA tiers can all be unlocked at conversion thresholds rather than priced as separate line items. The org scales into those by growing their usage, not by negotiating add-ons. Every commercial relationship reduces to three numbers. *** ## Mid-cycle raises An org maxes out halfway through the month. We raise X₀ to X₁ without changing Y. Stored conversions from completed jobs are frozen. They were computed with the old quoted price of Y/X₀. At the new limit, they take up proportionally less of the budget. **Theorem.** *Cost ceiling after one raise:* $$ C = \frac{Y}{TP}\left(2 - \frac{X_0}{X_1}\right) $$ **Proof.** At maxout: stored conversions = X₀, cost so far = Y/TP. Remaining budget is X₁ − X₀ conversions at the new rate of X₁ · TP / Y per dollar. $$ C_1 = \frac{(X_1 - X_0) \cdot Y}{X_1 \cdot TP} $$ $$ C = \frac{Y}{TP} + C_1 = \frac{Y}{TP}\left(2 - \frac{X_0}{X_1}\right) \quad \blacksquare $$ Bounded between Y/TP and 2Y/TP. One raise can at most double the exposure. With TP ≥ 2, a single raise can never push cost above revenue, so there's real room to be flexible with orgs that need more capacity mid-month. Repeated raises accumulate. k raises after maxout, successive limits X₀ \< X₁ \< … \< Xₖ: $$ C = \frac{Y}{TP}\left(1 + \sum_{j=1}^{k}\left(1 - \frac{X_{j-1}}{X_j}\right)\right) < \frac{Y}{TP}(1 + k) $$ Linear in k. TP = 2 and three raises: cost can reach 2Y. Each phase respects TP individually, but they stack. The safe pattern is to raise Y proportionally when repeated raises are needed. *** ## Enforcement Before any action that costs money, the system checks: ```python theme={null} parse_conversions = SUM(parse_job_meta->>'conversions') # completed jobs, this month chat_conversions = chat_cost / quoted_price * TP # same formula, live total = parse_conversions + chat_conversions if total >= conversions_monthly_limit: block ``` Adding a new paid component to the platform means calling this check before it runs and writing a conversion record when it finishes. The gate sees a sum of floats against a ceiling. It doesn't know what kind of work produced them, which makes extending it to any new action the same few lines of code. A running job can overshoot by at most one job's worth of conversions, which is fine. The overshoot is bounded by document size and model context limits, not the billing system. `conversions_monthly_limit = NULL` means unlimited. Parse conversions are frozen at completion-time parameters. Chat conversions recompute live against current TP, so changing TP mid-month reprices chat retroactively but leaves stored parse records intact. *** ## Multi-tenant composition Each org has its own Y, X, TP. A platform operator white-labels Pathway, creates sub-orgs, sets pricing per tenant: ```python theme={null} @orgs_router.patch("/orgs/{org_id}/subscription") async def update_org_subscription(org_id, subscription): await session.execute(text(""" UPDATE organizations SET plan_type = :plan_type, subscription_amount_usd = :amount, conversions_monthly_limit = :limit, take_profit_factor = :tpf WHERE id = :oid AND deleted_at IS NULL """), {...}) ``` TP = 3 on a sub-org gives the operator 67% margin on that tenant's work. TP = 2 gives 50%. They can grant 10,000 conversions at \$500/month or 500 at \$500/month, same cost ceiling for them, different unit economics for their users. Admin panel showing all tenant organizations Admin panel showing all tenant organizations Clicking into any org opens the subscription modal where Y, X, and TP are configured directly. Manage Subscription modal showing Y, X, and TP fields Manage Subscription modal showing Y, X, and TP fields The operator sees their tenants' parse history the same way they see their own. Each job record carries the conversion cost, document count, pages, transactions, and time. Filterable by org, searchable by book name, with aggregate stats across the selection. Jobs dashboard with per-org filtering and aggregate stats Jobs dashboard with per-org filtering and aggregate stats Because X cancels at every level, the margin guarantee holds independently at each layer of the stack. Pathway sets TP on the operator. The operator sets TP on their tenants. Each party's margin is a function of their own TP and nothing else. On-prem deployments get the same structure. The operator sees the rate card, sees their TP, can verify the conversion math against every job record. The proofs hold for any party at any level because the algebra doesn't depend on who's running it. *** ## The broader point The conversion system is built around a specific assumption: paid tenants are high-trust relationships. The cost table is real, the formula is public, the proofs are on this page. An operator running Pathway on-prem can verify every conversion against the rate card and confirm that TP is the only markup. That trust runs in both directions. We trust our tenants enough to let them see the cost structure, and they trust us enough to let us optimize the pipeline without renegotiating every time we ship an improvement. The conversion is the contract that makes that possible. We can move to a cheaper model, add an expensive reconciliation pass, restructure the entire pipeline, and the org's bill stays predictable because we absorb the variance into TP or pass the savings through. Because X doesn't affect the cost bound, there's flexibility in how we use it. A new org can get more conversions upfront to explore the platform, and we absorb that cost in exchange for learning their usage patterns and locking in the right price sensitivity before they commit to a plan. An org scaling up mid-month can get a higher limit without renegotiating. Operators can set conversion counts that make sense for their market's expectations around unit pricing. The margin guarantee is structural. It follows from TP, and TP is set once per org. # A Look Inside the Parser Source: https://docs.lendpathway.com/pathway-internals/white-paper Reliable agentic extraction, reconciliation, and tagging over financial documents. *By Armaan Kapoor* **Abstract.** The LendPathway parser compiles [arbitrary financial documents](/platform/parser/supported-documents) (PDFs, scans, images, CSVs, structured exports) into a verified, [account-centric data model](/api-reference/endpoint/books#bank-statement-analytics) per deal. Given a set of bank statements from different institutions covering different time periods, it resolves a canonical business identity and account structure, extracts every transaction into unified per-account ledgers, and reconciles each ledger against the bank's printed balances as proof of extraction correctness. A compression layer then reduces the transaction space into semantic groups optimized for parallel classification, where deterministic taggers, LLM classifiers, and a [stateful funder registry](/platform/funder-registry) that learns from each organization's corrections jointly tag every transaction and cluster [debt positions](/bank-statements#debt-summary) by lender. This produces the [synopsis](/bank-statements), [spreadsheets](/platform/spreadsheets), and [embed views](/api-reference/endpoint/embed) that underwriting teams work from. This paper covers the bank statement pipeline. The system runs in production across revenue-based financing, term lending, and structured credit, where teams run hundreds of thousands of deals through it. *** ## 1. Underwriting and documents A merchant applies for a cash advance and submits three months of bank statements, an ISO application, maybe a credit report through a broker portal. Or a borrower applies for an unsecured term loan and submits tax returns and a tri-merge credit pull. The broker receiving this package needs to underwrite it quickly, figure out which lenders will take it, price it, submit it. The lender receiving the same package needs to determine whether the business generates enough revenue to service the advance, how much existing debt is already pulling from the account, whether the borrower is stacking multiple positions, and whether the documents are authentic. This entire industry runs on documents because the data that matters lives at the transaction level. An underwriter is not looking at a revenue number. They are reading the ledger: which deposits are real revenue versus internal transfers, which debits are MCA payments versus operating expenses, how many lenders are already in the account, whether the daily balance can absorb another position. Plaid and DecisionLogic and bank portal exports give you structured access to some of this, but they compress it through their own schemas. They normalize descriptions, drop fields, aggregate where you need line items. The bank statement is what the bank actually said happened. It is the highest-resolution record of a business's financial activity, and for underwriting at this level of detail, anything coarser loses signal. The input is not clean. Bank statements arrive as PDFs, photographed pages, DecisionLogic exports, CSV and Excel downloads. Some cover a full calendar month, some are month-to-date pulls. Credit reports come as multi-bureau exports from LexisNexis or MyScoreIQ, single-bureau PDFs, raw API output. Tax returns are scanned 1040s mixed with K-1s from different entities across different years. A single deal submission can contain all of these in the same [email](/platform/inbox). Even if every bank adopted a standard export format tomorrow, the hard problem would remain. Transactions still need to be tagged. Counterparties still need to be resolved across accounts and time periods. Debt positions still need to be clustered and attributed to specific lenders. Revenue still needs to be separated from noise. The rest of this paper focuses on the bank statement pipeline, which is where identity resolution, reconciliation, transaction compression, and position detection all live. Credit reports, tax forms, and loan applications run through their own parallel parsers. ## 2. Account resolution The bank statement pipeline receives a set of documents. Different banks, different months, sometimes different accounts for the same business. Before extracting a single transaction, the system has to establish what it is looking at. All documents get read in a single pass. The output is one canonical object: the business entity, every distinct bank account visible across all statements, and the principals. ```python theme={null} class _AccountMetadata(BaseModel): business: Business account_ledgers: List[AccountLedger] humans: Optional[List[Human]] ``` Each account gets an integer ID. Primary checking is `account_id: 1`. The full set of IDs becomes the coordinate system for every downstream step. Extraction schemas constrain output to this set. If the universe contains accounts 1 and 2, the model cannot produce transactions for account 3. Duplicate IDs are a hard failure. The system assumes unique coordinates everywhere, so it enforces them at the source. ```json theme={null} { "account_ledgers": [ { "account_id": 1, "account_name": "Business Checking", "account_number": "****4521", "account_type": "checking", "bank_name": "Chase" }, { "account_id": 2, "account_name": "Business Savings", "account_number": "****8903", "account_type": "savings", "bank_name": "Chase" } ] } ``` The principal names and masked account numbers seed downstream classification. `TRANSFER TO CHK ****4521` checked against the account universe resolves to internal transfer. A Zelle payment matching a name in `humans` resolves to owner draw. These signals only exist because identity resolution ran first. A dedup pass compares documents on period, account, and balances. Exact duplicates get excluded before extraction. ## 3. Parallel extraction N documents spawn N extraction agents concurrently. Each receives the account universe and a single document. Output is transactions grouped by account ID: ```json theme={null} { "transactions_by_account_number": [ { "account_id": 1, "transactions": [ { "transaction_date": "2024-11-01", "description": "ACH Credit - Stripe Transfer", "amount": 8412.33, "transaction_type": "credit" }, { "transaction_date": "2024-11-01", "description": "ACH Debit - Greenline Funding", "amount": 2847.00, "transaction_type": "debit" } ] } ] } ``` Amount is constrained positive at the schema boundary via `abs()` validator. Sign is carried in `transaction_type`. VLMs frequently confuse sign when debits and credits share a column, when negatives are parenthetical, or when the minus sign is a PDF rendering artifact. Forcing the schema to separate magnitude from direction eliminates this error class structurally. The model cannot produce a negative amount. After extraction, each ledger is sorted by date, walked forward from the starting balance to compute [running daily balances](/bank-statements#table-columns), and assigned local transaction IDs. This is the first point where the data has traceable shape. When extraction exceeds the model's output token limit, the system falls back to sequential chunks of \~100 transactions with deliberate overlap. The model cannot reliably resume from a position in a visual document, so the overlap region gets deduped on `(date, amount, normalized_description, type)`. ## 4. Reconciliation Every bank statement prints a starting balance and an ending balance. Walk the starting balance forward through the extracted transactions and the result has to match. Most statements also print total credits, total debits, deposit count, withdrawal count. All checkable invariants. This runs per ledger, not per document. A single PDF with two accounts produces two independent reconciliation problems. Each ledger has its own balance equation, its own credit and debit totals, its own transaction counts to verify against. This matters because extraction errors are not random. The most common failure mode is the model assigning a transaction to the wrong account in a multi-account document. A transaction that lands in account 1 instead of account 2 breaks both ledgers simultaneously: one is too high, the other too low, by the same amount. Per-ledger reconciliation catches this because the error shows up as a symmetric discrepancy across two ledgers in the same document. The primary check: $B_{\text{computed}} = B_{\text{start}} + \sum_{i} \text{signed}(t_i)$ $|B_{\text{computed}} - B_{\text{end}}| \leq 0.05$ and the ledger is verified. The balance equation produces a precise error correction signal: the magnitude and direction of any discrepancy tell the correction agent exactly what to look for. Each correction attempt is a siloed agent that receives only the current ledger state, the discrepancy, and the source document. It cannot see or trust prior attempts, so no single extraction error propagates unchecked. The secondary signals (credit sum, debit sum, transaction counts) provide additional constraints when available. If the balance reconciles but the deposit count is off, something was double-extracted or missed. When verification fails, a correction agent receives a prompt that is a pure function of the current ledger state: every transaction as a CSV row with running balances, the discrepancy magnitude and direction ("computed ending is \$2,400 HIGHER than expected, net change is too positive"), and the source document. For multi-account documents it also receives the other accounts' names and masked numbers, because that cross-account swap is exactly what it needs to look for. The agent operates through a constrained DSL: ```python theme={null} class BalanceFix(BaseModel): flip_indices: list[int] # flip debit↔credit remove_indices: list[int] # remove row add_transactions: list[Transaction] # missing rows from PDF give_up: bool explanation: str ``` It cannot rewrite descriptions or fabricate amounts. Flip a transaction's direction, remove a row, or add one it finds in the source that extraction missed. After each correction the system recomputes the balance and checks again. If the source document is internally inconsistent (mid-cycle statements, month-to-date summaries where the bank's own numbers don't add up), the agent reports why and the data proceeds flagged but usable. After reconciliation, every transaction across all ledgers receives a book-global integer ID starting from 1. This overwrites local per-ledger IDs and becomes the permanent coordinate for [tags](/bank-statements#tag-reference-global), [positions](/bank-statements#debt-summary), and [analytics](/api-reference/endpoint/books#bank-statement-analytics). The data shape then inverts: parsing produces document-centric output (document → accounts → transactions), but analytics needs account-centric output (account → transactions across all documents, sorted by date). Both views are maintained in the [canonical output](#10-canonical-output). ## 5. Transaction compression A typical deal produces \~800 transactions. The classification models need to reason over all of them. The system compresses descriptions through normalization (strip confirmation codes, ACH metadata, mask tokens, numerics, single-letter fragments) and groups transactions with identical cleaned descriptions. 800 transactions collapse to \~120 groups. Two group indexes are built from this compressed space because the two classification tasks need different views of the same data. The **loan index** strips sponsor bank names (OptimumBank, Cross River Bank, Pathward, the originating banks MCA funders route ACH through, not the funders themselves), ACH rail metadata (`ORIG CO NAME`, `CO ENTRY DESCR`, `PPD`, `CCD`), routing stopwords. Card purchases excluded. The classification model sees "Greenline Funding" where the raw description reads "ACH DEBIT ORIG CO NAME GREENLINE FUNDING CO ENTRY DESCR PAYMENT PPD". The **core index** preserves what the loan index strips. Account numbers stay because internal transfer detection depends on matching `TRANSFER TO CHK ****4521` against the account universe from §2. Semantic words ("transfer", "fee", "wire") stay. Both serialize to markdown tables. Models return group IDs rather than transaction IDs. `[1, 3]` tags two groups covering potentially dozens of transactions. Inverted indexes expand group tags back to individual transactions. ## 6. Classification Transaction groups get classified through three parallel engines: deterministic regex patterns, a core LLM, and a loan LLM. The regex layer handles the unambiguous stuff. Checks, wires, P2P, stop payments. NSF and overdraft fees are amount-gated between $0.01 and $200 because a \$3,000 debit with "NSF" in the description is a returned payment, not a fee. French-Canadian banking terminology is covered (Desjardins statements use `chèque`, `dépôt chèque`, `virement interbancaire`, `fonds manquants`). The core LLM reads the core group index with the full business context from §2. This is where the identity resolution pays off. The model can resolve internal transfers by matching masked account numbers against the account universe, and owner draws by matching names against the principals list. It also picks up payment processors, bank fees, interest, reversals, cash. The loan LLM reads the loan group index with the organization's funder registry injected into the prompt. The registry contains every lender the organization has encountered, with their known transaction description aliases. This is how the system catches MCA positions, bank loans, factoring lines, and the rest. Tags merge in priority: loan tags are non-stackable (one per transaction), core and deterministic tags stack. A transaction can carry `["merchant_cash_advance", "wire"]` but never two loan types. These tags drive every [downstream metric](/api-reference/endpoint/books#bank-statement-analytics): [true revenue, DTI, loan summaries, NSF counts](/bank-statements#metrics). ## 7. Position detection Tags tell you what kind of activity a transaction is. Positions tell you who is on the other end. For MCAs, the LLM maps tagged groups to [funder registry](/platform/funder-registry) entities, and each position gets enriched with metadata from the registry. Positions surface in the [debt board](/bank-statements#debt-summary) organized by loan type, with per-position financials computed from their [transaction ID lists](/cookbook/working-with-analytics#debt-positions). The registry is stateful per organization. Matches and aliases persist across parses, and underwriter corrections feed back into subsequent runs. The system gets better the more the organization uses it. For other loan types, character n-gram TF-IDF clusters counterparty names at τ=0.45. "GREENLINE CAPITAL", "GREENLINE CAP", "ACH DEBIT GREENLINE" collapse into one position. ## 8. Tampering detection Runs in parallel with classification. PyMuPDF extracts structural signals from each PDF: `%%EOF` count, creator/producer mismatch, creation vs modification timestamps, font inventory. A scoring model weighs these alongside reconciliation outcomes. Reconciled with suspicious metadata is a different risk profile than failed reconciliation with signs of editing. ## 9. Structured generation and model routing Every model call across the pipeline passes through a single function accepting a Pydantic `response_schema`. The LLM returns JSON, the system validates, nonconformance is rejected. Field validators enforce constraints: `abs()` on amounts, date normalization, enum coercion. The schema bounds what the model can express. We route primarily through Gemini Flash with thinking budgets tuned per task. Reconciliation gets a high thinking budget because the model needs to reason about discrepancies against the source document. Bulk extraction gets zero thinking budget because throughput matters more. VLM performance on financial documents has improved substantially over the past year, particularly on dense tabular layouts. The remaining failure modes are edge cases in visual parsing: merged cells, absent running balance columns, degraded OCR on fine print. Reconciliation catches most of these downstream. ## 10. Canonical output ```python theme={null} class HolyMCAResult(BaseModel): business: Optional[Business] humans: Optional[List[Human]] account_ledgers: Optional[List[AccountLedger]] bank_statements: Optional[List[HolyBankStatement]] merged_accounts: Dict[str, MergedMCAAccount] positions: Optional[List[StoredPosition]] transaction_count: int web_research: Optional[str] tampering_analysis: Optional[Any] ``` Every downstream surface is a derived view of this object. [Analytics](/api-reference/endpoint/books#bank-statement-analytics) are computed at read time, never stored separately. The [synopsis](/bank-statements) renders metrics, the [spreadsheet gallery](/platform/spreadsheets) generates formatted workbooks, [Salesforce sync](/platform/integrations#salesforce) pushes computed fields, the [chat agent](/platform/chat-engine) loads the full object into a sandboxed Python environment, and the [embed API](/api-reference/endpoint/embed) serves it to external clients building their own interfaces. Tags are the sole user-editable input. Change a tag, exclude a document, reassign a position, and every one of these surfaces recomputes. The hard work happens once. Everything else is a view. The broker who submitted the deal and the lender who underwrites it are both looking at derived views of the same canonical object. ***

We are looking forward to the future, and I am excited to continue building in the open.
— Armaan

# What is a Book Source: https://docs.lendpathway.com/platform/books/books A Book is a container for one deal's documents, parsed together. A Book holds all the documents for one deal. Upload the files, parse, and the Book populates with metrics, positions, scores, and income: ready to review, export, or push to your CRM. ## The Books page The [Books page](https://app.lendpathway.com/books) lists every Book in your org with its parse status, creation date, last parsed date, and who created it. Apply tags to organize your pipeline and filter by status, tag, user, or star. Books page Books page ## Create a Book From the Books page, click **New Book**. New Book button New Book button ## Upload documents Inside the Book, click **Upload Documents** and add the files. Upload documents Upload documents ## Parse Click **Parse** to start processing. Parsing runs in the background. Leave the tab and come back when it finishes. Parse book Parse book Click **Stop** to cancel a parse in progress. Stop parse Stop parse Click the bell icon in the top bar to open Parse History. It shows every parse job across your org: status, document count, page count, transaction count, conversions used, and elapsed time. Parse history Parse history Reparsing is rarely necessary. Transaction tags, debt positions, and exclusions are all configurable inside the Book without it. Reparsing replaces all existing data in the Book including transactions, loan positions, and analytics. This cannot be undone. Reparse book Reparse book ## What happens next What Pathway extracts depends on what documents are in the Book. Click one of the cards below to see what happens after you parse. Synopsis, transactions, debt positions, metrics, tampering analysis, and spreadsheets. # Chat engine Source: https://docs.lendpathway.com/platform/chat-engine An agent with a compute sandbox, connected to your data. > Every Book in Pathway has its own AI agent, Jack. It runs in a Linux sandbox with Python, a filesystem, and network access, and it works with your data using your exact permissions. This page explains how it works, from the context it starts with to the loop it runs. The Pathway chat engine The parser turns documents into a structured Book. Jack is how you act on that structure. Ask a question across the data, build a chart or a spreadsheet, pull in an email, reconcile two sources, or run an analysis no fixed report covers. The whole design follows from one decision: give the agent the same data access as the user, then give it a real computer. Scoped access keeps it safe. The computer makes it capable. Everything else on this page is detail underneath those two ideas. ## Using the chat Open a new chat or any existing one. Three controls sit around the composer: what the agent can reach, which model runs it, and how to find past chats. ### Connectors The **+** button opens the connector menu. Each toggle grants the agent one capability for this chat, and grants are per chat, so the same agent can be wired differently each time. Flip on Books and it can read your deals; add Gmail and it can read your inbox; add the Pathway API and it can query across everything you can access. Turning a connector on does two things: it hands the agent the tools for that surface and adds the instructions for using it to the agent's context. Keep the set tight. The agent works best when it carries only what the task needs. Connector menu on the composer | Toggle | What it gives the agent | | :---------------- | :--------------------------------------------------------------------------------------------------------------- | | **Attach a file** | Upload a CSV, PDF, image, or spreadsheet into the chat for the agent to read | | **Books** | Scope the chat to one or more Books so the agent reads their parsed data and analytics | | **Gmail** | Read the connected inbox through the read-only proxy, plus the approval-gated `send_email` tool | | **Slack** | Post messages to the org's connected Slack channels | | **Pathway API** | The read-only platform API: query Books, documents, analytics, funders, and more across what the user can access | | **Image Gen** | Generate images from a prompt | | **Widgets** | Render interactive UI back into the chat | Connectors map directly to the context the agent receives. Each one you enable appends its instructions to the system prompt, which is why a focused set of toggles keeps the agent sharp. See [What the model sees](#what-the-model-sees). ### Chatting about a Book Open a chat from inside a Book and that Book is attached automatically. The agent starts scoped to the deal in front of you, so you can ask about it right away. Chat panel open inside a Book You can also attach Books to any chat. Open **Books** in the connector menu, search, and check the ones you want. Pull in several at once to compare deals or ask across a set. Searching and attaching Books to a chat Inline `@` references to pull a Book into the middle of a message are coming soon. ### Model The model selector sets which model runs the chat. Pathway runs the agent on Gemini across the board, by choice, and exposes the current lineup as three tiers. Model selector with Lightning, Classic, and Pro | Tier | Model | Use it for | | :------------ | :-------------------- | :-------------------------------------------- | | **Lightning** | Gemini 3.1 Flash-Lite | Fast, lightweight questions and quick edits | | **Classic** | Gemini 3.5 Flash | The default balance of speed and depth | | **Pro** | Gemini 3.1 Pro | The hardest reasoning and multi-step analysis | ### Find past chats Open the **New Chat** dropdown to search and reopen earlier chats. The search box filters your history by title. Searching past chats ## What you can do Total deposits, true revenue, debt-to-income, a single transaction, a counterparty breakdown. The agent computes from the source data, not from a summary. Ask for a revenue trend, a cash flow heatmap, a custom Excel export, or an HTML report. The output renders inline in the chat. Connect Gmail and the agent can search the inbox, read threads, download attachments, and compare them against Book data. Sending requires your approval. Drop in a CSV, PDF, image, or spreadsheet. The agent reads it, parses it, and cross-references it against the Book. Chats don't have to be attached to a Book. A standalone chat gets the same sandbox and tools, just without a Book to read from. Use them for general computation or analysis. ## How it works Three pieces make up the engine. The agent reads only what the current user can read. Its permissions are the user's permissions. A Linux workspace where the agent runs code, writes files, and keeps its working memory. Any file the agent produces can be saved as an artifact and shown back in the chat. A turn runs as a loop over those pieces. You send a message, the model thinks, calls a tool, reads the result, and repeats until it has an answer. ```mermaid theme={null} flowchart LR U([You]) --> M(Model) subgraph loop [agent loop] direction LR M -->|tool call| T(Run tool) T -->|result| M end M -->|done| R([Answer]) classDef endpoint fill:#18181B,stroke:#18181B,color:#fff,rx:8,ry:8; classDef node fill:#F7F5F3,stroke:#D4D4D8,color:#18181B,rx:8,ry:8; class U,R endpoint; class M,T node; style loop fill:none,stroke:#D4D4D8,stroke-dasharray:4 4,color:#71717A; ``` A tool call executes, mostly against the sandbox, its result feeds back in, and the model decides what to do next. The loop continues until the model responds with text only or hits the round limit. Text and tool calls stream to the screen as they happen, and because the filesystem persists across the whole turn, each step builds on the last. The rest of this page walks through the parts: the context the model starts with, the sandbox it works in, the tools it holds, and how it reaches your data. ## What the model sees Before any tool runs, the backend assembles the context for the turn. It is built fresh each time, in a fixed order. The current date and time, and a line identifying the user and org by name and ID. This grounds the agent in who it is acting for. The core system prompt: how to use the sandbox, the tool conventions, and the rules for sharing files and computing instead of guessing. One line per skill, name and description. The full text is pulled on demand with `read_skill`. A section is appended for each capability the chat has enabled. Each one teaches the agent how to use that surface. Every prior message, tool call, and tool result, in order, so the agent has the full thread. The capability sections are the important part. A chat only carries instructions for what it can actually do, which keeps the prompt focused and the agent reliable. | Capability | What gets injected | | :--------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Pathway API | The full read-only API reference: how to read the token, list and filter Books, fetch `book_meta` and analytics, pull documents, and follow provenance back to an email thread or CRM record | | Gmail | The read-only Gmail proxy, its endpoints, and Gmail search syntax, plus the approval-gated `send_email` tool | | Salesforce | How to run read-only SOQL with `query_salesforce` and page large results into the sandbox with `export_salesforce_to_sandbox` | | Slack | The connected channels and the `post_to_slack` block format | | Image generation | The `generate_image` tool | | Widgets | How to render interactive UI back into the chat | The base instructions and skills index are always present. The rows above are added only when that capability is on for the chat. The result is that two chats can hold the same agent but see different context: a Book chat with Gmail connected gets the API and Gmail sections; a standalone chat with neither gets just the base. The whole assembled prompt is sent as the first message of the turn, followed by the conversation history. The agent reads live data through the injected APIs rather than from a frozen snapshot, so it always works against the current state of a Book. ## The sandbox Each chat gets a fresh sandbox, created from a cached snapshot in seconds. It runs Amazon Linux with Python 3.13 and full `sudo`. `pandas`, `numpy`, `matplotlib`, `seaborn`, `openpyxl`, `xlsxwriter`, `scipy`, `tabulate`, `duckdb`, `streamlit`, and `plotly` are pre-installed. Anything else installs with `pip`, and system packages install with `dnf`. The filesystem is rooted at `/tmp/`, and the network is open. Two things are placed in the sandbox before the first message: | Path | Contents | | :---------------- | :----------------------------------------------------------------------------------------------------- | | `/tmp/.api_token` | A read-only API token for the current user. The agent uses it to call Pathway APIs and the Gmail proxy | | `/tmp/uploads/` | Files the user attached to the conversation | Everything else, the agent creates: scripts, query results, charts, spreadsheets, and exports all land under `/tmp/`. Book data is not dumped to disk up front. The agent reads it live through the API when it needs it, which keeps the workspace small and the data current. Sandboxes are ephemeral. They live for the conversation and are destroyed after. If one times out mid-conversation, a new one is created from the snapshot, the token and uploads are restored, and the conversation continues. The first sandbox on the platform bootstraps the package set and takes a snapshot. The snapshot is cached in Redis with a 29-day TTL and refreshed before it expires, so every later sandbox starts from it. ## The filesystem is the memory The agent does not pass data between steps through a message protocol. It writes files and reads them back. The filesystem is its working memory. It pulls a Book's analytics from the API and saves the response to a file. It downloads a document and opens it with `view`. It writes an intermediate result and finds it again later with `ls`, `cat`, `grep`, or a script. State that matters is on disk, not held in the prompt. This is why cross-domain work needs no special integration. Asked to compare bank deposits against invoices from email, the agent: Calls the Gmail proxy to find the thread, then downloads the attachments to the sandbox. Fetches analytics from the Pathway API and saves the response to a file. A Python script reads both sets of files and produces the comparison. The email, the PDF, the parsed ledger, and the script all end up as files in the same namespace. Tools compose because they share the filesystem, the same way shell commands compose because they share stdio. ## Tools The core tools are built on two primitives: the shell and the filesystem. The primary tool. Runs scripts, installs packages with `pip` or `dnf`, calls endpoints with `curl`, pipes output. stdout and stderr come back as text. Long output is truncated at the midpoint to keep the context window manageable. Writes content to a path, creating parent directories as needed. For Python scripts the agent usually writes through a `bash` heredoc instead, which keeps the script and its run in one step. Replaces a known string in an existing file. Used to revise a script or file in place after the agent has read it. Reads a file. Text comes back with line numbers. Images and PDFs are rendered into the model's visual context, so the agent can actually see them. Reads a file from the sandbox, uploads it to permanent storage, and returns a URL through the asset proxy. The agent embeds the URL in its response. Images render inline, HTML in a sandboxed iframe, Excel as a live Office Online preview, everything else as a download link. Two of these serve different readers. `save_artifact` is for the user: a permanent URL rendered in the response. `view` is for the agent: bytes in the context window for visual reasoning. That second one closes the loop. The agent can write a script, generate a chart or spreadsheet, `view` the result, fix the script, and run it again before it ever responds. ## Skills A skill is a written set of instructions for a task the agent does often: generating a PDF with Typst, extracting tables from a scanned statement, publishing an interactive dashboard, doing web research. Skills keep the agent reliable on hard tasks without bloating its context. Each skill appears in the system prompt as a single line: a name and a short description. The full instructions stay out of context until the agent decides it needs them, then it loads them with `read_skill`. ```text theme={null} read_skill("pdf_generation") → full Typst instructions, examples, and tips ``` This is the same lazy-loading idea as the filesystem. The agent carries a small index of what it can do, and pulls the detail on demand. Skills today cover PDF generation, table extraction from PDFs, interactive dashboards, web research, and reading these docs. ## Extending the agent with code The agent does not need a custom plugin for every system. It has a real computer, so reaching something new is usually just code. * **Any API**: the agent can `curl` an endpoint or `pip install` a client and call it from Python. Anything with an HTTP interface, including MCP servers, is reachable from the sandbox. * **Reusable instructions**: drop a Markdown file or a Python snippet into the Book, and the agent reads it like any other file. A documented procedure becomes something the agent can follow; a helper script becomes something it can run. * **New packages and tools**: `pip install` for Python, `sudo dnf install` for system packages. If a task needs a library that isn't preinstalled, the agent installs it. The point is that capability is not gated behind integration work. The sandbox is a general computer with your data in it, and most "can it do X" questions reduce to "can you do X with code," where the answer is usually yes. ## Scoped platform access Every chat carries a temporary, read-only API token for the current user, written to `/tmp/.api_token`. It lets the agent call Pathway's own APIs under the user's access control: Books, documents, analytics, funders, screening settings, emails, and organization data. The Gmail proxy uses the same token. This is how the agent reasons across many Books without loading the whole organization into the prompt. It finds the Books it needs with the `query_books` tool, fetches analytics for the relevant ones from the API, and runs Python over what it is allowed to read. The token is read-only. Writes return `403`, and it is revoked when the chat ends. The agent can analyze your data but cannot change it. ## Gmail When a user connects Gmail, the agent reads the inbox through a read-only proxy at `/api/google-inbox/proxy/`, using the same API token. The proxy only accepts `GET` requests, so reading is read-only by design. Any Gmail read endpoint works through it: search messages, fetch a message or thread, list labels, download an attachment. ```python theme={null} import requests with open("/tmp/.api_token") as f: token = f.read().strip() headers = {"Authorization": f"Bearer {token}"} gmail = "https://api.lendpathway.com/api/google-inbox/proxy" # Search, then read each match r = requests.get(f"{gmail}/messages", headers=headers, params={"q": "from:john subject:invoice", "maxResults": 10}) for msg in r.json().get("messages", []): detail = requests.get(f"{gmail}/messages/{msg['id']}", headers=headers, params={"format": "full"}).json() ``` This is the pattern throughout: email is not a bespoke set of tools, it is an HTTP surface the agent reaches with code. It searches, reads the threads that matter, downloads attachments to the sandbox, and processes them with the core tools. A CSV gets loaded into `pandas`, a PDF gets inspected with `view`, an invoice image gets read visually by the model. Sending is the one exception. It runs through the `send_email` tool, which composes a message and attaches sandbox files, and the user must approve it before it goes out. Reading is read-only through the proxy. Sending requires explicit user approval on each email. ## Book data A Book chat is pointed at a Book, and the agent reads that Book's data from the Pathway API. Two endpoints carry most of it. `GET /api/books/{id}` returns the Book including `book_meta`: business identity, owner details, bank account metadata, ledger transactions with dates and descriptions, loan positions with matched disbursements and payment schedules, web research, and tampering analysis. `GET /api/books/{id}/analytics` returns the metrics: total deposits, true revenue, average daily balance, days negative, NSF and overdraft counts, debt-to-income ratio, per-statement breakdowns by account, counterparty clusters, and loan summaries by type. The Book overview tab, the Salesforce sync, and the spreadsheet generator all read from this same data. The agent reads it too, with the user's permissions. It can answer a metric question directly, or go further: cross-reference transactions across accounts, build a visualization, run a regression, or produce a report in a format the structured UI does not offer. Tags are the single input that everything downstream derives from. When you re-tag a transaction, marking a deposit as an internal transfer or assigning a payment to a position, the analytics recompute. The agent reads the updated numbers on its next call, the spreadsheets regenerate, and the Salesforce metrics update. The parser owns the structured Book. The agent works on top of it. It does not replace deterministic parsing, reconciliation, or analytics. It gives you a way to ask new questions and produce new artifacts without waiting for a new product workflow. ## Artifacts and the asset proxy Every file in the platform lives in S3 and is served through one endpoint: `/api/assets/{s3_key}`. The key holds an unguessable UUID, so the endpoint needs no separate login. It signs a short-lived URL and returns a redirect. The frontend receives a URL, checks the mime type, and renders the matching preview: images inline, PDFs in a viewer, HTML in a sandboxed iframe, Excel in Office Online, everything else as a download link. It does not distinguish between a chart the agent just made and a spreadsheet the parser produced. Both are just files behind the same proxy. For Excel files, adding `?embed=office` returns a fresh Office Online embed URL instead of a redirect. This powers the interactive spreadsheet previews in chat, in the Book's spreadsheet tab, and in the embed API. ## State Two things hold the state of a chat: the sandbox filesystem and the conversation history. Files on disk, messages in the database. That is the whole system. A shell, a filesystem, and two additions on top: `save_artifact` to publish a file, and `view` to let the agent see one. Simple primitives that compose into the work. # Funder Registry Source: https://docs.lendpathway.com/platform/funder-registry A per-organization knowledge base of lenders, MCA funders, and their transaction description aliases. ## Overview The funder registry is a persistent, org-scoped directory of every lender and funder your organization has encountered. Each entry carries metadata (contact info, type, states served) and, critically, a list of **transaction description aliases** — the strings that appear in bank statement ACH descriptions when that funder pulls or pushes money. When the parser runs, the registry is injected into the loan classification and position detection prompts. The LLM uses the aliases to match transaction groups to known funders, even when the raw description is buried under sponsor bank names and ACH rail metadata. A transaction reading `ACH DEBIT ORIG CO NAME GREENLINE FUNDING CO ENTRY DESCR PAYMENT PPD` gets resolved to "Greenline Funding" because the registry carries that alias. *** ## How it works **Two-tier initialization.** Pathway ships a set of global default funders (the major MCA companies, common lenders). When your organization first accesses the registry, these defaults are atomically copied into your org's own namespace. From that point forward, your copy is independent — you can add funders, remove ones you'll never see, and customize aliases without affecting anyone else. **Prompt injection.** During classification, the registry serializes into a reference table inside the LLM prompt: ``` funder_id, funder_name, funder_aliases 1, Greenline Funding, ['GREENLINE FUNDING', 'GREENLINE CAP', 'GRN FUNDING'] 2, Libertas Funding, ['LIBERTAS', 'LIBERTAS FUNDING LLC'] ... ``` The model returns funder IDs rather than free-text names. Each matched position gets enriched with the full funder record — contact info, website, type — so the [debt board](/bank-statements#debt-summary) can display it without a second lookup. **Corrections feed forward.** When an underwriter reassigns a position to a different funder or adds a new alias, that correction lives in the registry for every subsequent parse. The system does not need to re-learn what it has already been told. Over time, each organization's registry converges toward a complete map of the funders they actually encounter. *** ## Managing funders Access the funder directory from your organization settings, or through the embeddable [funder directory](/api-reference/endpoint/embed#embed-routes) view. Each funder entry supports: | Field | Purpose | | ----------------- | ----------------------------------------------------------------------------- | | **Title** | Display name (e.g. "Greenline Funding") | | **Aliases** | Transaction description strings the parser should match against | | **Type** | Lender, MCA funder, factoring company, debt consolidator, ISO/broker, or bank | | **Contact** | Name, email, phone, address — surfaces on position cards in the debt board | | **States served** | Geographic coverage, useful when submitting deals | | **Rank** | Sort order in your directory | You can: * **Add** funders your org works with that aren't in the defaults * **Edit** aliases when you encounter a new description variant for a known funder * **Delete** funders you'll never see * **Reset** to global defaults if needed *** ## API Funder management is available through the API for organizations that want to sync their registry programmatically. ``` GET /funders # List all funders for your org GET /funders/:id # Get a single funder POST /funders # Create a new funder PATCH /funders/:id # Update funder metadata or aliases DELETE /funders/:id # Remove a funder POST /funders/reset # Reset to global defaults ``` The `transaction_description_aliases` array in `funder_meta` is the field that drives classification. Adding an alias here is the most direct way to teach the parser about a new description variant. # Inbox Source: https://docs.lendpathway.com/platform/inbox Receive deal submissions by email. Pathway creates and parses the book automatically. When someone sends documents to your org's unique email address, Pathway creates a Book, uploads the attachments, classifies them, and starts parsing. No manual upload needed. Find the Inbox in the left sidebar under **Documents > Inbox**. *** ## Setup Click **Configure** in the top-right corner of the Inbox page to open Email Processing settings. Screenshot2026 03 16at7 07 20PM Screenshot2026 03 16at7 07 20PM **1. Enable email processing** Toggle the switch to **Active**. When disabled, incoming emails are ignored. **2. Configure allowed senders** Only emails from whitelisted addresses are processed. This prevents random or spam emails from creating Books. * Type an email address and click **+** (or press Enter) to add it * Click **Add Team** to bulk-add all org members at once * Hover over any address and click **X** to remove it **3. Copy your org's email address** Each organization gets a unique address in the format: `ai+@lendpathway.com` For example: `ai+acme-inc-52b92@lendpathway.com`. This is the address people send documents to. Click **Save Changes** before closing. Unsaved changes are lost. Inbox configuration Inbox configuration *** ## How it works 1. A sender on the allowed list sends an email with PDF attachments to your org's email address 2. Pathway creates a Book, uploads all attachments, classifies each file, and starts parsing 3. The thread appears in the Inbox with its processing status 4. The sender receives a reply email with the parsed results Emails from senders not on the allowed list are ignored. Document classification is automatic. Mix bank statements, credit reports, tax forms, and loan applications in a single email and Pathway classifies each one correctly. *** ## Inbox view The Inbox lists email threads sorted newest first. **Toolbar:** * **Search**: filter by subject, sender name, sender email, or snippet * **Date** filter: filter by date range * **Sender** filter: filter by specific sender **Each thread row shows:** | Element | Description | | :--------------- | :--------------------------------------------------------------------------------- | | **Subject** | Email subject line | | **Sender** | Name and email address | | **Attachments** | Count of attachments | | **Date** | When the latest message arrived | | **Status badge** | **Completed** (green), **Failed** (red), or **Cancelled** (yellow) | | **Info badges** | Once parsed: Identity, Accounts, Documents, and Pass/Autodeny badges appear inline | Clicking a thread with a linked Book navigates to that Book's page. Unread threads with no linked Book show a blue dot. Inbox view Inbox view *** ## Reply email When bank statements are parsed successfully, the original sender receives a reply in the same thread with a synopsis of the results. The reply includes: * Business name, statement count, and date range * Autodeny or Pass screening badge (if screening is configured) * Link to the full Book in Pathway * Six KPI cards: Avg Monthly Revenue, Avg Daily Balance, True Revenue, Days Negative, NSF, & Overdraft * Debt positions table if MCA positions were detected * Breakdown by month * Book Summary Chart Inbox reply email Inbox reply email *** ## Triage After parsing, each thread row shows Identity, Accounts, Documents, and Screening badges inline. This lets you triage submissions from the Inbox without opening the Book. * **Completed + Pass**: parsed and cleared screening * **Completed + Autodeny**: parsed but failed screening rules * **Failed**: error during parsing Screenshot2026 03 16at7 07 20PM 1 Screenshot2026 03 16at7 07 20PM 1 Screenshot2026 02 26at10 15 07PM 1 # Integrations Source: https://docs.lendpathway.com/platform/integrations > LendPathway connects to the tools your team already uses. All integrations live under the **Connectors** section in the sidebar. *** ## **Gmail** Connect your Google account to manage email directly inside LendPathway. Each user connects their own inbox via OAuth — LendPathway never stores your password. ### **Connecting** 1. Go to the **Integrations** page (under Connectors in the sidebar) 2. Click the **Gmail** card 3. Sign in with Google and grant read and send access 4. When the popup closes, your inbox is connected Screenshot2026 03 15at11 05 02PM Screenshot2026 03 15at11 05 02PM You can disconnect at any time from the same Integrations page. Disconnecting removes stored tokens immediately. ### **What you can do** Once connected, click **Gmail** in the sidebar to open your inbox. Screenshot2026 03 17at7 58 55PM Screenshot2026 03 17at7 58 55PM * **Browse messages** — Switch between Inbox, Sent, Starred, and Trash. Search across all messages. Filter by All or Unread. * **Read threads** — Open any email thread and view the full conversation. Download attachments directly. * **Send and reply** — Compose new emails or reply inline with support for CC, BCC, and file attachments. Screenshot2026 03 17at8 01 15PM Screenshot2026 03 17at8 01 15PM * **Contacts** — LendPathway syncs contacts from the last six months of your inbox (From, To, and CC fields). You can also add contacts manually. Screenshot2026 03 17at8 05 07PM Screenshot2026 03 17at8 05 07PM *** ## **Google Drive** Export any generated spreadsheet to Google Drive as a native Google Sheet. ### **Connecting** 1. Go to the **Integrations** page (under Connectors in the sidebar) 2. Click the **Google Drive** card 3. Authorize LendPathway to create files in your Drive You can also connect from the **Google Drive** page itself — if you're not connected yet, a **Connect Drive** button appears at the top. LendPathway only requests the `drive.file` scope, which limits access to files LendPathway creates. It cannot read or modify your existing Drive files. Screenshot2026 03 15at10 34 03PM 1 Screenshot2026 03 15at10 34 03PM 1 ### **Exporting a spreadsheet** 1. Open a book with completed analytics 2. Go to the **Bank Spreadsheet**, **Credit Sheet**, or **Tax Sheet** tab 3. Click **Open in Drive** in the floating toolbar at the bottom 4. LendPathway uploads the workbook and converts it to a native Google Sheet 5. The Sheet opens in a new tab Available for Excel-based templates only. If your access token expires, LendPathway refreshes it automatically — no need to reconnect. ### **Google Drive page** Click **Google Drive** in the sidebar to see all spreadsheets LendPathway has exported to your Drive. You can search by name and click any file to open it in Google Sheets. Screenshot2026 03 15at10 44 46PM Screenshot2026 03 15at10 44 46PM *** ## **Salesforce** Sync parsed deal data to Salesforce Opportunities. Map LendPathway metrics to your existing fields, and optionally let LendPathway auto-parse new deals as they enter your pipeline. ### **Connecting** > [See the Salesforce Connector page for more information](https://docs.lendpathway.com/platform/salesforce-connector) ### **Browsing Opportunities** Once connected, click **Salesforce** in the sidebar to browse Opportunities. * **Filter** by stage, owner, or search by name * **Sort** by recently modified or oldest created * **Select an Opportunity** to view its details and attached files in a side panel * **Create a book** from an Opportunity's files — with an optional **Auto-parse** checkbox that starts parsing immediately and syncs results back to Salesforce when done (image of the Salesforce page showing the Opportunities list with an Opportunity selected and detail panel open) ### **Field Mappings** Map LendPathway analytics to Salesforce Opportunity fields so parsed results push back to your CRM. 1. On the **Salesforce** page, click **Configure Attributes** in the top-right toolbar 2. For each LendPathway metric, select the target Salesforce field 3. LendPathway validates that the Salesforce field type is compatible before saving Available metrics: | **Metric** | **Type** | | :------------------------------ | :------- | | Average Daily Balance | Currency | | Total Negative Days | Number | | Average Negative Days (Monthly) | Number | | Beginning Balance | Currency | | Ending Balance | Currency | | Total Deposits | Currency | | Average Monthly Deposits | Currency | | Average Monthly Withdrawals | Currency | | Average Monthly Deposit Count | Number | | Total True Revenue | Currency | | Average Monthly True Revenue | Currency | | Total Revenue Transactions | Number | | Total Loan Payments | Currency | | Average Monthly Loan Payments | Currency | | Business Name | String | Screenshot 2026 04 09 At 6 25 38 PM Screenshot 2026 04 09 At 6 25 38 PM ### **Pushing Results** After a book is parsed, you have two options: * **Manual push** — Open the book, click the **Salesforce badge** in the top-right info badges, and confirm. * **Automatic sync** — If the book was created from a Salesforce Opportunity with **Auto-parse** enabled, analytics push automatically after parsing completes. ### **Auto-Parse** Auto-parse monitors your Salesforce pipeline and creates books for new Opportunities that match your criteria. 1. On the **Salesforce** page, click **Auto-Parse** in the top-right toolbar 2. Configure: * **Required stages** — Only Opportunities in these stages are picked up * **Require attachments** — Skip Opportunities with no files * **Checkbox field** (optional) — A Salesforce checkbox that must be true to trigger * **Parse complete field** (optional) — A Salesforce field LendPathway sets to true when parsing finishes LendPathway polls every 5 minutes. New matching Opportunities are created as books, parsed, and synced back without manual intervention. Auto-parse creates books and consumes usage. Monitor your usage to avoid unexpected overages. Screenshot 2026 04 09 At 6 28 01 PM *** ## **Embed API** Build LendPathway into your own application. Submit documents via the API, embed parsed results in an iframe, and receive webhooks when processing completes. The Embed API uses **Personal Access Tokens (PATs)**, not OAuth. See the [API Reference](vscode-file://vscode-app/api-reference) for full endpoint documentation. ### **Authentication** To create a PAT: 1. Click your **org name** at the bottom of the sidebar to open the org switcher 2. Click **Settings** 3. On the **Account** tab, scroll to **API Access Tokens** 4. Click **New Token**, give it a name, and copy the token Include the token in every request as a Bearer token in the `Authorization` header. Tokens are hashed at rest — LendPathway stores only the SHA-256 hash, not the raw token. Treat your token like a password. (image of the API Access Tokens section in Settings showing a token and the New Token button) ### **Submitting Documents** `POST /api/submit-book` accepts multipart form data. | **Field** | **Required** | **Description** | | :------------ | :----------- | :---------------------------------------------- | | `files` | Yes | One or more PDF files | | `book_name` | Yes | Name for the book | | `description` | No | Optional description | | `webhook_url` | No | URL to receive a callback when parsing finishes | LendPathway creates a book, uploads the files, and starts parsing in the background. The response returns immediately with the new `book_id`. ### **Webhooks** If you provide a `webhook_url`, LendPathway sends a POST request when processing finishes: ```json theme={null} { "book_id": "uuid", "status": "completed", "book_url": "https://app.lendpathway.com/books/" } ``` If parsing failed, an `error` field is included with a description. ### **Embedding Results** Embed parsed analytics in your UI with token-authenticated iframes. 1. Create an embed token: `POST /api/embed/token` with `book_id`. Tokens expire after 24 hours by default. Pass `permanent: true` for tokens that don't expire. 2. Build the iframe URL: `https://app.lendpathway.com/embed/book/{embed_token}` 3. Drop that URL into an `