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"
}
]
}
}Start a new bank connection
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.
This one’s genuinely async — the bank or document pipeline
takes a few seconds to a minute. You get 202 Accepted and an
operation_id right away; the connection.active (or
connection.requires_action / connection.failed) webhook
fires when it’s done. Poll GET /operations/{operation_id}
if you can’t run a webhook handler.
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.
The canonical id, con_<SOURCE>_<bankAccountId> such as
con_FINICITY_41294 or con_MX_1224, is only assigned once
the user finishes the widget and the source pushes its first
callback. 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"
}
]
}
}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
Reserved, not yet honored. The header is accepted and ignored today: nothing reads it, no response is replayed, and
idempotency_conflictis never returned. Do not auto-retry a failed POST on the assumption that this protects you — a retriedPOST /clientsorPOST /clients/{id}/connectionscreates a duplicate. Send the header if you want to be ready for it; do not depend on it. The behavior described below is the intended contract for a future release.
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.
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.
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.
