---
title: "SimpleFIN protocol reference"
description: "Wire-level reference for the BankSync SimpleFIN server: the claim exchange, endpoints, the superset Account Set, query parameters and limits."
section: "API"
canonical: "https://banksync.io/docs/api/simplefin"
---

BankSync is a [SimpleFIN](https://www.simplefin.org/protocol.html) server. This page is the wire-level reference for people writing or debugging a SimpleFIN client against it. If you just want to connect Actual Budget or Securo, the [setup guide](/docs/integrations/simplefin-setup) is shorter and has the per-app steps.

> **Root URL:** https\://api.banksync.io/simplefin. Every endpoint below is relative to it. The same paths are
> served on the staging host for integration testing.

## The exchange

A SimpleFIN **setup token** is standard base64 of a claim URL. A client decodes it, POSTs to the
URL once, and receives an **Access URL** whose userinfo is the long-lived credential:

```bash
# 1. Decode the setup token to a claim URL, then POST to it (once).
CLAIM_URL="$(echo "$SETUP_TOKEN" | base64 --decode)"
ACCESS_URL="$(curl -s -X POST -H 'Content-Length: 0' "$CLAIM_URL")"
# => https://<id>:<secret>@api.banksync.io/simplefin

# 2. Read accounts with the credentials embedded in that URL.
curl -s "${ACCESS_URL}/accounts?version=2&pending=1"
```

Store the Access URL as securely as you would the financial data it returns — it is a bearer
credential, and it is the only thing standing between the internet and the account history it can
read. Setup tokens come from Settings → Developers → SimpleFIN in the app, and from the
`create_simplefin_access` MCP tool.

## Endpoints

| Endpoint              | Auth  | Returns                                                                                                              |
| --------------------- | ----- | -------------------------------------------------------------------------------------------------------------------- |
| `GET /info`           | none  | `{ "versions": ["1.0", "2.0"] }`                                                                                     |
| `GET /create`         | none  | `302` to the BankSync app's create-access screen (the link SimpleFIN clients show for "get a token")                 |
| `POST /claim/{token}` | none  | `200` with the Access URL as **plain text**; `403` plain text starting with `Forbidden` for any invalid token        |
| `GET /accounts`       | Basic | `200` with an Account Set; `403` for any credential problem; `429` with `Retry-After` when the daily budget is spent |

A claim works exactly once. A second POST with the same token is refused with `403`, which the
protocol tells clients to surface as "this token may be compromised" — so if you see it during
development, you pasted the same token twice. Claims are also rate-limited per IP.

`/accounts` returns a single `403` for every authentication failure — unknown id, wrong secret,
revoked, rotated, never claimed. That is the protocol's design (it denies an attacker an oracle)
and the body is still a well-formed Account Set with an `errlist` entry, so a client that parses
before it checks the status gets something it can show.

## The Account Set

One response satisfies both dialects of the protocol, so a client written against either works
without negotiating. Version 1 fields (`org`, `errors`) and version 2 fields (`connections`,
`conn_id`, `errlist`) are both present in every response.

| Field                          | What it holds                                                                                                                        |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
| `accounts[].id`                | Stable account identifier — link against this                                                                                        |
| `accounts[].name`              | The account name as BankSync shows it                                                                                                |
| `accounts[].currency`          | ISO 4217 code                                                                                                                        |
| `accounts[].balance`           | Decimal **string**, never a float. Negative for credit cards, loans and mortgages                                                    |
| `accounts[].available-balance` | Decimal string, when the provider reports one                                                                                        |
| `accounts[].balance-date`      | Unix seconds — when the institution last refreshed that balance, not when you asked                                                  |
| `accounts[].org`               | `{ id, name, "sfin-url", domain?, url? }` — v1 institution metadata                                                                  |
| `accounts[].conn_id`           | Key into `connections[]` — v2 institution linkage                                                                                    |
| `accounts[].transactions`      | Omitted entirely on `balances-only=1`; otherwise always an array                                                                     |
| `accounts[].holdings`          | Investment positions: `id`, `symbol`, `description`, `shares`, `market_value`, `cost_basis`, `purchase_price`, `currency`, `created` |
| `connections[]`                | `{ conn_id, name, org_id, org_name?, org_url?, sfin_url }` — the same institutions in v2 shape                                       |
| `errors[]`                     | v1: human-readable strings, one per bank that could not be read                                                                      |
| `errlist[]`                    | v2: `{ code, msg, conn_id?, account_id? }` with protocol error codes (`con.auth`, `con.`, `gen.`)                                    |
| `x-api-message[]`              | Per-request advice — a clamped window, a quota warning. Absent when there is none                                                    |

Transactions carry `id`, `posted` (Unix seconds; `0` while pending), `amount` (signed decimal
string), `description`, `pending`, and — where the provider gives them — `payee`, `memo` and
`transacted_at`.

A bank that needs re-authentication is reported as `con.auth` with the message
`Connection to <bank> may need attention`, and every healthy bank's data still ships. Actual
pattern-matches that exact prefix to show its reconnect prompt, so the wording is stable.

### Query parameters

| Parameter         | Effect                                                             |
| ----------------- | ------------------------------------------------------------------ |
| `start-date`      | Unix epoch seconds, inclusive. Defaults to 30 days ago.            |
| `end-date`        | Unix epoch seconds, exclusive. Defaults to the end of today (UTC). |
| `pending=1`       | Include pending transactions. Excluded by default.                 |
| `account=<id>`    | Repeatable. Narrows the response to those accounts.                |
| `balances-only=1` | Skip transactions entirely — cheap for listing accounts.           |
| `version=2`       | Accepted, but the response is the same superset either way.        |

Parameters are total: an unparseable value falls back to its default and is explained in
`x-api-message` rather than failing the request. `account=` can narrow an access's scope but never
widen it — asking for an account outside the grant returns nothing, not everything.

> **Freshness and limits:** A request no more than 365 days wide is honoured; a wider one is clamped and explained in
> x-api-message. Responses are cached for 10 minutes (1 minute when any bank reported an error),
> so a client that polls aggressively still reads the banks at a sensible rate. Each access has a
> daily refresh budget (generous enough for hourly polling) that is charged on cache misses only,
> and is warned in x-api-message before it is throttled with 429.

## Scope and lifecycle

Each access is scoped to a workspace and, optionally, to a subset of its accounts; the scope can be
narrowed or widened by an admin at any time and takes effect on the next read. **Rotate** issues a
new setup token and invalidates the current Access URL immediately. **Revoke** ends the access
permanently — the row stays for audit. Both are available in the app and via the
`rotate_simplefin_access` / `revoke_simplefin_access` MCP tools.

## Related

- [Connect Actual Budget, Securo and other SimpleFIN apps](/docs/integrations/simplefin-setup) — the user guide
- [API keys](/docs/api/api-keys) — for your own scripts against the BankSync REST API
- [MCP server](/docs/mcp/overview) — for AI agents
