Skip to main content
AI connector

Teammates can use LedgerSync in Claude and ChatGPT

Administrators and employees at a firm now connect LedgerSync in Claude or ChatGPT with their own LedgerSync login.
  • Each person sees only what they see in the LedgerSync app: an employee sees the clients assigned to them, an administrator sees the whole firm.
  • A client an employee creates through the AI is assigned to them, as in the app.
  • The firm owner gets an email each time a teammate connects.
  • Changing or resetting a password now disconnects only that person’s AI connections. The firm owner’s password change no longer disconnects teammates.
  • Sub-account (firm) logins and their employees, and logins with access days and hours, cannot connect yet. Client logins and support sessions cannot connect.
  • Only the firm owner links the desktop app. Teammates can check whether it is connected.
See Use LedgerSync in Claude or ChatGPT.
AI connector

A simpler AI connector guide for LedgerSync customers

The AI connector pages are now written for firms that already use LedgerSync.
  • A firm new to LedgerSync signs up in the LedgerSync app first (ledgersyncappv2.com/sign-up), then connects Claude or ChatGPT.
  • The setup pages keep their title and progress bar at the top while you scroll, and the setup videos open over the page.
  • The Claude Team or Enterprise and ChatGPT Business, Enterprise or Edu steps are now at the top of each setup page.
See Use LedgerSync in Claude or ChatGPT.
AI connector

Sign in to the AI connector with your LedgerSync app login

When you connect LedgerSync in Claude or ChatGPT, the LedgerSync sign-in page now opens.
  • Firms that use the LedgerSync app sign in with their normal LedgerSync username or email and password, plus their two-step code if they use one. No developer portal account and no email to support.
  • New firms create their LedgerSync account on that page: Create account, the sign-up form, then the order page. Our team reviews each new firm once. Until then the connector answers the new code firm_under_review when asked to connect a bank. Clients can be added.
  • Only the firm owner can connect for now.
  • Approved developer accounts type their developer portal email on the LedgerSync page, which sends them to the developer portal sign-in. An application still waiting for its first approval cannot connect yet.
  • Connections made before this change keep working.
  • The portal’s AI sign-up page (/start/ai) now sends new firms to the LedgerSync app sign-up.
See Use LedgerSync in Claude or ChatGPT.
AI connectorInstitutionsConnections
A few banks that LedgerSync reaches through FDE need the LedgerSync desktop app for refreshes. The app runs on a computer at the firm. The API and the AI connector now say which ones, and the connector can link the app.New field requires_desktop_app. It is optional, so it is additive for existing clients.
  • On institutions (GET /v3/institutions and GET /v3/institutions/{id}): true when LedgerSync reaches the bank through FDE and needs the desktop app to refresh it. Omitted when unknown.
  • On connections (GET /v3/connections/{id} and GET /v3/clients/{client_id}/connections): on FDE connections only, omitted for other sources. When true, the desktop app must be open and connected on the firm’s computer for a refresh to run.
Two new AI connector tools, on the live connector only:
  • ledgersync_get_desktop_app_status says whether the desktop app is linked and connected right now, with the Windows and Mac download links and the setup steps.
  • ledgersync_pair_desktop_app links the app with the 7-character code it shows. It replaces any computer linked before, so the AI asks the user to confirm first. Each account gets 10 tries an hour and 20 a day.
  • Bank search results and connections in the connector mark the banks that need the app.
  • New codes for linking: pairing_code_invalid, desktop_app_session_closed, desktop_service_unavailable and pairing_rate_limited.
Refreshes stopped by the desktop app say so.
  • A refresh FDE stops because the bank needs the desktop app and the app is not connected fails at once with desktop_app_required, and the connection reads requires_action with the same code. A refresh stopped while waiting for the choice of whether to use the desktop app fails with desktop_app_route_choice. Neither reached the bank, so neither counts against the daily refresh budget.
  • When FDE asks at the start of a refresh whether to use the desktop app, a working FDE connection no longer shows requires_action with mfa_required for that moment. It stays active, and no webhook fires for the question.
