Skip to main content
Each Account exposes a current_balance, plus its transactions through GET /accounts/{id}/transactions. This guide explains how fresh that data is, how to tell, and how to pull fresher data on demand. For what current_balance means and how it is signed, see Asset vs liability. available_balance is reported by MX only, and only for some accounts: it is common on checking and rare on credit cards. Read realized_capabilities.available_balance rather than assuming it is present.

How fresh is GET /accounts?

The balances you read are a cached snapshot from the last successful refresh — not a live call to the bank on every request. LedgerSync refreshes every active connection automatically about once a day, so under normal conditions the numbers are less than 24 hours old. Every Account carries a last_refreshed_at timestamp — the moment its data was last refreshed. Read it to decide whether you already have fresh-enough data or need to force a refresh.
For most use cases you do not need to call /refresh. The daily auto-refresh keeps active connections current, and GET /accounts always returns the latest snapshot. Reach for /refresh only when you need fresher-than-daily data.

How far back does the history go?

Freshness is one axis; depth is the other. Every Account carries an oldest_transaction_date: the oldest transaction we actually hold for it. It is measured from the transactions themselves, not copied from what the bank or the aggregator says it has. So it is exactly the minimum date you get if you page that account’s full history, and the two can never disagree.

What we request at connect

After that first pull, history grows forward with each refresh. It does not reach further back, so an account linked a year ago holds about a year. What actually arrives inside the requested window is up to the institution, and it is not something we can tell you before the connection exists. That is what the field is for: read oldest_transaction_date per account instead of assuming a window. It is null when we hold no transactions for that account yet, which is normal between linking a connection and its first transaction pull finishing.
oldest_transaction_date can move backwards. It is a fact about what we hold right now, and a backfill landing earlier transactions lowers it. Re-read it rather than caching it as a fixed property of the account.
Like Transaction.date, it is a calendar day (YYYY-MM-DD) with no time component, and it is truncated by the same code, so comparing the two is safe.

Forcing a refresh

When you need up-to-the-moment data (a user tapped “refresh”, you’re reconciling something time-sensitive), call POST /connections/{id}/refresh:
It returns 202 Accepted with an operation_id and starts the refresh asynchronously, so balances are not updated by the time the call returns. All three connectable sources (Finicity, MX, and FDE) answer this call the same way: 202 now, the outcome later on a webhook.
A refresh keeps the accounts you already have current. It is not uniformly a discovery call. On MX and FDE it also picks up an account the client opened after linking; on Finicity it never does, because a Finicity refresh only touches the accounts already attached to the connection. See Adding a new account.
1

POST /connections/{id}/refresh

You get 202 and an operation_id.
2

Wait for the account.refresh.completed webhook

It fires when the fresh data is in (or account.refresh.failed on a terminal failure). Listen for the webhook — don’t poll GET /accounts in a loop.
3

Re-read GET /accounts (and /transactions)

The snapshot now reflects the fresh pull and last_refreshed_at has advanced.
See Connection lifecycle and Webhooks for the account.refresh.completed / account.refresh.failed payload shapes, and the Refresh a connection endpoint in the API Reference for the full contract.

What the operation tells you

The webhook is the signal to build on, but the operation_id is a real fallback when you can’t run a webhook handler: GET /v3/operations/{operation_id} reaches a terminal succeeded or failed, and always terminates even if the events never arrive. A refresh is one aggregation at the bank but reports per sub-account, so the operation stays queued until every account on the connection has reported. That matters for how you read the verdict:
  • succeeded - every account refreshed. result.accounts[] lists them, and result.accounts_refreshed counts them.
  • failed with refresh_partially_failed - the aggregation ran, but at least one account didn’t come back. This is deliberately a failure rather than a partial success, so branching on status alone can’t quietly ship a connection that’s missing an account’s transactions. error.accounts[] breaks it down per account.
  • failed with refresh_timed_out - nothing reported in time. The refresh may still have run; check last_refreshed_at.
Expect a couple of minutes end to end, not seconds: the bank aggregation alone typically runs about a minute, the per-account transaction pull follows it, and the event carrying the result adds its own hop. The estimated_seconds on the 202 is a best-effort floor, not a deadline. See the connection refresh error codes for the full list.
MX aggregates on its own schedule, several times a day, and refreshes through this same async webhook flow. A refresh you request triggers an MX aggregation, and the result arrives on the account.refresh.completed webhook, so the shape of the call matches Finicity and FDE.What is not the same is what comes back. An MX aggregation re-reads the member’s full account list, so it also picks up accounts the client opened after linking. FDE does the same on its extraction run. A Finicity refresh does not, and no amount of refreshing will surface an account the client never shared. See Adding a new account.

Refreshing many connections at once

You can refresh all of a customer’s banks in one batch - each connection is a separate POST /connections/{id}/refresh call, and that’s well within the request-rate limit (live keys allow 60 requests/minute). Refresh also carries a daily budget of its own, separate from that request rate, because every refresh is a real aggregation at the institution rather than just another API call. Exceed it and the call returns 429 rate_limit_exceeded without queueing anything or touching the bank.
  • Fire the batch, then wait on the account.refresh.completed webhooks — don’t poll operations or /accounts in a tight loop.
  • Watch the X-RateLimit-* headers and back off before you hit zero — see Rate limits.
  • Each refresh triggers a real aggregation at the bank, so space large batches out rather than firing hundreds at once.
  • Don’t put /refresh on a timer. Active connections already refresh once a day on their own, and our upstream provider’s terms don’t permit client apps automating calls to the refresh services. Refresh when something asked for it - a user tapped refresh, or you’re reconciling something time-sensitive.
A refresh is asynchronous and eventually-consistent. Don’t block a user flow waiting for /refresh to finish — show the cached balance immediately (with last_refreshed_at), and update your UI when the webhook arrives.