Bank feeds¶
SAE Books ingests bank transactions via the CDR (Consumer Data Right) open-banking framework, brokered through an aggregator (currently SISS Data Services / ACSISS). The integration is generic over the aggregator — adding a second provider is a contract implementation, not a rewrite.
Provisional
Bank-feed onboarding is gated behind manual NDA + consumer-consent
flows administered by the aggregator. The integration code lives
in saebooks.services.bank_feeds but the production wiring is not
yet enabled in the community edition. Self-hosters who want to
run feeds today need to plumb in their own aggregator credential
pair and accept the NDA terms directly.
Why CDR¶
Australia's CDR regime is the closed door behind which "screen scraping" used to live. Going through a CDR-accredited aggregator gives:
- A consent UI the consumer trusts (it lives at the bank)
- Refresh tokens with explicit lifetimes the consumer can revoke
- Standardised JSON shapes across all major banks
- A defensible audit trail when the ATO or the consumer asks "where did this data come from?"
The downside is that aggregator integration is paperwork-heavy — there's an NDA, a fitness-and-propriety check, and quarterly attestations. SAE Books abstracts that out so an end customer sees "connect bank" and not "fill in this 38-page form".
Flow¶
The pull cadence is configurable per tenant — the default is daily, but high-volume tenants run hourly. Webhooks from the aggregator trigger an out-of-band pull when fresh data is available, so users rarely wait 24 hours to see a transaction.
Data model¶
Incoming bank lines land in bank_statement_lines with one of four
states:
UNMATCHED— no journal yet; awaiting rule or human reviewPARTIAL— partially allocated (split across multiple targets)MATCHED— fully reconciledIGNORED— explicitly excluded (test transactions, duplicates)
Allocations are recorded as rows in bsl_matches, a junction table
that links a bank statement line to one or more targets (each
target_type is either PAYMENT or JOURNAL_ENTRY). A single
deposit can be split across multiple payments — each split gets its
own bsl_matches row. The sign convention is that amounts on deposit
lines are positive and withdrawal lines negative.
Each bank_statement_line also tracks which bank_feed_account_id
it came from (the aggregator-side account link) and an external_id
used to deduplicate on resync.
Matching pipeline¶
Incoming bank lines run through a rules pipeline, in order:
- Bank rules — per-company rules matched against the line
description using one of four match types:
CONTAINS,STARTS_WITH,EXACT, orREGEX. Rules are ordered bypriority(descending) thenname. The first matching rule wins. If the rule hasauto_create=Truea posted journal entry is created automatically; otherwise the rule is suggested for human confirmation. - Manual classification — lines with no matching rule fall through to the operator UI for manual account selection.
Rules map a bank line to a GL account and optional tax code, and can also associate a contact (customer/supplier) and a custom description template.
Reconciliation¶
Reconciliation runs as an offline sweep (saebooks.services.bank_feeds.reconcile).
For each active feed account the sweep computes:
- Feed total — sum of all
bank_statement_lines.amountfor the feed - GL total — sum of
journal_lines(debit − credit) on the linked ledger account forPOSTEDandREVERSEDentries - Variance —
feed_total − gl_total; anything over $0.01 is flagged - Unmatched count — lines still in
UNMATCHEDstatus - Staleness — last statement line older than 7 days
Each feed account gets a severity: ok, warn (unmatched lines), or
error (stale feed or GL variance). The operator dashboard surfaces
this at /admin/bank-feeds/health.
The sweep is read-only — it writes nothing. The CLI entry point is
python -m saebooks.cli reconcile-feeds.
Failure modes¶
- Consent expired — consumer has revoked the bank's consent or the 12-month CDR cap has lapsed. The user sees a "reconnect" CTA; no transactions are lost, just paused.
- Aggregator outage — the daily pull fails, the next pull catches up automatically. Webhooks may be delayed.
- Bank schema change — the aggregator usually absorbs these; if
it doesn't, the integration logs a structured error referencing
the offending field and the row is left in
UNMATCHEDstatus.
All three failure modes are visible in the operator dashboard at
/admin/bank-feeds/health.