Skip to main content
Every error we return has the same shape and a stable, machine-readable code. Branch on the code, show your user something friendly, log the trace id, and consult the catalog below for the exact action. That’s the whole game.

The error envelope

Every error response, validation, auth, not found, rate limit, or a bank-side failure, carries the same JSON body under error:
code, message, doc_url, type, category and is_user_actionable are on every error. The rest appear only when they apply.
Branch on code, never on message and never on type. We reword messages over time, and type is deliberately coarse, several very different codes share type: api_error. The code is the contract.

HTTP status, at a glance

Error-code catalog

Every code the API can return. The catalog is also browsable at portal.ledgersyncappv2.com/errors, and each error.doc_url deep-links to one entry.

Authentication & authorization

Request problems

Idempotency-Key is reserved and not honored yet. The header is accepted and ignored: no response is replayed and idempotency_conflict is never returned. Do not auto-retry a failed POST on the assumption that the key protects you — a retried POST /v3/clients or POST /v3/clients/{id}/connections creates a second one. Send the header if you want to be ready for it, but make retries safe on your side, and reconcile by listing before you re-create.

Rate limit

Connection refresh

Outcomes of POST /connections/{id}/refresh. Like statement_extraction_failed, these arrive inside the polled operation’s error rather than as the HTTP response - the call itself answers 202 and the verdict lands later. A refresh is one aggregation at the bank but reports per sub-account, so the operation only turns terminal once every account on the connection has reported. When it does, error.accounts[] lists each account with its own status (succeeded / failed / pending) and, where it failed, its own nested error.
refresh_timed_out and refresh_outcome_not_observable look alike and mean different things. The first is “the bank or our pipeline was slow”, and retrying can help. The second is a configuration gap on our side that retrying will never clear - chasing the institution over it would send you somewhere there is nothing to find.

Server

Connection & source errors (the bank side)

These come back on connection operations, most often inside the polled operation’s error, with the raw upstream code in error.source_diagnostic_code. All carry type: api_error and HTTP 502, so you must branch on code, not on type. They group cleanly by the action they require:
The wire error.category is too coarse to branch on. CAPABILITY_UNAVAILABLE covers agreement_required, statements_not_supported, and fde_unsupported; CONNECTION_ERROR covers both mfa_required and reauthorization_required. Always switch on error.code.
Send the end user back through the widget (re-authenticate): Don’t re-auth, retry with backoff (credentials are fine): Don’t retry, the capability isn’t there: User must act at the bank (not in your widget):

A handling pattern

The same steps work in any language: check the status, parse the envelope, branch on code, log the trace id.

Rate limiting

A 429 (rate_limit_exceeded) just means slow down. Use exponential backoff with jitter, and honor Retry-After when present.
The same backoff is the right response to 5xx and the retryable bank-side codes above. Only non-429 4xx errors mean “don’t retry until you change something.”

The trace id

Every response, success or error, includes an X-LS-Trace-Id header (also mirrored as error.trace_id in error bodies):
Log it on every request, even successful ones. When something looks wrong downstream (“this account has no transactions”), the trace id from the original read is what we need.

Getting help

When you file a ticket, include: the X-LS-Trace-Id (or error.trace_id), the error.code, the endpoint, and whether you were in sandbox or live. That’s everything we need to find your exact request.

Testing in sandbox

Reproduce every error path on purpose with sandbox test banks.

Connection lifecycle

Most bank-side codes surface as a connection.failed transition.