Skip to main content
This is the end-to-end path: mint a key, register a webhook, create a Client, find a bank, initiate a connection, hand off the widget, and read back accounts and transactions. Every call here uses the sandbox base URL and a test bank, so you can run the whole thing without touching real credentials.
Base URL (sandbox): https://api-sandbox.ledgersyncappv2.com/v3  ·  Auth: Authorization: Bearer sk_test_...  ·  Every response carries an X-LS-Trace-Id header — grab it if you ever open a support ticket.
Rather have an assistant write this for you? Build with an AI assistant has a brief you can paste straight into Claude or ChatGPT, plus the manual steps it cannot do on your behalf.

Prefer Postman?

Import our collection and run the whole flow without leaving Postman. The Quickstart folder chains every step below and captures ids for you, so you can run it top-to-bottom.
1

Import the collection

In Postman, Import → Link and paste the hosted URL (or download it and Import → File):
It ships folders for every resource — Clients, Connections, Accounts, Transactions, Statements, Operations, Webhooks, Institutions, and more — plus the chained Quickstart folder.
2

Set two variables

On the collection, set api_key to your key and point base_url at the environment you’re testing:
3

Run the Quickstart folder

Run it top-to-bottom — it creates a Client, discovers an institution, initiates a connection, and captures each returned id into the collection variables for the next request.
Prefer a raw, always-current endpoint list? Postman can import our OpenAPI spec directly: Import → Linkhttps://api.ledgersyncappv2.com/v3/openapi.yaml.

The mental model

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 has Transactions and Statements. Two things to internalize now, because they save debugging later:
  • You never see bank credentials. The user types them into a LedgerSync-hosted widget. You just open a URL.
  • The data source is chosen for you. LedgerSync routes each institution to Finicity, MX, or FDE server-side. There is no source parameter — the id you pass (ins_...) already encodes everything.

Finicity

Broadest US coverage. OAuth where the bank supports it.

MX

Alternate aggregator. Catches banks Finicity misses.

FDE

LedgerSync’s proprietary extraction for banks neither aggregator covers.
1

Mint a sandbox key

In the portal, open Build → API keys and create a sandbox key. It starts with sk_test_ — live keys start with sk_live_. Keep test and live strictly separate; they hit different base URLs and different data.Set it in your shell so the snippets below just work:
Confirm it authenticates:
A 401 means a bad or missing key. See Authentication for the full contract.
2

Register a webhook subscription

Connections finish asynchronously — the user could take thirty seconds or ten minutes inside the widget. Rather than poll forever, subscribe to webhooks and react when connection.active arrives.The easiest path is the portal Webhooks page — add your endpoint, pick events, and see deliveries and retries visually. To do it over the API, subscribe to all eight event types:
The response includes a signing_secret:
The signing_secret is shown once. Store it now — you need it to verify every incoming delivery. If you lose it, rotate the subscription.
Subscription ids start with whk_ followed by 32 hex characters. Anything else is rejected as unparseable and comes back 404.
Verify signatures constant-time and dedupe on event_id. The full recipe (headers, HMAC, retries, the 30-second ack window) is in the Webhooks guide. Want to see a payload land right away? Fire a test:
That returns { "event_id": "..." } and nothing to poll — watch your endpoint for the webhook.test delivery. The response returns a bare UUID; the delivered X-LS-Webhook-Event-Id is the same UUID as evt_ plus its digits with the dashes removed.
3

Create a Client

A Client represents one end-user.
Hold onto cli_9f2a4c1b8e07d3a5f6b20c14 — every read later is scoped to it. phone is optional; add "sms_invitation": true alongside a non-blank phone to have LedgerSync text the client a login invitation on creation.
external_id and metadata do not persist yet. They are accepted and echoed back on create, as above, but GET /clients/{id} and GET /clients return them as null, and PATCH silently drops them. Store your own mapping from your user id to the cli_... id on your side, and key everything off the cli_... id. Do not build a lookup that reads external_id back from us.
4

Discover the institution

Search the catalog to get the institution_id you’ll connect to. Each row carries a capabilities block so you know upfront what a bank supports.
Pick the row whose name matches what you searched. ?q=FinBank can return several FinBank variants — grab the one named exactly FinBank for the no-MFA happy path.
5

Initiate the Connection

