> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ledgersyncappv2.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Keeping data fresh

> How current the balances and transactions from GET /accounts are, how to check, and how to force a refresh when you need fresher-than-daily data.

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](/guides/connection-lifecycle#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.

<Info>
  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.
</Info>

## 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.

```json theme={null}
{
  "id": "acc_FINICITY_84213",
  "name": "Business Checking",
  "type": "checking",
  "last_refreshed_at": "2026-09-04T06:12:03Z",
  "oldest_transaction_date": "2026-05-28"
}
```

### What we request at connect

| Source                   | History requested on first connect                                   |
| ------------------------ | -------------------------------------------------------------------- |
| Finicity                 | The last **90 days**                                                 |
| MX                       | The last **730 days**. MX returns what it holds, which is often less |
| Portal connections (FDE) | Not windowed. You get what the extraction run returns                |
| Uploaded statements      | Exactly the statements that were uploaded                            |

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.

<Note>
  `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.
</Note>

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`:

```bash theme={null}
curl -X POST https://api.ledgersyncappv2.com/v3/connections/con_FINICITY_41294/refresh \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Length: 0"
```

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.

<Warning>
  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](/guides/adding-accounts).
</Warning>

<Steps>
  <Step title="POST /connections/{id}/refresh">
    You get `202` and an `operation_id`.
  </Step>

  <Step title="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.
  </Step>

  <Step title="Re-read GET /accounts (and /transactions)">
    The snapshot now reflects the fresh pull and `last_refreshed_at` has advanced.
  </Step>
</Steps>

See [Connection lifecycle](/guides/connection-lifecycle) and [Webhooks](/guides/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](/guides/errors#connection-refresh) for the
full list.

<Note>
  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](/guides/adding-accounts).
</Note>

## 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](/guides/going-live#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.

<Warning>
  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.
</Warning>