See Errors and Banks that need the LedgerSync desktop app.
AI connector

New firms can sign up for Claude and ChatGPT in a few minutes

A firm new to LedgerSync can now sign up with only its name and a card at portal.ledgersyncappv2.com/start/ai/new. There is no sandbox step and no developer form.
  • The first charge is one month after the card is added.
  • We review each firm once, within 2 business days, and email the answer. A sign-up we do not approve, or one that closes before its review, is not charged.
  • The firm can add LedgerSync to Claude or ChatGPT while it waits; it starts working as soon as the firm is approved.
  • On the live connector, Claude and ChatGPT now explain in plain words what a new firm still needs to do, with a link to the right page.
  • Developers building on the API keep the usual path: a sandbox request, then the Live application and payment.
See Use LedgerSync in Claude or ChatGPT.
AI connector

Use LedgerSync in Claude and ChatGPT

Add the LedgerSync connector to Claude or ChatGPT and ask in plain words: find a client, send them a secure link to connect a bank, read balances, transactions and statements, see which connections need attention, and refresh a connection. Bank sign-ins stay on LedgerSync’s own pages, never in the chat.
  • Live: https://api.ledgersyncappv2.com/mcp. Sandbox: https://api-sandbox.ledgersyncappv2.com/mcp.
  • Sign in with your developer portal account. Live needs the same approval and payment as live API keys; existing LedgerSync app customers are linked, not charged again.
  • The portal’s new Connected apps page lists the AI apps connected to your account and disconnects one at once.
See Use LedgerSync in Claude or ChatGPT.
ConnectionsWebhooks

Refreshes of MX and FDE connections now finish

POST /v3/connections/{id}/refresh on an MX or FDE connection now ends the way it does on Finicity: the operation turns succeeded or failed once every account on the connection has reported, and account.refresh.completed or account.refresh.failed fires for each account. The one exception is an FDE refresh refused straight away (below), which fails the operation without a webhook.
  • MX and FDE refresh operations reach a real verdict. They used to record no accounts to wait on, so every one ended failed with refresh_timed_out after 90 minutes, whatever happened at the bank.
  • MX refresh webhooks are sent for client connections. account.refresh.completed and account.refresh.failed were never sent for an MX connection that belongs to a client, which is every MX connection made through the API. They are now.
  • An MX refresh that finds nothing new still finishes. MX sends LedgerSync no notice when a sync brings no new or changed transactions, so LedgerSync now asks MX once the sync is over and reports it. Expect the verdict about three minutes after the call.
  • An FDE refresh the bank answers with a security question fails at once with mfa_required, instead of waiting out the timeout. Send the user a fix link from POST /v3/connections/{id}/reauthorize. Any other refusal fails at once with source_internal_error, with the FDE status in source_diagnostic_code (for example FDE-UPDATED_ERROR). No webhook fires for either; read the operation.
  • FDE connections report last_refreshed_at. It was always null on FDE connections. Their created_at and updated_at, which follow it, fill in too.
  • Sandbox refresh operations finish too. On sandbox they never matched their results and always timed out.
  • An earlier sync can no longer answer your refresh. A sync result that reached LedgerSync before your call, but was still being processed, could close your refresh with that older data. Only a result that arrives after your call answers it now.
Correction to the 2026-08-25 entry. It said MX and FDE connections “already reported completion correctly”. They did not: their refresh operations always ended refresh_timed_out, as described above.See Keeping data fresh.
Statements

Statement downloads work again

From 2026-09-24 until the fix on 2026-09-25, GET /v3/statements/{id}/download answered 500 instead of the PDF. It returns the file again. If a scheduled download failed in that window, run it again.
ConnectionsWebhooks

Connection status and webhooks now agree

