Skip to main content
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 new liabilities object:
The object is absent unless we know something, so it is null for every account that is not a credit card or line of credit, and null for cards at banks that publish no terms. Check for the object before reading fields inside it, and treat an absent field as “the bank did not say”, not zero.Bank connections report all five fields where the bank publishes them. MX reports every field except the statement balance, which it does not track. Portal connections and uploaded statements report none.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:
It is measured from the transactions we hold rather than copied from what the upstream provider reports about itself, so it is exactly the minimum 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_at advances, so re-reading GET /v3/accounts after 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 succeeded or failed once 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_failed with a per-account breakdown in error.accounts[], so branching on status alone 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_exceeded without touching the bank.
  • estimated_seconds on the 202 was raised from 10 to 120 to match how long a refresh actually takes. It is a best-effort floor, not a deadline.
MX and FDE connections are unchanged - they already reported completion correctly and keep the behaviour they had.See Keeping data fresh and the connection refresh error codes.
Connections

Reauthorize (fix) a broken connection in place

New POST /v3/connections/{connection_id}/reauthorize — repair a connection that reports status: requires_action without minting new ids.
  • Returns a reauthorize action with a hosted reauth_url; redirect the end user there to re-enter credentials or re-consent at the institution, then wait for connection.active (or connection.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, and FDE. Uploaded-statement (PDF) sources are read-only and have no connection to reauthorize.
See Connection lifecycle.
ChecksFDE

Check images are now readable from the API

The check_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:checks scope. Existing keys don’t have it — tick it on a new key in the portal, or PATCH /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 side field.
  • 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.
See Check images.
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.completed webhook (or account.refresh.failed on terminal failure), then re-read GET /v3/accounts for the updated balances and transactions.
  • All three connectable sources (Finicity, MX, FDE) now answer /refresh uniformly, 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 with source: "PDF" (ids like acc_PDF_42, txn_PDF_8837).
  • Read-only: PDF isn’t linked through the widget and never appears under GET /v3/connections — a PDF account’s connection_id is null.
  • Exposed on GET /v3/accounts and GET /v3/accounts/{id}/transactions. Statements aren’t served for PDF accounts.
  • Everything else — pagination, filtering, the response shape — reads exactly like an aggregator source.
See the data model.
Pagination

Cursor pagination on all list endpoints

Every list endpoint now returns an opaque next_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.
  • limit defaults to 100 (max 500; 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.
Full guide: Pagination.
Statements

Extract transactions from a bank-statement PDF

New POST /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 202 with an operation_id; poll GET /v3/operations/{id} (most statements finish in under a minute). The result carries kind: 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 the write:statements scope. Per-customer concurrency and daily budgets apply on top of the standard rate tiers — exceeding them returns 429.