connection.active: it fires when a user finishes linking and data starts flowing. That payload carries the canonical connection id (like con_FINICITY_41294) — the only id that works on /accounts, /transactions, and /statements. Subscribe to webhooks and you get it delivered for free.
Webhooks are the recommended way to learn about connection state. You can poll
GET /v3/operations/{id} during the initial link (see Connect a bank), but for everything after — refreshes, disconnects, capability changes — webhooks are the only push channel.Subscribe
The easiest path is the Webhooks page in the portal: paste your endpoint URL, tick the events you want, and copy the signing secret. It’s the recommended day-to-day way to manage subscriptions, rotate secrets, and inspect recent deliveries. To do it programmatically,POST /v3/webhooks/subscriptions:
Event catalog
There are exactly eight real events, plus awebhook.test event you can fire on demand. Subscribe only to the ones you handle.
| Event | Fires when |
|---|---|
connection.initiated | You’ve kicked off a link; the user hasn’t finished the widget yet. |
connection.requires_action | The connection is waiting on the user in the widget (sign-in, MFA, account pick). |
connection.active | The user finished linking. Data is flowing. Carries the canonical connection id. |
connection.failed | Bad credentials or an aggregator-reported failure. The link did not complete. |
connection.disconnected | The 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. |
webhook.test | A manual test delivery you triggered. Safe to ignore in business logic. |
Delivery shape
Every delivery is an HTTPPOST to your URL with a JSON body and these X-LS-Webhook-* headers:
| Header | Meaning |
|---|---|
X-LS-Webhook-Event-Id | Stable unique id for this event. Dedupe on it (same value as event_id in the body). |
X-LS-Webhook-Event-Type | The event type, e.g. connection.active. Lets you route before parsing. |
X-LS-Webhook-Timestamp | Unix seconds when we produced the event. Part of the signed payload and used for replay protection. |
X-LS-Webhook-Signature | hex(HMAC-SHA256(signing_secret, timestamp + "." + raw_body)). Verify before trusting anything. |
X-LS-Webhook-Signature-Prev | Present only for 24h after you rotate the signing secret — the same signature under your previous secret. Accept either while you roll your verifier so a rotation never drops a delivery. |
event_id, type, created_at,
api_version, livemode — plus a data object whose shape depends on the
event:
Example delivery body
Verify the signature
Never trust a delivery you haven’t verified. The signed payload is the timestamp header, a literal., then the raw body:
expected to X-LS-Webhook-Signature in constant time. Also reject
anything whose X-LS-Webhook-Timestamp is more than five minutes old — that
stops replay attacks.
During a secret rotation, deliveries also carry
X-LS-Webhook-Signature-Prev
for 24h — the same payload signed with your previous secret. Accept a match on
either header while you roll the new secret across your instances, and a
rotation never drops an event.Use a constant-time comparison —
hmac.compare_digest in Python, crypto.timingSafeEqual in Node. A plain == leaks timing information an attacker can use to forge signatures.Retries and idempotency
If your endpoint doesn’t return a2xx quickly, we retry with exponential backoff for up to 24 hours. That means:
- Return
2xxwithin 30 seconds. Do the minimum — verify, enqueue, respond. Push slow work (DB writes, downstream calls) onto a background queue. A slow handler looks like a failure and gets retried. - Deliveries can repeat. A retry after a network blip — or a delivery you already processed but responded to slowly — means the same event can arrive more than once. Dedupe on
event_id. Treat it as an idempotency key: if you’ve seen it, ack and move on.
Idempotent handling
Order is not guaranteed under retries. Design handlers to be self-contained: react to the state in
data, don’t assume the previous event already landed.Test-fire a delivery
Once a subscription exists, send yourself a real signed delivery:Fire a test event
event_types filter, so you can trigger it regardless of what you subscribed to. Use it to confirm your endpoint is reachable, your signature check passes, and your dedupe logic works before you rely on live events.
connection.capability_changed in depth
An active connection isn’t all-or-nothing. A bank might keep serving transactions while its statements feed breaks for a week. connection.capability_changed tells you when one individual capability flips between working and not-working — so you can, say, warn a user that statements are temporarily unavailable without tearing down the whole connection.
Capabilities that can change:
| Capability | Covers |
|---|---|
transactions | Transaction data feed. |
balance | Current balance. |
available_balance | Available (spendable) balance. |
statements | Statement documents. |
check_images | Check image retrieval. |
succeeded, failed, skipped, unsupported, pending.
Hysteresis: no flapping
We don’t fire on every hiccup. A capability only transitions after a run of consistent observations:failedafter 3 consecutive failed observations.succeededafter 2 consecutive succeeded observations.- Sentinel statuses (
unsupported,skipped,pending) are not debounced — they flip immediately and arrive withconsecutive_observations: 1.
Payload
connection.capability_changed
data.change. previous_status and
current_status tell you the direction of the flip; consecutive_observations
is how many in a row triggered it (3 for a fail, 2 for a recovery); last_error
is populated only when current_status is failed.
Other payload shapes
Most events carrydata.connection (shown above). Three carry a different data.
connection.requires_action adds an action object telling you what the user must do. The common one is widget_url:
connection.requires_action
action.kind is a discriminated union — widget_url (open it), mfa_challenge
(carries challenge_id + questions), or reauthorize. Branch on kind.
account.refresh.completed carries data.account, not data.connection:
account.refresh.completed
new_transaction_count > 0 as your cue to re-read
/accounts/{id}/transactions for that connection.
account.refresh.failed replaces the balances with a failed_at and an
error (same shape as HTTP error responses, including code, type, and
category):
account.refresh.failed
Next steps
Connection lifecycle
How connections move through initiated, active, failed, and disconnected.
Connect a bank
The full linking flow, from institution search to a canonical connection id.
Errors
The error envelope, status codes, and how to branch on
code.Testing
Sandbox banks and credentials to drive every event end-to-end.