GET /v3/connections/{id} and the connection.* webhooks now report the same status for the same state.
  • MX connections read their real status. The read endpoint used to report any unhealthy MX connection as disconnected. It now follows MX’s own member status, the same one the webhooks report: MX PREVENTED (too many failed sign-ins) and DENIED read requires_action with credentials_invalid, MX DISCONNECTED and CLOSED read disconnected, and a healthy or still-syncing member reads active. Expect some MX connections to move from disconnected to their real status.
  • The reason matches the status. A connection.requires_action for MX PREVENTED used to carry institution_unavailable with is_user_actionable: false. It now carries credentials_invalid, actionable. An MX-locked account is the one deliberate exception: requires_action with account_locked_by_institution and is_user_actionable: false, because the user acts at the bank. See Errors for the MX codes behind each error.
  • Connection events name the client. data.connection.client_id is now present on status-change events whenever the owner can be resolved, and the key is omitted, never null, when it cannot.
  • Deleting a connection sends connection.disconnected, as the testing guide already described.
  • Status changes found by the scheduled daily refresh now send webhooks. A connection that recovers on its own after connection.failed sends connection.active. One exception remains: a Finicity MFA challenge on an existing connection reads requires_action on GET without an event.
  • A new MX connection is named by a webhook. When a first MX link connects, or the bank rejects it, a connection.* event now carries the canonical con_MX_... id. Before, only the initiate connection.requires_action under the placeholder id fired, so an MX link that connected without an MFA step, or failed outright, had to be found by listing the client’s connections.
disconnected no longer means “the user revoked access”: it means the source stopped aggregating the connection, you deleted it, or (for Finicity and portal connections) LedgerSync’s own refresh of it failed. See Connection lifecycle.
AccountsTransactions

Card terms, account masks and retracted transactions

  • liabilities appears only on liability accounts, such as credit cards, lines of credit, loans and mortgages. It no longer appears on checking, savings or investment accounts, even when the bank reports a figure there. Identify cards by type: credit_card; loans and mortgages carry the object too.
  • MX now reports the statement balance. It was dropped before, so every MX statement_balance was null. It fills in as each MX account next syncs.
  • Card terms refresh on the scheduled daily refresh, not only when a refresh is triggered.
  • mask is now filled for MX, portal (FDE) and uploaded-statement accounts, as at most the last 4 digits of the account number. It used to be null for all three.
  • GET /v3/transactions/{id} returns 404 for a Finicity transaction the source retracted. The list never returned those; the single read now agrees. If you stored such an id, treat the 404 as “removed”.
Corrections to the 2026-09-04 entry. The object’s presence is not the card signal (loans and mortgages carry it). Finicity does not report all five fields everywhere, and MX does report the statement balance. The real coverage, measured per source and field, is in Credit-card terms.
Webhooks

Webhook subscriptions now report delivery health

last_success_at and last_failure_at were in the WebhookSubscription schema but documented as reserved, and never appeared in a response. They are now populated and returned on every GET, LIST and PATCH:
Read them together: last_failure_at newer than last_success_at means deliveries are failing right now, and that is the condition worth alerting on. A subscription stays active however badly delivery is going, because we keep retrying rather than disabling your endpoint, so status alone was never enough to tell you your webhooks had stopped arriving.Failures are stamped on every failed attempt, including retries still to come, and a later success never clears last_failure_at, so an endpoint that keeps flapping stays visible. Both fields are omitted rather than sent as null when there is nothing to report, so check for the key rather than comparing to null.Existing subscriptions were backfilled from delivery history, so the timestamps are meaningful immediately rather than only reflecting deliveries from today onward. See Check delivery health.
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 repairs a connection that needs credentials or consent renewed, aiming to preserve existing 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).
  • ID preservation depends on the provider and whether the repaired login and accounts match the stored connection. Starting another Finicity Full Connect session can also reuse an existing bank login; use that flow to add accounts, and verify canonical IDs afterward.
  • 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.