curl --request POST \
--url https://api-sandbox.ledgersyncappv2.com/v3/clients/{client_id}/connections \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"institution_id": "<string>",
"redirect_url": "<string>",
"credentials": {}
}
'import requests
url = "https://api-sandbox.ledgersyncappv2.com/v3/clients/{client_id}/connections"
payload = {
"institution_id": "<string>",
"redirect_url": "<string>",
"credentials": {}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({institution_id: '<string>', redirect_url: '<string>', credentials: {}})
};
fetch('https://api-sandbox.ledgersyncappv2.com/v3/clients/{client_id}/connections', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api-sandbox.ledgersyncappv2.com/v3/clients/{client_id}/connections",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'institution_id' => '<string>',
'redirect_url' => '<string>',
'credentials' => [
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api-sandbox.ledgersyncappv2.com/v3/clients/{client_id}/connections"
payload := strings.NewReader("{\n \"institution_id\": \"<string>\",\n \"redirect_url\": \"<string>\",\n \"credentials\": {}\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api-sandbox.ledgersyncappv2.com/v3/clients/{client_id}/connections")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"institution_id\": \"<string>\",\n \"redirect_url\": \"<string>\",\n \"credentials\": {}\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api-sandbox.ledgersyncappv2.com/v3/clients/{client_id}/connections")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"institution_id\": \"<string>\",\n \"redirect_url\": \"<string>\",\n \"credentials\": {}\n}"
response = http.request(request)
puts response.read_body{
"operation_id": "op_01HXYZ8A6N7K2W9PQ4T5Z3V6E0",
"status": "queued",
"poll_url": "https://api-sandbox.ledgersyncappv2.com/v3/operations/op_01HXYZ8A6N7K2W9PQ4T5Z3V6E0",
"estimated_seconds": 123
}{
"error": {
"code": "unknown_api_key",
"message": "The API key you presented doesn't match any active key.",
"type": "auth_error",
"doc_url": "https://portal.ledgersyncappv2.com/errors/unknown_api_key",
"category": "AUTH_ERROR",
"is_user_actionable": true,
"source_diagnostic_code": "FIN-103",
"param": "client.email",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"errors": [
{
"param": "client.email",
"message": "must be a valid email address",
"code": "invalid_email"
}
]
}
}{
"error": {
"code": "unknown_api_key",
"message": "The API key you presented doesn't match any active key.",
"type": "auth_error",
"doc_url": "https://portal.ledgersyncappv2.com/errors/unknown_api_key",
"category": "AUTH_ERROR",
"is_user_actionable": true,
"source_diagnostic_code": "FIN-103",
"param": "client.email",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"errors": [
{
"param": "client.email",
"message": "must be a valid email address",
"code": "invalid_email"
}
]
}
}{
"error": {
"code": "unknown_api_key",
"message": "The API key you presented doesn't match any active key.",
"type": "auth_error",
"doc_url": "https://portal.ledgersyncappv2.com/errors/unknown_api_key",
"category": "AUTH_ERROR",
"is_user_actionable": true,
"source_diagnostic_code": "FIN-103",
"param": "client.email",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"errors": [
{
"param": "client.email",
"message": "must be a valid email address",
"code": "invalid_email"
}
]
}
}{
"error": {
"code": "unknown_api_key",
"message": "The API key you presented doesn't match any active key.",
"type": "auth_error",
"doc_url": "https://portal.ledgersyncappv2.com/errors/unknown_api_key",
"category": "AUTH_ERROR",
"is_user_actionable": true,
"source_diagnostic_code": "FIN-103",
"param": "client.email",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"errors": [
{
"param": "client.email",
"message": "must be a valid email address",
"code": "invalid_email"
}
]
}
}{
"error": {
"code": "unknown_api_key",
"message": "The API key you presented doesn't match any active key.",
"type": "auth_error",
"doc_url": "https://portal.ledgersyncappv2.com/errors/unknown_api_key",
"category": "AUTH_ERROR",
"is_user_actionable": true,
"source_diagnostic_code": "FIN-103",
"param": "client.email",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"errors": [
{
"param": "client.email",
"message": "must be a valid email address",
"code": "invalid_email"
}
]
}
}Open a bank connection session
Kick off a connection between a Client and a bank. Send only
institution_id (a v3 catalog id from GET /v3/institutions).
LedgerSync’s router picks the underlying source (Finicity vs
MX) per its routing rules — integrators don’t pick a source.
The response:
- Finicity / MX — we hand back a widget URL. Open it in
the user’s browser (or your mobile webview) and they’ll
finish linking. For MX the URL points at a
LedgerSync-hosted wrapper page
(
GET /connections/mx-wrapper) that embeds the MX widget and gives you the sameredirect_url+ls_tokencompletion hop as Finicity (verify it atPOST /connections/redirect-token/verify). - FDE — we return a
widget_urlto a LedgerSync-hosted connect page where the end user enters their bank credentials and answers any security questions in-session. Raw credentials are never sent to the API.
You get 202 Accepted and an operation_id right away. Poll
GET /operations/{operation_id} until the widget URL is ready,
then open result.connection.action.widget_url. Operation
succeeded means the URL was issued; the user still needs to
complete the widget. Connection lifecycle webhooks report later
status changes.
Add accounts to an existing Finicity connection
Call this endpoint with the existing client_id and the bank’s
v3 catalog institution_id. When routed to Finicity, it opens
Full Connect using that client’s existing Finicity customer.
In the widget, use the option to add accounts under the existing
bank and complete the bank’s authorization and account selection.
POST /connections/{connection_id}/reauthorize opens Finicity
Connect Fix for repairs and does not guarantee account selection
on a healthy connection.
If Finicity returns the same institution login and account identities, LedgerSync reuses the existing connection and account ids and attaches newly shared accounts. A different provider login can create a separate connection, so this is not an unconditional guarantee that all ids stay the same.
Snapshot all pages of GET /v3/accounts?client_id=... before the
session, then re-list after completion and compare account ids.
There is no public account.added webhook, and adding accounts
to an active connection does not guarantee another
connection.active event. The widget completion redirect and
operation success do not establish that the accounts are already
persisted; allow time for the account list to update.
Note on connection.id. For Finicity and MX the embedded
result.connection.id is a UUID-shaped placeholder
(con_<uuid>) that cannot be used with any other endpoint.
For a first-time connection, the canonical id,
con_<SOURCE>_<bankAccountId> such as con_FINICITY_41294 or
con_MX_1224, is assigned when the source callback creates the
connection. An account-addition session can reuse an already
assigned canonical id. This operation reaches succeeded as soon
as the widget URL is issued, and its stored result is never
rewritten, so a placeholder here stays a placeholder. Check
the shape before storing an id, and read the canonical one
from the connection.active webhook or from
GET /clients/{client_id}/connections. See the
Connection lifecycle section at the top of this reference
for the full handshake.
curl --request POST \
--url https://api-sandbox.ledgersyncappv2.com/v3/clients/{client_id}/connections \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"institution_id": "<string>",
"redirect_url": "<string>",
"credentials": {}
}
'import requests
url = "https://api-sandbox.ledgersyncappv2.com/v3/clients/{client_id}/connections"
payload = {
"institution_id": "<string>",
"redirect_url": "<string>",
"credentials": {}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({institution_id: '<string>', redirect_url: '<string>', credentials: {}})
};
fetch('https://api-sandbox.ledgersyncappv2.com/v3/clients/{client_id}/connections', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api-sandbox.ledgersyncappv2.com/v3/clients/{client_id}/connections",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'institution_id' => '<string>',
'redirect_url' => '<string>',
'credentials' => [
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api-sandbox.ledgersyncappv2.com/v3/clients/{client_id}/connections"
payload := strings.NewReader("{\n \"institution_id\": \"<string>\",\n \"redirect_url\": \"<string>\",\n \"credentials\": {}\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api-sandbox.ledgersyncappv2.com/v3/clients/{client_id}/connections")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"institution_id\": \"<string>\",\n \"redirect_url\": \"<string>\",\n \"credentials\": {}\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api-sandbox.ledgersyncappv2.com/v3/clients/{client_id}/connections")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"institution_id\": \"<string>\",\n \"redirect_url\": \"<string>\",\n \"credentials\": {}\n}"
response = http.request(request)
puts response.read_body{
"operation_id": "op_01HXYZ8A6N7K2W9PQ4T5Z3V6E0",
"status": "queued",
"poll_url": "https://api-sandbox.ledgersyncappv2.com/v3/operations/op_01HXYZ8A6N7K2W9PQ4T5Z3V6E0",
"estimated_seconds": 123
}{
"error": {
"code": "unknown_api_key",
"message": "The API key you presented doesn't match any active key.",
"type": "auth_error",
"doc_url": "https://portal.ledgersyncappv2.com/errors/unknown_api_key",
"category": "AUTH_ERROR",
"is_user_actionable": true,
"source_diagnostic_code": "FIN-103",
"param": "client.email",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"errors": [
{
"param": "client.email",
"message": "must be a valid email address",
"code": "invalid_email"
}
]
}
}{
"error": {
"code": "unknown_api_key",
"message": "The API key you presented doesn't match any active key.",
"type": "auth_error",
"doc_url": "https://portal.ledgersyncappv2.com/errors/unknown_api_key",
"category": "AUTH_ERROR",
"is_user_actionable": true,
"source_diagnostic_code": "FIN-103",
"param": "client.email",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"errors": [
{
"param": "client.email",
"message": "must be a valid email address",
"code": "invalid_email"
}
]
}
}{
"error": {
"code": "unknown_api_key",
"message": "The API key you presented doesn't match any active key.",
"type": "auth_error",
"doc_url": "https://portal.ledgersyncappv2.com/errors/unknown_api_key",
"category": "AUTH_ERROR",
"is_user_actionable": true,
"source_diagnostic_code": "FIN-103",
"param": "client.email",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"errors": [
{
"param": "client.email",
"message": "must be a valid email address",
"code": "invalid_email"
}
]
}
}{
"error": {
"code": "unknown_api_key",
"message": "The API key you presented doesn't match any active key.",
"type": "auth_error",
"doc_url": "https://portal.ledgersyncappv2.com/errors/unknown_api_key",
"category": "AUTH_ERROR",
"is_user_actionable": true,
"source_diagnostic_code": "FIN-103",
"param": "client.email",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"errors": [
{
"param": "client.email",
"message": "must be a valid email address",
"code": "invalid_email"
}
]
}
}{
"error": {
"code": "unknown_api_key",
"message": "The API key you presented doesn't match any active key.",
"type": "auth_error",
"doc_url": "https://portal.ledgersyncappv2.com/errors/unknown_api_key",
"category": "AUTH_ERROR",
"is_user_actionable": true,
"source_diagnostic_code": "FIN-103",
"param": "client.email",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"errors": [
{
"param": "client.email",
"message": "must be a valid email address",
"code": "invalid_email"
}
]
}
}Authorizations
Pass your secret key in the Authorization header as a Bearer
token: Authorization: Bearer sk_test_... (sandbox) or
Bearer sk_live_... (production).
Keys are created in the developer portal and the plaintext secret is shown exactly once at creation. Treat them like passwords — never embed them in mobile apps or front-end code.
Headers
Safe-retry key for POSTs. Send any unique string per logical
request (a UUIDv4 is great). If the network drops and you retry
with the same Idempotency-Key within 24 hours, you get the
exact same response back instead of creating a duplicate. A
replayed response carries Idempotent-Replayed: true.
Cached responses include failures (4xx and 5xx) too. If a call failed because of a bad input and you want to try again with the corrected input, use a fresh key — otherwise you'll keep getting the cached failure.
If the original request is still running you get 409
with a Retry-After header. Retry with the same key after
that interval. Do not generate a new key to get past a 409 —
a new key is a new request, which is exactly the duplicate this
header prevents.
Reusing a key for a genuinely different request (different
body, or a different path) returns 409 idempotency_conflict.
The key is scoped to the API key that first used it, so a
sandbox key and a live key never share an entry.
A replay still counts against your request rate limit, but it
does not consume the daily refresh budget on
POST /connections/{connection_id}/refresh, and it does not
take a connection-concurrency slot.
255"6f1a8c50-3e9c-4d4a-b1f5-2c5b9a2f7d11"
Path Parameters
Body
Source-hiding initiate-connection shape. The integrator passes
only institution_id (a v3 catalog id from
GET /v3/institutions); LedgerSync's router picks the underlying
source. No source field.
A v3 catalog id (e.g., ins_01HZX9CHASE) from GET /v3/institutions.
URL the widget redirects to on completion.
Ignored. FDE connects via a LedgerSync-hosted page, so raw bank credentials are never accepted over the API. Retained only so a body that still carries it is not rejected.
Response
The request was accepted but the underlying work runs
asynchronously. Use the returned operation_id to poll
GET /operations/{operation_id} or just wait for the matching
webhook event.
Returned with 202 Accepted for async operations.
"op_01HXYZ8A6N7K2W9PQ4T5Z3V6E0"
queued "https://api-sandbox.ledgersyncappv2.com/v3/operations/op_01HXYZ8A6N7K2W9PQ4T5Z3V6E0"
Rough ETA for completion. Best-effort.
