REST API · Python · Accounting
Bank Matching API
FastAPI service that matches bank lines to bills
- REST API
- OAuth / SSO
- Workflow automation
An internal tool for an accounting firm: staff import a client's bank CSV, a matching engine pairs outgoing payments with the client's open bills, and staff approve, reject or categorise each line. The Python REST API sits behind the Next.js app, which calls it with a short-lived signed token, and every action is audited.
/ What it does
- [01]Versioned REST API (/api/v1) in FastAPI, with interactive OpenAPI docs at /docs.
- [02]Service-to-service auth: Next.js signs a short-lived HS256 JWT per request, and the API checks its expiry, issuer, audience and which clients the user may access.
- [03]Parses five bank CSV layouts (CBA, Westpac, ANZ, NAB, generic); re-uploading a file creates no duplicates.
- [04]Matching rules: amount equal to the cent, a date window, and fuzzy vendor names that ignore noise like "PTY LTD".
- [05]Built behind an AccountingRepository interface, so QuickBooks Online can replace the local tables without touching the UI.
/ How it works
From request to result
- Person
- API call
- Data
- [01]
Staff
Uploads a client's bank statement CSV
Next.js server action
- [02]
Next.js server
Signs a short-lived JWT; the browser never calls the API
Authorization: Bearer <HS256 JWT>
- [03]
FastAPI
Detects the bank layout, parses to integer cents, de-duplicates
POST /api/v1/clients/{id}/statements
- [04]
Matching engine
Exact / check / no-match against open bills
POST /api/v1/clients/{id}/reconciliation/run
- [05]
Staff
Approves, rejects or categorises each line
POST …/reconciliation/approve
- [06]
Audit log
Every decision recorded with who and when
GET /api/v1/clients/{id}/audit
/ API surface
- GET
/api/v1/clientsClients this user may access (from the token's claims)
- POST
/api/v1/clients/{id}/statementsImport a bank CSV (201 Created)
- POST
/api/v1/clients/{id}/reconciliation/runRun the matching engine
- POST
/api/v1/clients/{id}/reconciliation/approveApprove matched lines
- POST
/api/v1/clients/{id}/transactions/{tid}/categoriseAssign an account from the chart of accounts
- GET
/api/v1/clients/{id}/auditAudit trail
/ From the code
def get_current_user(creds = Depends(_bearer)) -> CurrentUser:
if creds is None:
raise HTTPException(401, "Missing bearer token")
s = get_settings()
try:
claims = jwt.decode(
creds.credentials,
s.internal_api_secret,
algorithms=["HS256"],
audience=s.jwt_audience,
issuer=s.jwt_issuer,
options={"require": ["exp", "sub"]},
)
except jwt.PyJWTError:
raise HTTPException(401, "Invalid or expired token") from None/ Built to be reliable
- Money stored as integer cents and parsed with Decimal ("$1,150.00", "(85.50)", "85.50 DR").
- Client isolation: every row carries client_id, and the token says which clients a user can see.
- Re-importing the same statement is safe: duplicates are detected.
- 52 automated tests across parsing, bank layouts, matching rules and the approve flow.
- Next step: QuickBooks Online over OAuth 2.0, with requestid idempotency on every write.
/ Screens

Next project →
CareRoster
Rostering platform for NDIS providers