Create a Connection under the Client with just the institution_id. No source, no credentials.
You get back 202 Accepted and an operation_id. The connection is being set up in the background.
Read the operation to collect the widget URL:
status: "succeeded" here means the widget URL was issued, not that the bank is linked. The operation’s result is a snapshot written once, at that moment, and it is never rewritten — so the con_<uuid> placeholder above stays the placeholder no matter how many times you re-poll.Take action.widget_url from this response and nothing else. Judge any connection id by its shape: con_<SOURCE>_<number> is canonical and safe to store, con_ plus a UUID is not and returns 400 missing source separator on /accounts, /transactions, and /statements. The canonical id arrives from the connection.active webhook, or from GET /clients/{client_id}/connections.
6

Hand off the widget

Open widget_url in the user’s browser — redirect, iframe, or webview, whatever fits your app. The user searches for their bank, signs in (credentials or the bank’s own OAuth), picks accounts, and closes it. Credentials go to the bank, never through you.A small nuance by source: Finicity and MX show an aggregator widget with a bank search; FDE shows a LedgerSync-hosted connect page with the bank already chosen (no search, since institution_id fixed it).
Don’t want to build a picker at all? Use the hosted link instead: POST /v3/clients/{id}/connect-session returns { url, expires_at } — a LedgerSync-hosted page you email the member. They search, pick, and connect their own bank. See Connect a bank for both flows side by side.A hosted link cannot be revoked through the API today: the create call returns only url and expires_at, there is no endpoint that lists sessions, and DELETE /v3/clients/{id}/connect-session/{sid} needs an sid you are never given. Treat expires_at as the only expiry.
7

Receive connection.active

When the user finishes, LedgerSync fires connection.active to your webhook. This is your signal that data is flowing and the canonical id is ready.
Persist con_FINICITY_41294 against your user. That’s the id you’ll use for every read. (The other statuses — requires_action, failed, disconnected — are covered in the Connection lifecycle guide.)
The payload carries no institution_id. If you need the institution for a connection, read it from GET /v3/connections/{id}, which returns an institution object.
8

List accounts and transactions

Reads are Client-scoped — pass client_id on every one. Start with the accounts on this connection:
Then pull transactions for an account, filtered by date:
That’s the full loop — Client → Connection → Account → Transactions. The ids encode their source (acc_FINICITY_..., txn_MX_..., txn_FDE_...), but the shapes are identical no matter which aggregator served them.

The transaction object

Every row in a /transactions response has the same shape, whatever aggregator served it. The fields: That’s the complete set. There is no subcategory, type, or location field.

Amount sign

amount is signed to one convention across every source. Most aggregators already deliver a signed value and LedgerSync passes it straight through; where a source instead reports an unsigned magnitude with the direction in a separate field (MX does this), LedgerSync applies the sign for you. Either way you get:
  • Money out (charges, purchases, withdrawals) is negative. A $28.34 card purchase comes through as -28.34.
  • Money in (deposits, payroll, credits) is positive. A $1,500 payroll deposit comes through as +1500.00.
The sign is a LedgerSync guarantee, not a per-source quirk — you do not need to branch on which aggregator backs a connection. acc_FINICITY_*, acc_MX_*, acc_FDE_* and acc_PDF_* all report money out as negative and money in as positive. Before you post amounts into a ledger it is still worth a quick read against your own sandbox data with FinBank — the fixtures above are exactly what the sandbox returns, so you can confirm the convention end to end for your integration.
Dedupe on id. The txn_<SOURCE>_<n> id is stable across refreshes, so use it as your upsert/idempotency key. Re-pulling a date range that overlaps a previous pull returns the same ids — key on id and you’ll never double-count a transaction.

Testing with FinBank

The no-MFA happy path. Search ?q=FinBank and pick the row named exactly FinBank. In the widget, sign in with Banking Userid demo / Banking Password go. It flips to active immediately — no MFA, no OAuth round-trip — and auto-populates accounts, transactions, and statements. To exercise FDE instead, use “Ledgersync Bank” (ins_a7397a8d0656e1b7). The full bank list, plus MFA and OAuth variants and their credentials, lives in the portal Testing playbook.

Handling errors

Every error returns the same envelope — branch on code, not on the HTTP status alone:
Details and every code in the Errors guide.

Where to go next

Connect a bank

The widget flow and the hosted connect-session link, in depth.

Webhooks

Verify signatures, dedupe on event_id, handle retries.

Connection lifecycle

requires_action, active, failed, disconnected — and capability changes.

API reference

Every endpoint, parameter, and response shape.