This guide walks you through linking your first bank account and reading its transactions with the LedgerSync API, starting from a sandbox key.
Tip: Every response includes an X-LS-Trace-Id header. Include it in any support request.
Four objects, one line of descent. A Client is your record of one end-user. Each Client owns one or more Connections (one per linked bank). Each Connection exposes Accounts, and each Account holds Transactions and Statements.
Two important things to know upfront:
Log in to the developer portal and create a sandbox key. Sandbox keys start with sk_test_. Set it in your terminal:
export LS_KEY="sk_test_your_key_here"
export LS_BASE="https://sandbox-api.ledgersyncappv2.com/v3"
Confirm authentication:
curl "$LS_BASE/institutions?q=chase" -H "Authorization: Bearer $LS_KEY"
A 401 response means a bad or missing key.
Bank connections complete asynchronously. Subscribe to webhooks so LedgerSync notifies you when a connection goes active, with no polling required.
curl -X POST "$LS_BASE/webhooks/subscriptions" -H "Authorization: Bearer $LS_KEY" -H "Content-Type: application/json" -d '{ "url": "https://yourapp.com/webhooks/ledgersync", "event_types": ["connection.active", "connection.failed", "connection.disconnected", "account.refresh.completed"] }'
The response includes a signing_secret, shown only once. Save it immediately. You will use it to verify every incoming delivery: each delivery carries an X-LS-Webhook-Signature header signed with that secret.
A Client represents one end-user. Set external_id to your own internal user ID so you can cross-reference without storing LedgerSync IDs everywhere.
curl -X POST "$LS_BASE/clients" -H "Authorization: Bearer $LS_KEY" -H "Content-Type: application/json" -d '{ "external_id": "user_4820", "name": "Example Client" }'
Save the returned id, which starts with cli_. Every later read is scoped to it.
Search the institution catalog to get the institution_id you want to connect. Each result includes a capabilities block showing what the bank supports (transactions, statements, check images).
curl "$LS_BASE/institutions?q=FinBank" -H "Authorization: Bearer $LS_KEY"
For sandbox testing, search for FinBank, a test bank. Use the id from your search result rather than hardcoding one.
Create a Connection under your Client with just the institution_id. No source selection, no credentials.
curl -X POST "$LS_BASE/clients/cli_.../connections" -H "Authorization: Bearer $LS_KEY" -H "Content-Type: application/json" -d '{ "institution_id": "ins_..." }'
You receive a 202 Accepted and an operation_id. Poll GET /v3/operations/{operation_id} until status is succeeded, then take the widget_url from the result.
Important: The connection ID at this stage is a placeholder (con_ followed by a long random ID). Do not use it to read data. The canonical ID (con_ followed by a source code and a number) is only available after the user completes the widget and connection.active fires.
Open the widget_url in the user's browser. The user signs in to their bank and authorizes their accounts. You never see their credentials.
Don't want to build a bank picker? Use the hosted connect link instead: POST /v3/clients/{id}/connect-session returns a LedgerSync-hosted URL you can email to the user. They search, pick, and connect their own bank with no front-end work on your side.
When the user finishes, LedgerSync sends connection.active to your webhook endpoint with the canonical connection ID. Save that ID against your user record. If you cannot run a webhook handler, list the Client's connections with GET /v3/clients/{id}/connections, which only returns canonical IDs.
Then read accounts and transactions:
# List accounts
curl "$LS_BASE/accounts?client_id=cli_...&connection_id=con_..." -H "Authorization: Bearer $LS_KEY"
# List transactions
curl "$LS_BASE/accounts/acc_.../transactions?client_id=cli_...&from=2026-01-01" -H "Authorization: Bearer $LS_KEY"
Use connection and account IDs exactly as LedgerSync returns them.
Every error returns the same envelope. Branch on code, not just the HTTP status: