Skip to main content
There are two ways to get a user’s bank connected. Both finish with the same connection.active webhook and the same data behind it. Choose based on how much UI you want to own.

Hosted link

We host everything: bank search, login, MFA. You create a link and send it. No institution_id needed up front.

Bring your own picker

You render the bank search from GET /institutions, then start the connection with the chosen institution_id.

The hosted flow

You create a connect session for a client and get back a URL. Email it to your member. They open it, find their bank, log in on our page, and you get a webhook the moment it’s live.
Bank credentials are entered only on the LedgerSync-hosted page. They never touch your servers, and never come through the API as raw values.

Three calls, start to finish

1

Create the member as a Client

A Client is just your record of “this is Alice.”
Add "phone" to store a contact number. To have LedgerSync text the member a login invitation as soon as the Client is created, also send "sms_invitation": true — a non-blank phone is required, or the call returns 400.
2

Create a connect session, get the link

Response
Email url to your member. That is the only link they need.
3

Receive connection.active

Register a webhook once, and we tell you the instant the connection is live.
Webhook
Now read accounts, transactions, and statements for con_FINICITY_41294.
Need to kill a link you already emailed? DELETE /clients/{id}/connect-session/{sid} invalidates it immediately.

Prefer to build your own picker?

If you already have onboarding UI, skip the hosted page. Call GET /institutions?q=chase to render the bank search inside your app, then POST /clients/{id}/connections with the chosen institution_id. You still get the same widget hand-off for credentials and the same connection.active webhook, you just own the search box.

Redirect flow

When a member finishes on the aggregator widget (FINICITY / MX), the browser needs somewhere to land back in your app. Pass a redirect_url when you start the connection and we bounce the member back to it with a short-lived ls_token. Exchange that token server-side for the canonical connection id.
1

Register your callback URLs (once per environment)

Allowlist every URL you’ll redirect to. The match is exact — scheme, host, port, and path must line up with what your app actually uses — and URLs are https-only and per-environment (a sandbox URL does not carry over to live). Each POST overwrites the full set.
Response
2

Pass redirect_url when you start the connection

Include redirect_url in the initiate body. It must exactly match an allowlisted entry or the call is rejected with 400 invalid_request.
3

Member lands back on your callback with an ls_token

After the widget completes, the browser is redirected to your redirect_url with an ls_token query parameter:
4

Exchange the token for the connection id

Your callback POSTs the token back to verify it and recover the connection. No API key is required on this call — the token’s signature is the trust boundary.
Response
connection_id is the canonical con_SOURCE_id you use everywhere else (accounts, transactions, statements).
ls_token is a single-use, signed token with a short expiry. Verify it once, server-side — a second verify for the same token returns 400 invalid_request. Treat landing on your callback as a UI hand-off only; the connection.active webhook remains the source of truth that data is ready.
The redirect handshake is a browser convenience, not a substitute for webhooks. If a member closes the tab before the redirect fires, you still get connection.active — reconcile on the webhook, not on the callback landing.

Embedding

However you launch the connect flow, the hand-off is the same: you open a LedgerSync-hosted URL, the member enters credentials only on our page, and you find out it worked. What differs is how you open that URL. The default, and the only mode you need for the hosted connect-session flow: open the URL as a full-page redirect (web) or in a system/webview (mobile). Don’t try to frame it. When you set a redirect_url, the member lands back on it after a successful connect with a single-use ls_token appended:
Verify it server-side with POST /v3/connections/redirect-token/verify to read the resulting connection_id.
ls_token arrives only on your redirect_url query string — never through postMessage. Treat it as a one-time credential: verify it once, server-side, then discard it.

Iframing the MX wrapper page

The MX Connect wrapper page (the widget_url returned for MX connections) is the one hosted page you may iframe. On success it notifies the parent window:
To receive it, verify the origin and constrain your own page’s CSP to our host:
In sandbox the wrapper is served from https://api-sandbox.ledgersyncappv2.com — use that host for both the event.origin check and your frame-src.
The postMessage is a completion signal only — it carries connection_id, never ls_token. It is posted with a "*" target origin, so the origin check in your listener is mandatory. If you set a redirect_url, the wrapper also redirects its own window (never window.top) with ls_token, exactly like the redirect flow above; the postMessage and the redirect are not mutually exclusive.

Iframe attributes and OAuth

The wrapper needs no Permissions-Policy allow attributes — it uses no camera, microphone, or geolocation. A plain iframe works; widget_url is the URL POST /clients/{id}/connections hands back:
It’s a first-party, frame-guarded page, so you don’t need a sandbox attribute. If your CSP forces one, it must permit scripts, forms, same-origin, and popups — the widget runs JS, submits the credential form, needs its own origin, and opens OAuth in a new tab:
OAuth banks can’t complete inside a frame. Institutions that use OAuth (many large banks) block being iframed, so the wrapper opens the bank’s OAuth page in a new tab (window.open) and the member finishes there. So allow popups if you sandbox, and don’t assume the whole flow stays in your frame. Credential-based banks complete fully in-frame. Either way you get the same ls/mx/memberConnected postMessage (and the ls_token redirect, if you set one) once the connection succeeds.
Detecting completion without a redirect is exactly what the postMessage is for: after the origin check, listen for type === "ls/mx/memberConnected" and use event.data.connection_id. You never need a redirect_url to know the member finished.

What you cannot iframe

For the hosted connect-session page, use a redirect or open it in a new tab — framing it will be blocked by the browser.