SimpleFIN protocol reference
Wire-level reference for the BankSync SimpleFIN server: the claim exchange, endpoints, the superset Account Set, query parameters and limits.
5 min read
BankSync is a SimpleFIN 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 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:
# 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 — the user guide
- API keys — for your own scripts against the BankSync REST API
- MCP server — for AI agents
Use this page with your AI assistant
Every BankSync doc is available as plain Markdown for agents and LLMs.