Bank connections in the LedgerSync API are asynchronous. When a user links their bank, they may finish in seconds or take several minutes. Webhooks let LedgerSync push a notification to your server the instant something changes — so you never have to poll.
The most important event is connection.active. It fires when a user finishes linking and data starts flowing. Its payload carries the canonical connection ID (e.g., con_FINICITY_41294) — the only ID that works on /accounts, /transactions, and /statements.
The easiest path is the Webhooks page in the developer portal: paste your endpoint URL, pick the events you want, and copy the signing secret.
To subscribe via API:
curl -X POST https://api-sandbox.ledgersyncappv2.com/v3/webhooks/subscriptions \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/webhooks/ledgersync",
"event_types": ["connection.active", "connection.failed", "connection.disconnected", "account.refresh.completed"]
}'The response includes a signing_secret — shown only once. Save it immediately in a secrets manager. If you lose it, rotate the secret from the portal Webhooks page.
| Event | When it fires |
|---|---|
connection.initiated | You kicked off a link; the user hasn't finished the widget yet. |
connection.requires_action | The connection is waiting on the user (sign-in, MFA, account selection). |
connection.active | User finished linking. Data is flowing. Carries the canonical connection ID. |
connection.failed | Bad credentials or aggregator-reported failure. The link did not complete. |
connection.disconnected | User revoked access, or the connection needs a re-link. |
connection.capability_changed | A single capability on an active connection flipped working ↔ not-working. |
account.refresh.completed | A background data refresh for an account finished successfully. |
account.refresh.failed | A background data refresh for an account failed. |
You can also use "*" as the event type to subscribe to all events.
Never trust a delivery without verifying it. Every webhook delivery includes these headers:
X-LS-Webhook-Event-Id — unique ID for the event (use for deduplication)X-LS-Webhook-Timestamp — Unix seconds when LedgerSync produced the eventX-LS-Webhook-Signature — HMAC-SHA256 signature to verifyThe signed payload is: timestamp + "." + raw_body
Compute: hex(HMAC-SHA256(signing_secret, signed_payload)) and compare to X-LS-Webhook-Signature using a constant-time comparison. Also reject deliveries where the timestamp is more than 5 minutes old (replay protection).
Critical: Sign the raw bytes of the body exactly as received. If your framework re-serializes the JSON before you verify, the bytes will change and the signature will not match. Capture the raw body before any JSON middleware processes it.
event_id. The same event can arrive more than once due to retries. Treat event_id as an idempotency key: if you've seen it, acknowledge and skip.data rather than assuming a previous event already arrived.Send a real signed test delivery to your endpoint at any time:
curl -X POST \ https://api-sandbox.ledgersyncappv2.com/v3/webhooks/subscriptions/whsub_01HXYZ.../test \ -H "Authorization: Bearer sk_test_..."
The test delivery is signed with your real signing secret and bypasses the event type filter, so it exercises your full verification and deduplication logic end-to-end.
GET /v3/webhooks/subscriptionsstatus: active or paused)