Accounting export
Posted ledger activity is consolidated into one balanced journal entry per business date and pushed into QuickBooks Online — cursored by entry identifier so a backdated correction can never be stepped over, and idempotent in three independent layers.
A lender’s books do not live in the servicing platform. They live in the accounting system the controller closes every month, and for most lenders that is QuickBooks Online. Without an export, the path between them is a person reading a report and typing journal entries by hand — monthly, under time pressure, at the exact moment errors are most expensive.
The accounting export makes that path automatic, scheduled, and auditable. It is deliberately narrow, and the boundaries matter as much as the capability — the ledger stays authoritative throughout.
The export does
- One balanced entry per business date
- Pushed into QuickBooks Online on a schedule
It deliberately is not
- A source of truth — QuickBooks holds a copy, never a fact about a loan
- A posting engine — a second derivation is a second ledger that can disagree
- A two-way sync; nothing flows back
- A bookkeeping product — no bank feeds, no period close, no chart of accounts
The connection
Requests carry an explicit minor version rather than accepting whatever the default happens to be, so the next version bump is a deliberate one-line change reviewed by a person instead of a silent change in behaviour discovered at close.
Three properties of the authorization flow are non-negotiable, because getting any of them wrong loses the connection or leaks the grant:
The redirect lands on the platform, not the browser app. An endpoint receiving an authorization code must redirect rather than return a page, so the code cannot leak through page resources or a referrer header. The platform’s own connector route validates the request, exchanges the code, stores the grant, and only then sends the operator back to the administration screen. The authorization code never reaches the single-page application.
The state parameter carries the tenant. A vendor redirect is a plain browser navigation with no tenant header, so state carries the tenant identifier plus a single-use, short-lived, server-minted nonce, compared in constant time and consumed on use. The tenant identifier is not the secret; the nonce is.
Token refresh is single-writer. The refresh token rotates on roughly a daily clock, and if two refreshes race, the loser fails and the winner’s token can be revoked along with it — destroying the connection. Refresh therefore takes a row lock, re-reads before refreshing, and persists whatever token comes back in the same transaction as the call that produced it, whether or not it changed.
The cursor is an entry identifier, never a date
A correction posted today can carry last month’s effective date: the ledger reprocesses retroactively from the original transaction date, which is what makes a backdated adjustment correct. A date-keyed cursor would step straight over that correction — it is older than the last exported date — and the books would diverge silently, in the direction of understating a change that was made deliberately.
So each batch is a half-open range of journal entry identifiers, from the last exported identifier up to the highest currently readable one. The identifier range decides what is included, which is a completeness property. The business date decides how it is presented and dated, which is a bookkeeping property. Separating the two is what lets a backdated correction be both exported and correctly dated.
Consolidation, and why it is not optional
A single loan payment produces several journal entries — principal, interest, fees, penalties — each with its own debit and credit legs. A one-for-one feed would push millions of rows into an accounting package that is not built to hold them and that nobody could read.
Activity is therefore consolidated: one balanced entry per business date, one line per ledger account. A day on which four thousand loans took a payment becomes four or five lines rather than sixteen thousand. Per-loan detail stays queryable in the platform and can be attached to the entry as supporting detail, so an accountant can open the underlying schedule from inside their own books.
Batches partition by currency first — one entry carries one currency, with an explicit exchange rate rather than whatever rate the accounting system would have picked. Account totals are then rounded to the currency’s minor unit, and because independently rounded totals can miss balance by a cent, the residual posts to an operator-mapped rounding account. A residual larger than the configured tolerance fails the batch instead of posting it, because a large residual is not a rounding artifact — it is a mapping fault wearing one.
The mapping is the operator’s, and it is effective-dated
Every ledger account maps to a destination account, is marked do not export, or remains undecided. An undecided account is a decision the platform does not have, so a batch containing one fails, naming the account, rather than guessing or dropping the line.
Mappings are effective-dated. Re-pointing interest income six months from now must not rewrite what six months of already-posted batches say they contained — so what was sent stays recorded as sent, independently of what the mapping says today.
Three rules are enforced at configuration time rather than at posting time, because two in the morning is the wrong moment to discover them:
- A line hitting receivables or payables must name a customer or vendor. The mapping screen refuses such a mapping without one.
- Class and department references are sent only when the destination company’s preferences enable them, read live rather than assumed.
- An inactive destination account cannot be posted to. Accounts are deactivated rather than deleted in accounting systems, so active state is re-validated before every run and never cached from the day the mapping was made.
Corrections are always reversing entries
A journal entry cannot be voided. It can only be deleted, and a delete leaves no trace behind it. Delete-and-repost would therefore erase the evidence that a correction happened at all.
So corrections are always reversing entries, never delete-and-repost. This is a hard rule in the exporter rather than a preference, and it matches how the platform’s own ledger behaves: posted lines are never edited, and a correction is an explicit adjustment that leaves the original in place.
Idempotency, in three independent layers
No single layer is sufficient, so all three run:
- A stable request key. Derived deterministically from the batch, stored before the first attempt, and reused byte-for-byte on every retry — never regenerated per attempt.
- Query before retry. Before retrying a create whose outcome is unknown, the batch’s document number is looked up in the destination. A hit means the first attempt succeeded; the entry is adopted rather than created again.
- The platform’s own batch record. Written before the call and reconciled after — the only layer whose retention the platform controls.
A duplicate-request rejection from the accounting system is a success: it means the original write landed. It resolves to the existing entry and is never retried.
Every entry carries a deterministic document number — the natural lookup key that makes a retry resolvable, and what lets a controller recognize the platform’s postings in their own ledger — plus a source note recording the batch, the business date, the entry-identifier range, and the loan count. The destination’s own audit log is not reachable through its API, so without that note nobody could later tell where an entry came from.
Failures that must not be retried
| Condition | Treatment |
|---|---|
| A success response carrying a fault | A failure. Validation faults arrive inside HTTP 200 responses; a status-only check would mark unposted batches as posted |
| Rate limited | Back off. The backoff does not depend on a retry-after header, because one is not always sent |
| Server error | Retryable |
| Timeout | Outcome unknown. Never resubmitted blind — resolved by document-number lookup first |
| Debits do not equal credits | Not retryable. The consolidation is wrong; the batch fails and raises review |
| Accounting period closed | Not retryable. A person decides whether to reopen the period or restate |
| Invalid account reference | Usually a mapped account was deactivated. Fails naming the account and marks the mapping stale |
| Authorization failed after a fresh refresh | The grant is gone. The connection moves to reconnect-required |
A closed period is handled by holding the entry, not by silently re-dating it. The default is that an entry belongs on the day it corrects — that is what makes the books tie to the servicing system period by period — and when that day falls in a closed period, the review names both dates so an operator can release it into the open period with the original date recorded in the memo. The platform does not write the closing date, and would not if it could.
Provider errors never carry a borrower identifier, a credential, a URL query, or a raw vendor body. The vendor’s own correlation identifier is recorded on the batch, because that is the reference their support will ask for.