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.
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.
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_reviewwhen 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.
AI connectorInstitutionsConnections
See which banks need the LedgerSync desktop app, and link it from Claude and ChatGPT
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 fieldrequires_desktop_app. It is optional, so it is additive for existing clients.- On institutions (
GET /v3/institutionsandGET /v3/institutions/{id}):truewhen LedgerSync reaches the bank through FDE and needs the desktop app to refresh it. Omitted when unknown. - On connections (
GET /v3/connections/{id}andGET /v3/clients/{client_id}/connections): on FDE connections only, omitted for other sources. Whentrue, the desktop app must be open and connected on the firm’s computer for a refresh to run.
ledgersync_get_desktop_app_statussays whether the desktop app is linked and connected right now, with the Windows and Mac download links and the setup steps.ledgersync_pair_desktop_applinks 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_unavailableandpairing_rate_limited.
- 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 readsrequires_actionwith the same code. A refresh stopped while waiting for the choice of whether to use the desktop app fails withdesktop_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_actionwithmfa_requiredfor that moment. It staysactive, and no webhook fires for the question.
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.
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.
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
failedwithrefresh_timed_outafter 90 minutes, whatever happened at the bank. - MX refresh webhooks are sent for client connections.
account.refresh.completedandaccount.refresh.failedwere 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 fromPOST /v3/connections/{id}/reauthorize. Any other refusal fails at once withsource_internal_error, with the FDE status insource_diagnostic_code(for exampleFDE-UPDATED_ERROR). No webhook fires for either; read the operation. - FDE connections report
last_refreshed_at. It was alwaysnullon FDE connections. Theircreated_atandupdated_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.
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: MXPREVENTED(too many failed sign-ins) andDENIEDreadrequires_actionwithcredentials_invalid, MXDISCONNECTEDandCLOSEDreaddisconnected, and a healthy or still-syncing member readsactive. Expect some MX connections to move fromdisconnectedto their real status. - The reason matches the status. A
connection.requires_actionfor MXPREVENTEDused to carryinstitution_unavailablewithis_user_actionable: false. It now carriescredentials_invalid, actionable. An MX-locked account is the one deliberate exception:requires_actionwithaccount_locked_by_institutionandis_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_idis now present on status-change events whenever the owner can be resolved, and the key is omitted, nevernull, 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.failedsendsconnection.active. One exception remains: a Finicity MFA challenge on an existing connection readsrequires_actiononGETwithout 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 canonicalcon_MX_...id. Before, only the initiateconnection.requires_actionunder 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
liabilitiesappears 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 bytype: credit_card; loans and mortgages carry the object too.- MX now reports the statement balance. It was dropped before, so every MX
statement_balancewas 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.
maskis 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}returns404for 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”.
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: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 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 repairs a connection
that needs credentials or consent renewed, aiming to preserve existing 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). - 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, 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.
