curl --request POST \
--url https://api-sandbox.ledgersyncappv2.com/v3/connections/{connection_id}/refresh \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{}'import requests
url = "https://api-sandbox.ledgersyncappv2.com/v3/connections/{connection_id}/refresh"
payload = {}
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({})
};
fetch('https://api-sandbox.ledgersyncappv2.com/v3/connections/{connection_id}/refresh', 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/connections/{connection_id}/refresh",
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([
]),
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/connections/{connection_id}/refresh"
payload := strings.NewReader("{}")
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/connections/{connection_id}/refresh")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api-sandbox.ledgersyncappv2.com/v3/connections/{connection_id}/refresh")
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 = "{}"
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"
}
]
}
}Refresh a connection
Force a re-pull of accounts and recent transactions for this Connection. Banks usually post new transactions once a day so we already refresh active connections automatically — call this only when you have a specific reason to need fresher data (user just clicked a “refresh” button, you’re reconciling something time-sensitive).
Source-specific behavior
On-demand refresh is not uniformly available across sources.
The HTTP outcome depends on the connection’s source:
- Finicity (
con_FINICITY_*) — returns202 Acceptedwith anoperation_id. The backend queues the refresh asynchronously; on success theaccount.refresh.completedwebhook fires when the bank call finishes, and on terminal failure theaccount.refresh.failedwebhook fires instead. Refreshes only the accounts already linked on this connection: it updates balances and transactions on rows we already hold and never adds an account. An account the end-user opened, or chose not to share, after the original link is not picked up here no matter how often you call it. UsePOST /connections/{connection_id}/reauthorizefor that. - MX (
con_MX_*) — returns202 Acceptedwith anoperation_id. MX has no synchronous refresh API, so the backend triggers a member aggregation and returns immediately; MX pulls fresh balances and transactions asynchronously, and theaccount.refresh.completedwebhook fires when it finishes (account.refresh.failedon terminal failure). The aggregation re-reads the member’s whole account list, so an account added at the bank since link time is created here without any end-user step. The same happens on the scheduled passes, so on MX you can simply wait. - FDE (
con_FDE_*) — returns202 Acceptedwith anoperation_id. FDE re-runs its document extraction pipeline; on success theaccount.refresh.completedwebhook fires on completion, and on terminal failure theaccount.refresh.failedwebhook fires instead. The run re-reads the portal’s sub-accounts, so a sub-account that has appeared since link time is created here too.
Accounts opened after linking
Only Finicity is closed: refresh there is update-only. MX and FDE
discover new accounts on a refresh, and on MX the scheduled
passes do the same without you calling anything. Either way
nothing pushes you a notification, because there is no
account.added webhook. After a refresh completes,
re-list GET /v3/accounts?client_id=…&connection_id=… and diff
on account id. Account.created_at carries when LedgerSync first
saw the account, which is the field to sort by.
Refresh support by source
All three connectable sources — Finicity, MX, and FDE —
support on-demand refresh and answer 202 Accepted with an
operation_id; the outcome always arrives asynchronously on
the account.refresh.completed / account.refresh.failed
webhook, never in the 202 body. Uploaded-statement (PDF)
data is read-only, has no connection, and is never refreshed.
Reading the operation
The operation this returns reaches a terminal state, so polling
GET /operations/{operation_id} is a real fallback when you cannot
run a webhook handler. A refresh is one aggregation at the bank but
reports per sub-account, so it stays queued until every account on
the connection has reported, and resolves itself if those reports
never arrive.
On success result carries accounts_refreshed and an accounts
array. A refresh where the aggregation ran but an account did not come
back is reported as failed with refresh_partially_failed, carrying
the same per-account breakdown under error.accounts, so branching on
status alone cannot silently accept a connection that is missing an
account’s transactions.
Rate limiting
Refresh carries a daily per-customer budget separate from the standard
request-rate tiers, because each call is a real aggregation at the
institution rather than just another API request. Exceeding it returns
429 without queueing anything or contacting the bank. Active
connections already refresh about once a day on their own, so this
endpoint is for when something actually asked for fresher data.
Empty-body POST
This endpoint takes no request body. Send an explicit
Content-Length: 0 header — Google’s HTTPS load balancer
in front of the API rejects body-less POSTs without one
with 411 Length Required. Most HTTP clients add this
automatically; some (raw fetch, certain SDKs in
keep-alive mode) do not.
curl --request POST \
--url https://api-sandbox.ledgersyncappv2.com/v3/connections/{connection_id}/refresh \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{}'import requests
url = "https://api-sandbox.ledgersyncappv2.com/v3/connections/{connection_id}/refresh"
payload = {}
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({})
};
fetch('https://api-sandbox.ledgersyncappv2.com/v3/connections/{connection_id}/refresh', 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/connections/{connection_id}/refresh",
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([
]),
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/connections/{connection_id}/refresh"
payload := strings.NewReader("{}")
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/connections/{connection_id}/refresh")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api-sandbox.ledgersyncappv2.com/v3/connections/{connection_id}/refresh")
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 = "{}"
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
Canonical Connection id (con_<SOURCE>_<bankAccountId>). The
placeholder UUID returned during initiate is rejected here with
400 — only active connections can be refreshed.
"con_FINICITY_41294"
Body
No body. Send Content-Length: 0 so the upstream load
balancer doesn't reject the request with 411.
The body is of type object | null.
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.
