Accounts
Credit-card terms on the Account
Credit limit, statement balance, minimum payment and payment due date were not available on the API. They are now, on a newliabilities object:statement_balance is signed like current_balance: what the client
owes is negative, so the two agree on the same account. That sign is
normalized rather than reported, because banks disagree with each other
about it. One consequence: a card that was in credit at statement close is
reported as owed. credit_limit, available_credit and minimum_payment
are always positive.payment_due_date is only as current as the last refresh, so a date in the
past means the cycle rolled over before our last pull, not that a payment is
overdue. See
Credit-card terms.TransactionsAccounts
Fetching a single transaction works on every source, and accounts now say how far back they go
GET /v3/transactions/{id} resolves ids from all four sources. It only
ever worked for Finicity and uploaded-statement transactions. An MX id came
back 404, and a portal-connection (FDE) id failed outright, even though
both were listed by GET /accounts/{id}/transactions moments earlier. An id
we hand you now resolves.FDE transactions carry account_id. The field is documented as required
and was arriving null on that source, so there was no way to tell which
account a transaction belonged to without tracking the request you made to
get it.New oldest_transaction_date on the Account. It answers “how much
history do I have here”, per account:date
you get if you page that account’s whole history. It is null when we hold
no transactions for the account yet, and it moves backwards if a backfill
lands earlier data, so read it rather than caching it. See
How far back does the history go?.The field is optional and nullable, so it is additive for existing clients.Accounts
Account type is now guaranteed, and balances are signed consistently
Three things about the Account object were true in the reference but not in
the responses. They are true now, and a contract test keeps them that way.type is always one of the six documented values, and never null.
It used to be a pass-through of whatever the underlying aggregator called the
account, so a Finicity credit card arrived as creditcard, an
uploaded-statement one as credit card, and only MX ever sent the documented
credit_card. Accounts from portal-based connections sent no type at all.
If you branch on type === "credit_card" you will now match every credit
card, on every source.New subtype field. The coarse enum discards real detail, so the
source’s own label now travels alongside it: money_market, mortgage,
line_of_credit, ira, plan_401k, bank, and so on. It is an open
vocabulary rather than an enum, so new values are never a breaking change.
Branch on type; use subtype for display or for finer routing you control.If you were relying on the old raw values, subtype is what you were
reading before, so switching your comparison from type to subtype gets
you exactly the previous behaviour.current_balance is signed from the account holder’s point of view.
Positive is value the client holds, negative is value they owe, on every
source. Previously the sign followed whichever aggregator supplied the
account, so the same credit card could be positive or negative depending on
how it happened to be connected. Summing current_balance across a client’s
accounts now gives net worth directly.A real credit balance on an overpaid card is still positive, and a checking
account in overdraft is still negative, so the sign stays meaningful in both
directions. available_balance is unchanged: on a card it is available
credit, not a debt, and it is never sign-flipped.The account.refresh.completed webhook is signed the same way, so the value
you read from GET /accounts and the value you receive on the event agree
for the same account.available_balance capability now reports honestly. It was declared
never for every source while MX accounts were returning a real value. MX is
now sometimes, which is accurate: MX reports it for many checking accounts
and few credit cards. Every other source is genuinely never.Also fixed: the transactions endpoint told you to check a status field that
does not exist. The field is the pending boolean.Connections
Refresh now pulls transactions, and its operation actually finishes
POST /v3/connections/{id}/refresh on a Finicity connection re-aggregated
balances only. Transactions were not pulled, last_refreshed_at never moved,
and no account.refresh.completed webhook fired - so the operation it handed
you stayed queued forever and polling it never returned a terminal state.
Fixed. The call also answers in milliseconds now instead of blocking.- Transactions are pulled and
last_refreshed_atadvances, so re-readingGET /v3/accountsafter the webhook shows what you would expect. - The operation reaches a terminal state. A refresh is one aggregation at
the bank but reports per sub-account, so the operation turns
succeededorfailedonce every account on the connection has reported - and resolves on its own if the events never arrive, so polling always terminates. - Partial failures are reported as failures, not as partial successes. If
the aggregation ran but one account did not come back, you get
refresh_partially_failedwith a per-account breakdown inerror.accounts[], so branching onstatusalone cannot quietly ship a connection that is missing an account’s transactions. - New error codes:
refresh_partially_failed,refresh_timed_out,refresh_outcome_not_observable,no_refreshable_accounts. - Refresh now has a daily budget separate from the request-rate limit,
since each one is a real aggregation at the institution. Over it, the call
returns
429 rate_limit_exceededwithout touching the bank. estimated_secondson the202was raised from10to120to match how long a refresh actually takes. It is a best-effort floor, not a deadline.
Connections
Reauthorize (fix) a broken connection in place
NewPOST /v3/connections/{connection_id}/reauthorize — repair a connection
that reports status: requires_action without minting new ids.- Returns a
reauthorizeaction with a hostedreauth_url; redirect the end user there to re-enter credentials or re-consent at the institution, then wait forconnection.active(orconnection.failed). - Keeps the same
con_/acc_/txn_ids, unlike starting a fresh connection — so a delta re-sync over the overlap window dedupes cleanly against transactions you already have. - Available for
FINICITY,MX, andFDE. Uploaded-statement (PDF) sources are read-only and have no connection to reauthorize.
ChecksFDE
Check images are now readable from the API
Thecheck_images capability has had a read path since day one on the data
side, but no route to fetch it. Three endpoints now serve it, nested under the
owning account:- Requires the new
read:checksscope. Existing keys don’t have it — tick it on a new key in the portal, orPATCH /v3/api-keys/{id}to widen the key you already have without rotating its secret. - FDE-only. Non-FDE accounts return an empty list rather than an error, so you can call this uniformly across sources.
- There is no back image — LedgerSync captures the front of a check only, so there is no
sidefield. - Bank-reported fields and
ocr.*are kept separate, so you can always tell what the bank said from what OCR read off the paper. - The sandbox Ledgersync Bank (
ins_a7397a8d0656e1b7) returns real check images, so the whole flow is testable end-to-end.
ConnectionsMX
On-demand refresh now works for MX connections
POST /v3/connections/{id}/refresh now supports MX (con_MX_*) the same way
it already supported Finicity and FDE: it returns 202 Accepted with an
operation_id and triggers a fresh aggregation. (Previously MX returned
502 refresh_not_supported.)- Completion is asynchronous — listen for the
account.refresh.completedwebhook (oraccount.refresh.failedon terminal failure), then re-readGET /v3/accountsfor the updated balances and transactions. - All three connectable sources (Finicity, MX, FDE) now answer
/refreshuniformly, so you no longer need to special-case MX.
AccountsTransactions
PDF: a read-only fourth data source
Accounts created from uploaded bank statements now surface through the API. Their accounts and transactions come back withsource: "PDF" (ids like acc_PDF_42, txn_PDF_8837).- Read-only:
PDFisn’t linked through the widget and never appears underGET /v3/connections— aPDFaccount’sconnection_idisnull. - Exposed on
GET /v3/accountsandGET /v3/accounts/{id}/transactions. Statements aren’t served forPDFaccounts. - Everything else — pagination, filtering, the response shape — reads exactly like an aggregator source.
Pagination
Cursor pagination on all list endpoints
Every list endpoint now returns an opaquenext_cursor and a has_more flag. Pass next_cursor back as the cursor query param and loop until has_more is false.- Keyset (seek) based, so it stays correct even as rows are added or removed between pages — no skipped or double-counted items.
limitdefaults to100(max500; higher values are capped, not rejected).- Cursors are opaque and query-scoped — reusing one against a different account, filter, or date window returns
400 invalid_request.
Statements
Extract transactions from a bank-statement PDF
NewPOST /v3/statements/extract — upload a bank-statement PDF and get back the OCR-extracted transactions, powered by the same engine behind the LedgerSync app’s Bank Statement Converter.- Customer-level and standalone: no
client_id, and the upload never attaches to a connection, account, or stored statement. - Async: returns
202with anoperation_id; pollGET /v3/operations/{id}(most statements finish in under a minute). The result carrieskind: statement_extraction. There’s no webhook for this one — polling is the completion signal. - PDF only, up to 30 MB (larger uploads are rejected with
413). Requires thewrite:statementsscope. Per-customer concurrency and daily budgets apply on top of the standard rate tiers — exceeding them returns429.
