> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ledgersyncappv2.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Check on an async operation

> A handful of endpoints (initiate connection, refresh) hand
you back an `operation_id`. Polling
here is the fallback if you can't run a webhook handler —
usually you'll get the result via webhook first. The
`result` field is populated when `status=succeeded`; `error`
when `status=failed`.

**For `connection.initiate` operations specifically:** while
the operation is `queued`/`running` the embedded
`result.connection.id` is a UUID-prefixed placeholder
(`con_<uuid>`) that is **not** valid against any other
endpoint. Once `status=succeeded`, `result.connection.id`
is the canonical
`con_<SOURCE>_<bankAccountId>` (e.g. `con_FINICITY_41294`,
`con_MX_1224`) — store **that** id and use it for subsequent
`/connections`, `/accounts`, `/transactions`, and
`/statements` calls. See the **Connection lifecycle** section
at the top of this reference for the full handshake.




## OpenAPI

````yaml /openapi.yaml get /operations/{operation_id}
openapi: 3.1.0
info:
  title: LedgerSync API
  version: '2026-05-22'
  summary: A modern REST API for connecting bank accounts and pulling financial data
  description: >
    > **First time here?** The [**Getting started**
    guide](https://portal.ledgersyncappv2.com/dashboard/getting-started) walks
    the full integration end-to-end (about 15 minutes). This page is the
    endpoint reference for after you've read it.


    Welcome. This is LedgerSync's API for connecting your users' bank

    accounts and pulling their transactions, statements, and account

    details.


    Everything is JSON over HTTPS. Errors are easy to read. Webhooks

    fire as state changes. Sandbox is one key away.


    ## Connection lifecycle and the two `connection.id` formats


    A Connection's `id` changes shape once the user finishes linking

    their bank — there are two distinct identifiers, and you must

    only use the canonical one to read accounts, transactions, or

    statements.


    1. **Placeholder id (pending state).** `POST /v3/clients/{id}/connections`
       returns `202 Accepted`. The initiate operation first surfaces
       a `connection_with_action` result whose embedded
       `connection.id` is a UUID-prefixed placeholder of the form
       `con_<uuid>` (e.g. `con_01HXYZ8A6N7K2W9PQ4T5Z3V6E0`), paired
       with a `widget_url`. The placeholder is **not** a valid id for
       any other endpoint — calling
       `GET /v3/connections/{placeholder_uuid}` returns
       `400 Bad Request` with `missing source separator`. Treat it as
       opaque routing state for the widget step only.
    2. **Canonical id (active state).** Once the user finishes the
       widget and the source pushes its first callback, the
       connection moves to `status=active` and the
       `connection.active` webhook fires. From that point on every
       surface — `GET /v3/clients/{id}/connections`,
       `GET /v3/connections/{id}`, the webhook payload, and the
       `connection.initiate` operation when re-polled — reports the
       canonical id `con_<SOURCE>_<bankAccountId>` (for example
       `con_FINICITY_41294`, `con_MX_1224`). **This** is the id every
       other endpoint accepts (`/accounts`, `/transactions`,
       `/statements`, refresh, delete).

    The correct integration pattern is therefore:


    - Initiate the connection, capture `operation_id`.

    - Poll `GET /v3/operations/{operation_id}` (or wait for the
      `connection.active` webhook) until `status=succeeded`.
    - Read `result.connection.id` **from the succeeded operation** —
      that is the canonical id. Store it; do not store the
      placeholder.

    Do not try to `GET /v3/connections/{placeholder_uuid}` during the

    pending window. Listing endpoints

    (`GET /v3/clients/{id}/connections`, `GET /v3/connections/{id}`)

    only ever return canonical ids, because they only surface

    connections that have reached `active`.


    ## Quick start


    Mint a sandbox key in the developer portal, then create your first

    **Client** (your end-user — the person whose bank we'll be reading):


    ```bash

    curl \
      https://api-sandbox.ledgersyncappv2.com/v3/clients \
      -H "Authorization: Bearer sk_test_..." \
      -H "Content-Type: application/json" \
      -d '{"email":"alice@example.com","name":"Alice"}'
    ```


    You'll get back a `Client` object with an `id`. From there the

    full flow is:


    1. **Register a webhook** at `POST /v3/webhooks/subscriptions` so
       you can be notified when the connection progresses.
    2. **Initiate a connection** at
       `POST /v3/clients/{id}/connections` with an `institution_id`
       from `GET /v3/institutions`. LedgerSync's router picks the
       underlying source. Every source hands back a `widget_url` in
       the `connection.requires_action` result — for FDE it points at
       a LedgerSync-hosted connect page where credentials are entered,
       never sent to the API.
    3. **Open the widget URL** in your user's browser. They pick
       their bank, log in, and choose accounts to share.
    4. **Receive `connection.active`** on your webhook URL. List
       accounts and transactions.

    **Don't want to build your own bank picker?** Skip steps 2–3: call

    `POST /v3/clients/{id}/connect-session` to get a LedgerSync-hosted

    link, and email it to your member. They open it, search for their

    own bank, pick it, and connect it — all on a page we host, with no

    `institution_id` needed up front. You still receive

    `connection.active` on your webhook exactly as above. Revoke a link

    at any time with `DELETE /v3/clients/{id}/connect-session/{sid}`.

    (This is the v3 replacement for the old `account/add/lite` widget.)


    Want a step-by-step walkthrough with curl per step plus a sandbox

    shortcut that skips the widget? Read the

    [Getting started
    guide](https://portal.ledgersyncappv2.com/dashboard/getting-started).


    ## Authentication


    Pass your secret key in the `Authorization` header as a Bearer

    token: `Authorization: Bearer sk_test_...` (sandbox) or

    `Bearer sk_live_...` (production). Treat secret keys like

    passwords — never embed them in mobile apps or front-end code.


    ## Conventions


    **Sync vs async.** Most endpoints respond synchronously — you

    get the resource back right away. A handful of flows are

    genuinely async (initiating a bank connection, extracting a

    statement, generating a verification report); those return

    `202 Accepted`

    with an `operation_id` you can poll, and the matching webhook

    fires when the work finishes.


    **Errors.** Every error is `{ "error": { "code", "message", "doc_url",
    "type" } }`.

    Branch on `code`. Click `doc_url` for the troubleshooting page.

    Every response carries an `X-LS-Trace-Id` header — paste it in

    support tickets and we can jump straight to your request.


    **Webhooks.** Every delivery carries `X-LS-Webhook-Signature`,

    computed over the **raw request body** with HMAC-SHA256. Verify

    it before trusting the payload. Full event catalog + signature

    example on the [Webhooks tab](#webhooks).


    ## Need help?


    Email [support@ledgersync.com](mailto:support@ledgersync.com) or

    open a thread in the developer portal. Quote the `X-LS-Trace-Id`

    from your response — it makes everything faster.
  contact:
    name: LedgerSync Developer Support
    email: support@ledgersync.com
    url: https://portal.ledgersyncappv2.com
  license:
    name: Proprietary
    url: https://ledgersync.com/terms
servers:
  - url: https://api-sandbox.ledgersyncappv2.com/v3
    description: >-
      Sandbox — use `sk_test_...` keys from the developer portal. Routes to the
      real Finicity and MX sandbox banks (FinBank, mxbank) via the same
      connector code paths as production.
  - url: https://api.ledgersyncappv2.com/v3
    description: >-
      Production — use `sk_live_...` keys. Hits real banks via Finicity, MX, or
      FDE depending on the connection.
security:
  - bearer: []
tags:
  - name: Clients
    description: |
      A **Client** is the end-user whose bank accounts you're managing
      — usually a real person or business. You create one Client per
      user before linking any accounts. Pass your own
      `external_id` to join Clients back to records in your system.
  - name: Connections
    description: |
      A **Connection** links a Client to a bank or financial-data
      source (Finicity, MX, or FDE). One Client can have many
      Connections — one per bank they've linked. You initiate a
      Connection, the user finishes it (widget or MFA), and a
      `connection.active` webhook tells you when it's ready.
  - name: Accounts
    description: |
      Once a Connection goes active, the bank's accounts (checking,
      savings, credit, loans) show up here. Use these endpoints to
      list, inspect, and refresh them.
  - name: Transactions
    description: |
      Posted transactions and pending charges for an account. New
      transactions are surfaced by a future push-delivery event for
      transactions after each refresh; you can also list/page them
      directly.
  - name: Statements
    description: |
      Period statements (usually monthly PDFs) the bank publishes.
      For FDE-sourced statements, extracted line items are attached
      after OCR completes.
  - name: Operations
    description: |
      The handful of LedgerSync calls that genuinely run asynchronously
      return an `operation_id`. Poll it here, or just listen for the
      matching webhook — your call.
  - name: Webhooks
    description: |
      Manage where LedgerSync delivers event notifications. Each
      subscription has its own HMAC signing secret. Rotate it whenever
      you want — the old secret stays valid for 24 hours so deploys
      don't break verification.
  - name: ApiKeys
    description: |
      Create, list, and revoke API keys. Keys are scoped to one
      environment (sandbox or live). The plaintext secret is returned
      exactly once at creation — store it somewhere safe.
  - name: Institutions
    description: |
      The unified v3 institution catalog. Search by name; pick a row;
      pass its `id` to `POST /clients/{client_id}/connections`.
      LedgerSync's router decides which underlying source (Finicity,
      MX) to use — integrators don't pick a source.
  - name: Settings
    description: |
      Account-level settings the integrator manages once per environment.
      Currently exposes the redirect-URL allowlist consulted at
      `/connections/initiate`. Same scope as Stripe Connect / Plaid Link /
      OAuth — register your app's redirect URIs once, not per end-user.
  - name: Sandbox
    description: |
      Sandbox-only helpers. Use `GET /sandbox/institutions` to list
      the sandbox-eligible test banks you can target when creating a
      sandbox connection (Finicity FinBank, MX `mxbank`, FDE test
      extractors). Lifecycle transitions and webhooks come from the
      same real connector paths as live traffic — no synthetic
      lifecycle driver, no replay fixtures. Drive the widget with
      the public test bank credentials documented in the developer
      portal.
  - name: Metrics
    description: |
      Read-only aggregations powering the developer portal's "Metrics"
      page. Every endpoint is scoped to the authenticated principal's
      customer and environment — `sandbox` and `live` data never mix
      across the wire. Default window when `from`/`to` are omitted is
      the last 30 days (max 366).
  - name: Health
    description: A simple liveness probe. No auth required.
  - name: PortalGating
    description: |
      Developer-portal access-request endpoints (sandbox + live gating).
      These are HMAC-signed calls from the LedgerSync portal to v3 and not
      part of the integrator-facing API surface. Documented here so the
      single spec stays authoritative.
paths:
  /operations/{operation_id}:
    parameters:
      - in: path
        name: operation_id
        required: true
        schema:
          type: string
    get:
      tags:
        - Operations
      summary: Check on an async operation
      description: |
        A handful of endpoints (initiate connection, refresh) hand
        you back an `operation_id`. Polling
        here is the fallback if you can't run a webhook handler —
        usually you'll get the result via webhook first. The
        `result` field is populated when `status=succeeded`; `error`
        when `status=failed`.

        **For `connection.initiate` operations specifically:** while
        the operation is `queued`/`running` the embedded
        `result.connection.id` is a UUID-prefixed placeholder
        (`con_<uuid>`) that is **not** valid against any other
        endpoint. Once `status=succeeded`, `result.connection.id`
        is the canonical
        `con_<SOURCE>_<bankAccountId>` (e.g. `con_FINICITY_41294`,
        `con_MX_1224`) — store **that** id and use it for subsequent
        `/connections`, `/accounts`, `/transactions`, and
        `/statements` calls. See the **Connection lifecycle** section
        at the top of this reference for the full handshake.
      operationId: getOperation
      responses:
        '200':
          description: The Operation, with `result` or `error` once it's done.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Operation'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
components:
  schemas:
    Operation:
      type: object
      description: Tracks an async operation's progress.
      required:
        - id
        - type
        - status
        - created_at
      properties:
        id:
          type: string
          example: op_01HXYZ8A6N7K2W9PQ4T5Z3V6E0
        type:
          type: string
          enum:
            - connection.initiate
            - connection.complete
            - account.refresh
            - statement.extract
            - transaction.sync
          example: account.refresh
        status:
          type: string
          enum:
            - queued
            - running
            - succeeded
            - failed
        result:
          description: |
            Resource produced by the operation, present only when
            `status=succeeded`. The `kind` field discriminates the
            variant for SDK code generation.
          oneOf:
            - $ref: '#/components/schemas/OperationResultConnection'
            - $ref: '#/components/schemas/OperationResultConnectionWithAction'
            - $ref: '#/components/schemas/OperationResultStatement'
            - $ref: '#/components/schemas/OperationResultStatementExtraction'
            - $ref: '#/components/schemas/OperationResultTransactionSync'
          discriminator:
            propertyName: kind
            mapping:
              connection:
                $ref: '#/components/schemas/OperationResultConnection'
              connection_with_action:
                $ref: '#/components/schemas/OperationResultConnectionWithAction'
              statement:
                $ref: '#/components/schemas/OperationResultStatement'
              statement_extraction:
                $ref: '#/components/schemas/OperationResultStatementExtraction'
              transaction_sync:
                $ref: '#/components/schemas/OperationResultTransactionSync'
        error:
          allOf:
            - $ref: '#/components/schemas/ErrorInfo'
          description: |
            Populated when `status=failed`. Same shape as `error.*`
            in an HTTP error response — branch on `code`.
        created_at:
          type: string
          format: date-time
        completed_at:
          type:
            - string
            - 'null'
          format: date-time
    OperationResultConnection:
      type: object
      required:
        - kind
        - connection
      properties:
        kind:
          type: string
          enum:
            - connection
        connection:
          $ref: '#/components/schemas/Connection'
    OperationResultConnectionWithAction:
      type: object
      required:
        - kind
        - connection
      properties:
        kind:
          type: string
          enum:
            - connection_with_action
        connection:
          $ref: '#/components/schemas/ConnectionWithAction'
    OperationResultStatement:
      type: object
      required:
        - kind
        - statement
      properties:
        kind:
          type: string
          enum:
            - statement
        statement:
          $ref: '#/components/schemas/Statement'
    OperationResultStatementExtraction:
      type: object
      description: |
        Result of a `statement.extract` operation started via
        `POST /statements/extract`: the transactions OCR-extracted from
        the uploaded PDF. Standalone — no Statement resource is created.
      required:
        - kind
        - page_count
        - transaction_count
        - transactions
      properties:
        kind:
          type: string
          enum:
            - statement_extraction
        page_count:
          type:
            - integer
            - 'null'
          description: Pages in the uploaded PDF.
        transaction_count:
          type: integer
          description: Number of extracted transactions.
        validated:
          type:
            - boolean
            - 'null'
          description: |
            Whether the extracted rows reconciled against the balances
            printed on the statement. `null` when the statement carries
            no reconcilable balances.
        validation_message:
          type:
            - string
            - 'null'
        transactions:
          type: array
          items:
            $ref: '#/components/schemas/ExtractedStatementTransaction'
    OperationResultTransactionSync:
      type: object
      required:
        - kind
        - account_id
        - new_transaction_count
      properties:
        kind:
          type: string
          enum:
            - transaction_sync
        account_id:
          type: string
        new_transaction_count:
          type: integer
    ErrorInfo:
      type: object
      description: |
        The inner error payload. Used as the top-level body of an
        HTTP error response (wrapped in `ErrorEnvelope`) and as the
        embedded `error` field on resources that record a failure
        (like `Operation.error`).
      required:
        - code
        - message
        - type
      properties:
        code:
          type: string
          description: |
            Stable machine-readable identifier. Branch on this in
            your code, not on `message`.
          example: unknown_api_key
        message:
          type: string
          description: Plain-English explanation safe to log.
          example: The API key you presented doesn't match any active key.
        doc_url:
          type: string
          format: uri
          description: |
            Link to the docs page for this specific error code.
            Shareable with teammates in support tickets.
          example: https://portal.ledgersyncappv2.com/errors/unknown_api_key
        type:
          type: string
          description: |
            Stripe-style broad failure-mode category — useful for
            "treat all of these the same way" branches. Derived
            from the HTTP status. Orthogonal to `category`, which
            classifies by origin.
          enum:
            - auth_error
            - invalid_request
            - rate_limit_error
            - idempotency_error
            - not_found
            - api_error
          example: auth_error
        category:
          type: string
          description: |
            Plaid-style coarse classification by origin. Branch on
            this for routing logic — retry the request, surface to
            the end user in the widget, or page on-call. Orthogonal
            to `type` (which is HTTP-status-derived).
          enum:
            - AUTH_ERROR
            - INSTITUTION_ERROR
            - CAPABILITY_UNAVAILABLE
            - CONNECTION_ERROR
            - RATE_LIMIT
            - INVALID_REQUEST
            - RESOURCE_NOT_FOUND
            - PLATFORM_ERROR
          example: AUTH_ERROR
        is_user_actionable:
          type: boolean
          description: |
            True when the end user can resolve this error (re-enter
            credentials, complete MFA, accept an updated
            agreement). False when the error requires
            institution-side or LS platform-side action. Useful for
            deciding whether to send the end user back to the
            widget or surface an "operational issue" banner.
          example: true
        source_diagnostic_code:
          type: string
          pattern: ^(FIN|MX|FDE)-[A-Z0-9_]+$
          description: |
            Opaque upstream-source diagnostic identifier (e.g.
            `FIN-103`, `MX-DENIED`). Present only on errors that
            originated at an aggregator/extractor. Use for support
            triage — do NOT branch on this value; the unified
            `code` and `category` are the integrator-facing
            vocabulary.
          example: FIN-103
        param:
          type: string
          description: |
            For validation failures (4xx), the request field that
            tripped the check. Dot-separated path for nested
            fields.
          example: client.email
        trace_id:
          type: string
          description: |
            Same as the `X-LS-Trace-Id` response header — paste in
            a support ticket to jump to the request.
          example: 4bf92f3577b34da6a3ce929d0e0e4736
        errors:
          type: array
          description: |
            Per-field validation errors (only present on 400 when
            multiple fields failed at once).
          items:
            type: object
            required:
              - param
              - message
            properties:
              param:
                type: string
                example: client.email
              message:
                type: string
                example: must be a valid email address
              code:
                type: string
                example: invalid_email
    ErrorEnvelope:
      type: object
      description: |
        Error envelope returned as the top-level body of every non-2xx
        HTTP response. The actual error payload lives in the `error`
        field — branch on `error.code`.
      required:
        - error
      properties:
        error:
          $ref: '#/components/schemas/ErrorInfo'
    Connection:
      type: object
      required:
        - id
        - client_id
        - source
        - status
        - created_at
      properties:
        id:
          type: string
          description: |
            Connection identifier. Two shapes can appear depending on
            lifecycle state:

            - `con_<uuid>` — placeholder id surfaced during the
              `connection_with_action` result while the connection is
              `pending`. Used only to route the widget step. Calling
              `GET /v3/connections/{placeholder_uuid}` returns 400 with
              `missing source separator`.
            - `con_<SOURCE>_<bankAccountId>` — canonical id once the
              connection reaches `active`. Every other endpoint
              (`/accounts`, `/transactions`, `/statements`, refresh,
              delete) expects this form.

            See the lifecycle prose at the top of this document.
          example: con_FINICITY_41294
        client_id:
          type: string
          example: cli_01HXYZ8A6N7K2W9PQ4T5Z3V6E0
        source:
          $ref: '#/components/schemas/Source'
        status:
          $ref: '#/components/schemas/ConnectionStatus'
        institution:
          type:
            - object
            - 'null'
          properties:
            id:
              type: string
            name:
              type: string
              example: Chase
            logo_url:
              type:
                - string
                - 'null'
              format: uri
        last_refreshed_at:
          type:
            - string
            - 'null'
          format: date-time
        last_error:
          oneOf:
            - $ref: '#/components/schemas/ErrorInfo'
            - type: 'null'
        expected_capabilities:
          description: |
            Catalog-derived promise for each capability we may deliver
            on this connection. Plaid-style 4-tier:
            `always`/`usually`/`sometimes`/`never`. Set when the
            connection enters `active` and refreshed when the underlying
            catalog row changes. Phase 1 emits only `always`/`never`
            (per-FI tiering uses a profile catalog scheduled for Phase 2).
          oneOf:
            - $ref: '#/components/schemas/ExpectedCapabilities'
            - type: 'null'
        realized_capabilities:
          description: |
            Observed outcome of the most recent attempt at each
            capability. Rolled-up across the connection's accounts —
            worst-status-wins per capability. See
            `RealizedCapabilities` for the per-capability shape.
          oneOf:
            - $ref: '#/components/schemas/RealizedCapabilities'
            - type: 'null'
        _debug:
          $ref: '#/components/schemas/ConnectionDebug'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    ConnectionWithAction:
      allOf:
        - $ref: '#/components/schemas/Connection'
        - type: object
          required:
            - action
          properties:
            action:
              oneOf:
                - $ref: '#/components/schemas/ConnectionActionWidget'
                - $ref: '#/components/schemas/ConnectionActionMfa'
                - $ref: '#/components/schemas/ConnectionActionReauthorize'
              discriminator:
                propertyName: kind
                mapping:
                  widget_url:
                    $ref: '#/components/schemas/ConnectionActionWidget'
                  mfa_challenge:
                    $ref: '#/components/schemas/ConnectionActionMfa'
                  reauthorize:
                    $ref: '#/components/schemas/ConnectionActionReauthorize'
    Statement:
      type: object
      required:
        - id
        - statement_date
      properties:
        id:
          type: string
          example: stmt_FINICITY_551
        account_id:
          type:
            - string
            - 'null'
          description: >-
            The owning account id. Populated when listing an account's
            statements; null on single get-by-id (the backend statement record
            carries no fin-account id to map back).
        source:
          $ref: '#/components/schemas/Source'
        statement_date:
          type: string
          format: date
        download_url:
          type:
            - string
            - 'null'
          format: uri
          description: >-
            Relative v3 URL to download the statement PDF, auth-scoped to your
            API key. Resolves to [`GET
            /statements/{statement_id}/download`](#operation/downloadStatement);
            GET it to stream the PDF bytes.
        exported_to:
          type:
            - string
            - 'null'
          description: Accounting platform the statement was exported to, if any.
    ExtractedStatementTransaction:
      type: object
      required:
        - date
        - description
        - amount
      properties:
        date:
          type: string
          description: |
            Transaction date as printed on the statement, normalized by
            the extractor — not guaranteed ISO-8601.
        description:
          type: string
        amount:
          type: number
          description: Signed decimal amount (deposits positive, charges negative).
        running_balance:
          type:
            - number
            - 'null'
          description: Printed per-row running balance, when the statement has one.
        principal_amount:
          type:
            - number
            - 'null'
          description: Principal portion for loan/mortgage statements that print one.
    Source:
      type: string
      description: |
        Financial-data source backing a connection. All sources wrap
        upstream aggregators or extraction pipelines.

        Values:
        - `FINICITY` — Finicity aggregation
        - `MX` — MX aggregation
        - `FDE` — Financial Document Extraction (LedgerSync proprietary)
      enum:
        - FINICITY
        - MX
        - FDE
      x-enum-descriptions:
        - Finicity aggregation
        - MX aggregation
        - Financial Document Extraction (LedgerSync proprietary)
    ConnectionStatus:
      type: string
      description: >
        Connection lifecycle status.


        - `initiated` — Connection started, waiting for end-user or upstream to
        complete

        - `requires_action` — Awaiting MFA or reauthorization from the end-user

        - `active` — Healthy, refreshing on schedule

        - `failed` — Connection failed; review `last_error`

        - `disconnected` — End-user revoked or LedgerSync disconnected
      enum:
        - initiated
        - requires_action
        - active
        - failed
        - disconnected
      x-enum-descriptions:
        - Connection started, waiting for end-user or upstream to complete
        - Awaiting MFA or reauthorization from the end-user
        - Healthy, refreshing on schedule
        - Connection failed; review last_error
        - End-user revoked or LedgerSync disconnected
    ExpectedCapabilities:
      type: object
      description: |
        Catalog-derived promise for each capability we may deliver on a
        connection. Exhaustive — every capability key is always present
        so integrators don't need to branch on key existence.
      required:
        - transactions
        - balance
        - available_balance
        - statements
        - check_images
      properties:
        transactions:
          $ref: '#/components/schemas/ExpectedCapability'
        balance:
          $ref: '#/components/schemas/ExpectedCapability'
        available_balance:
          $ref: '#/components/schemas/ExpectedCapability'
        statements:
          $ref: '#/components/schemas/ExpectedCapability'
        check_images:
          $ref: '#/components/schemas/ExpectedCapability'
    RealizedCapabilities:
      type: object
      description: |
        Observed per-capability outcomes. Exhaustive — every capability
        key is always present. Rolled-up across accounts at the
        Connection level; per-account ground truth at the Account level.
      required:
        - transactions
        - balance
        - available_balance
        - statements
        - check_images
      properties:
        transactions:
          $ref: '#/components/schemas/RealizedCapability'
        balance:
          $ref: '#/components/schemas/RealizedCapability'
        available_balance:
          $ref: '#/components/schemas/RealizedCapability'
        statements:
          $ref: '#/components/schemas/RealizedCapability'
        check_images:
          $ref: '#/components/schemas/RealizedCapability'
    ConnectionDebug:
      type: object
      x-stability: experimental
      description: |
        Observable-only debug metadata. **Do not parse, do not branch
        routing on these values.** Fields under `_debug` may be removed,
        renamed, or hashed at any time. Use the documented
        `realized_capabilities` and `last_error` blocks for capability
        and failure data.
      properties:
        source_used:
          type:
            - string
            - 'null'
          description: |
            Internal source identifier that handled this connection.
            For troubleshooting only — integrators should treat this as
            opaque. Example values: `FINICITY`, `MX`, `FDE`.
    ConnectionActionWidget:
      type: object
      required:
        - kind
        - widget_url
        - expires_at
      properties:
        kind:
          type: string
          enum:
            - widget_url
        widget_url:
          type: string
          format: uri
          description: |
            Hosted widget URL the end-user must open. For Finicity this
            is the Finicity Connect URL; for MX it is a
            LedgerSync-hosted wrapper page
            (`GET /connections/mx-wrapper?token=...`) that embeds the MX
            widget and completes with the same `redirect_url` +
            `ls_token` hop as Finicity. Same shape either way — open it
            and wait for your redirect/webhook.
        expires_at:
          type: string
          format: date-time
    ConnectionActionMfa:
      type: object
      required:
        - kind
        - challenge_id
        - questions
      properties:
        kind:
          type: string
          enum:
            - mfa_challenge
        challenge_id:
          type: string
        questions:
          type: array
          items:
            type: object
            required:
              - id
              - prompt
              - type
            properties:
              id:
                type: string
              prompt:
                type: string
              type:
                type: string
                enum:
                  - text
                  - choice
                  - image_choice
              choices:
                type: array
                items:
                  type: string
    ConnectionActionReauthorize:
      type: object
      required:
        - kind
        - reauth_url
        - expires_at
      properties:
        kind:
          type: string
          enum:
            - reauthorize
        reauth_url:
          type: string
          format: uri
        expires_at:
          type: string
          format: date-time
    ExpectedCapability:
      type: object
      required:
        - promise
      properties:
        promise:
          $ref: '#/components/schemas/CapabilityPromise'
    RealizedCapability:
      type: object
      required:
        - status
      properties:
        status:
          $ref: '#/components/schemas/CapabilityRealizedStatus'
        last_success_at:
          type:
            - string
            - 'null'
          format: date-time
          description: Timestamp of the most recent successful delivery, when known.
        next_refresh_after:
          type:
            - string
            - 'null'
          format: date-time
          description: |
            Earliest moment the next attempt is expected to fire.
            Null when no further attempt is scheduled (e.g. `unsupported`).
        last_error:
          description: |
            Source-translated error from the most recent failed attempt.
            Same shape as the top-level `ErrorInfo` envelope; only
            populated when `status=failed`.
          oneOf:
            - $ref: '#/components/schemas/ErrorInfo'
            - type: 'null'
    CapabilityPromise:
      type: string
      description: |
        Plaid-style 4-tier promise tier for a single capability.
        `always` — source guarantees delivery (e.g. Finicity
        transactions on a known-good FI). `usually` — supported,
        historical success rate is high. `sometimes` — supported but
        reliability is variable or unknown. `never` — source
        structurally cannot deliver this capability.
      enum:
        - always
        - usually
        - sometimes
        - never
    CapabilityRealizedStatus:
      type: string
      description: |
        Observed outcome of the most recent attempt at a capability.
        `succeeded` — last attempt delivered data; `last_success_at` is
        populated. `failed` — last attempt errored; `last_error` carries
        the source-translated diagnostic. `skipped` — capability not
        attempted this cycle (catch-up exhausted, integrator opt-out).
        `unsupported` — matching `expected_capabilities` promise is
        `never`; entry is still emitted so integrators don't have to
        disambiguate "missing means unsupported" from "not yet
        attempted". `pending` — initial sync running or attempt
        in flight.
      enum:
        - succeeded
        - failed
        - skipped
        - unsupported
        - pending
  responses:
    Unauthorized:
      description: |
        Either the `Authorization` header is missing or the bearer
        token doesn't match an active API key. Double-check the key
        and the environment — sandbox keys can't be used against the
        production base URL and vice versa.
      headers:
        X-LS-Trace-Id:
          $ref: '#/components/headers/X-LS-Trace-Id'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    NotFound:
      description: |
        We didn't find a resource matching the path parameters. Common
        causes: the id belongs to a different customer's key, the
        resource was deleted, or there's a typo in the id.
      headers:
        X-LS-Trace-Id:
          $ref: '#/components/headers/X-LS-Trace-Id'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
  headers:
    X-LS-Trace-Id:
      description: Distributed-trace ID for this request. Quote in support tickets.
      schema:
        type: string
        example: 4bf92f3577b34da6a3ce929d0e0e4736
  securitySchemes:
    bearer:
      type: http
      scheme: bearer
      bearerFormat: LedgerSync API key
      description: |
        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.

````