Skip to content

Plaid

The Plaid source ingests accounts and transactions from Plaid — a widely-used bank-data API with a token-per-connection model much like Teller and SimpleFIN. It runs alongside SimpleFIN, Teller, CSV import, and any other source, so you can mix providers in one ledger.

Source: internal/sources/plaid.

How it works

Plaid is a pull archetype source, the same archetype as SimpleFIN and Teller. On each sync the engine asks it to fetch; it fans out over every linked bank (each access token), lists that bank's accounts with balances, pulls its transactions in the lookback window, resolves the institution name, and merges everything into one neutral batch the engine persists.

  • One token per bank. A Plaid access token represents a single Item — one institution login. Link several banks through Plaid Link and add each token; the source fetches them all and merges the results. An Item that fails is logged and skipped, so one broken connection never blocks the rest.
  • Sign-flipped amounts. Plaid signs outflows positive — the opposite of kasas (and Teller/SimpleFIN). The source negates each amount so a purchase is stored negative and a refund positive, the convention every other source uses. The flip is done on the decimal string, with no float round-trip, so the value is exact — the same "data is sacred" handling every source gets.
  • Clean payee, raw description. Plaid's enriched merchant_name becomes the payee; the raw transaction name stays the description.
  • Namespaced ids. Accounts and transactions are stored under plaid:<id> so Plaid's ids never collide with another source's. The engine deduplicates by id, so overlapping fetches across syncs are idempotent.
  • Balances and institution names are best-effort. A balance that is absent, or an institution name that fails to resolve, is handled gracefully — the account still imports with its transactions rather than failing the sync.
  • Transactions are non-fatal. If an Item's transactions can't be fetched yet (Plaid's PRODUCT_NOT_READY right after linking), its accounts and balances still import and the next sync picks up the transactions.

Quiet until connected

Plaid is started only when both client_id and secret are configured. Until an access token is present it is skipped on sync, not errored — so setting up the app credentials and adding bank tokens later never logs failures in between.

Authentication

Plaid uses two layers of credentials, and the split maps cleanly onto kasas's config-vs-runtime secret model:

Credential What it is Where it goes
client_id + secret Your app-level Plaid credentials from the Plaid Dashboard, shared by every linked bank. The secret is per-environment. Sent in each request body. [plaid] config (infrastructure-level, set once): plaid.client_id / plaid.secret, or KASAS_PLAID_CLIENT_ID / KASAS_PLAID_SECRET.
Access token(s) A per-Item token — one per linked bank — obtained by exchanging a Plaid Link public_token. Identifies whose data to fetch. Runtime secrets — add each on the Sources page, and/or set plaid.access_token / plaid.access_tokens.

The environment (sandbox by default, or development / production) selects the Plaid host; the secret must match it. A bad or missing credential surfaces as a Plaid sync error, not a startup failure — one misconfigured source never takes down the rest.

Getting an access token

Plaid access tokens come from the Link flow: a front-end exchanges a public_token for an access token via /item/public_token/exchange. kasas consumes the resulting access token; running Link itself is outside kasas (use Plaid's Quickstart or your own Link integration). In the sandbox you can mint one with /sandbox/public_token/create followed by the exchange.

Configuration

Add a [plaid] block to your config file. The source starts when both client_id and secret are set.

[plaid]
client_id   = "your_client_id"
secret      = "your_sandbox_secret"
environment = "sandbox"
access_token = "access-sandbox-…"   # or add it from the Sources page

Set the production secret and environment, then add each bank's access token at runtime from the Sources page (it comes from Link, so the dashboard is the natural place for it).

[plaid]
client_id   = "your_client_id"
secret      = "your_production_secret"
environment = "production"
# access_token left empty — add banks from the dashboard Sources page.

For several banks, declare them as an array — and/or add more at runtime from the Sources page:

[plaid]
client_id     = "your_client_id"
secret        = "your_production_secret"
environment   = "production"
country_codes = ["US"]   # scopes institution-name lookups (optional; default US)
access_tokens = ["access-production-…", "access-production-…"]

The singular access_token is the env-friendly form (KASAS_PLAID_ACCESS_TOKEN). Config tokens and tokens added at runtime are unioned (deduplicated): config declares your fixed banks, the Sources page adds more without a restart.

Connecting banks at runtime

Once the source is started, open the dashboard Sources page: Plaid lists your connected banks (each masked) with a Remove button, and an Add field. For each bank, run it through Plaid Link to obtain an access token, paste it, and Add — no restart. Repeat for every bank; Remove disconnects one. Banks declared in config show as from config (edit the config file to change those).

Over REST, each token is one bank:

# Add a bank (append a token):
curl -X PUT https://<your-kasas-host>/api/v1/sources/plaid/credential \
  -H 'Content-Type: application/json' -d '{"token":"<access-token>"}'

# Remove a bank by its (masked) entry id from GET /api/v1/sources:
curl -X DELETE https://<your-kasas-host>/api/v1/sources/plaid/credentials/<id>

Managing it

Plaid is a first-class source, so it appears everywhere sources do:

  • Dashboard → Sources — connection status, the per-bank credential list (add / remove), and Sync now.
  • REST — GET /api/v1/sources (lists each bank, masked), POST /api/v1/sources/plaid/sync, PUT /api/v1/sources/plaid/credential (add a bank), and DELETE /api/v1/sources/plaid/credentials/{id} (remove one).
  • MCP — list_sources and sync_source (alongside trigger_sync, which syncs every source). Credential management stays on REST/dashboard, deliberately not MCP.

Limitations

  • Incremental sync. kasas re-fetches the lookback window each run via Plaid's /transactions/get (idempotent — the engine deduplicates by id), the same model as Teller and SimpleFIN. It does not yet use Plaid's cursor-based /transactions/sync, so removed transactions (cancelled pendings) are not pruned.
  • Transaction enrichment beyond payee (category, personal-finance category, location) is not yet mapped; it is a candidate for extensions when the engine persists per-batch extensions.

Where to go next