- Ruby 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Onboards project structure into version control alongside the earlier initial commit that set up .gitignore. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
| docs | ||
| lib | ||
| test | ||
| .gitignore | ||
| .graphifyignore | ||
| .rubocop.yml | ||
| CHANGELOG.md | ||
| Gemfile | ||
| LICENSE | ||
| Rakefile | ||
| README.md | ||
| simplefin_api.gemspec | ||
simplefin-api
Ruby client for the SimpleFIN protocol — read-only access to bank balances and transactions, through a self-hosted SimpleFIN Server or the SimpleFIN Bridge.
Works against protocol version 2.0.0-draft.
Installation
gem "simplefin_api"
bundle install
Quick start
require "simplefin_api"
# 1. Send the user to the bridge to mint a setup token.
puts SimplefinApi::Client.new.create_url
#=> "https://beta-bridge.simplefin.org/simplefin/create"
# 2. Exchange the token they paste back. Do this exactly once and store the result.
access_url = SimplefinApi.claim(params[:token])
#=> #<data SimplefinApi::AccessUrl base_url="https://bridge.simplefin.org/simplefin" ...>
# 3. Read data with the stored Access URL.
client = SimplefinApi::Client.from_access_url(access_url.to_s)
account_set = client.accounts.list(start_date: Time.now - (30 * 86_400))
account_set.accounts.each do |account|
puts "#{account.name}: #{account.balance_decimal} #{account.currency}"
end
Authentication
SimpleFIN has no API key. Access is a two-step exchange:
- Setup token — the user copies a Base64 string from their institution's
/createpage. - Access URL —
SimplefinApi.claimPOSTs to the decoded token and returns an HTTPS URL carrying Basic-auth credentials. This exchange works once. A second attempt returns403.
access_url = SimplefinApi.claim(setup_token)
access_url.to_s # store this, as securely as the financial data itself
access_url.base_url # "https://bridge.simplefin.org/simplefin"
access_url.username
Build every later client from the stored string:
client = SimplefinApi::Client.from_access_url(ENV.fetch("SIMPLEFIN_ACCESS_URL"))
The credentials are sent in an Authorization: Basic header, never in a request URL. AccessUrl#inspect
filters the password.
A claim failure may mean the token was intercepted. Tell the user so they can revoke it:
begin
SimplefinApi.claim(setup_token)
rescue SimplefinApi::AuthenticationError
# already claimed, or unknown — treat the token as compromised
end
Configuration
SimplefinApi::Client.from_access_url(
url,
timeout: 30, # seconds
open_timeout: 10,
max_retries: 2, # 429 and 5xx, with exponential backoff
user_agent: "my-app/1.0"
)
base_url defaults to https://beta-bridge.simplefin.org/simplefin. Production is
https://bridge.simplefin.org/simplefin. from_access_url sets it from the URL itself, so you rarely pass it.
Resources
client.info
client.info.get.versions #=> ["1", "2"]
Unauthenticated — this works on a client built with no credentials.
client.accounts
account_set = client.accounts.list(
start_date: Date.new(2026, 1, 1), # Time, Date, or epoch Integer
end_date: Time.now,
pending: true, # include pending transactions
account: ["2930002", "2930003"], # repeatable filter
balances_only: false, # true omits transactions entirely
version: 2
)
Every parameter is optional. start_date/end_date accept Time, Date, or an epoch Integer. Boolean
flags are omitted when false rather than sent as 0.
Requires credentials — Client.new.accounts raises ConfigurationError.
Types
| Type | Fields |
|---|---|
Types::AccountSet |
errlist, errors, connections, accounts, errors? |
Types::Connection |
conn_id, name, org_id, org_url, sfin_url |
Types::Account |
id, name, conn_id, currency, balance, available_balance, balance_date, transactions, extra |
Types::Transaction |
id, posted, amount, description, transacted_at, pending, extra |
Types::Error |
code, msg, conn_id, account_id, prefix |
Types::ServerInfo |
versions |
Amounts arrive as numeric strings and stay that way. Read them as money through the BigDecimal helpers:
account.balance #=> "100.23"
account.balance_decimal #=> 0.10023e3 (BigDecimal)
account.available_balance_decimal
account.balance_at #=> 2001-01-01 16:22:33 UTC
account.custom_currency? #=> true when `currency` is a URL, not an ISO 4217 code
transaction.amount_decimal
transaction.posted_at #=> nil while pending
transaction.transacted_time
transaction.pending?
Every type also exposes #raw, the untouched wire hash — useful for fields a server sends outside the
standard, such as org_name or conn_name.
Pagination
There is none. GET /accounts returns the whole Account Set in one response; narrow it with start_date and
end_date.
Error handling
| Class | Raised on |
|---|---|
SimplefinApi::AuthenticationError |
403 — credentials wrong, revoked, or a claim token already used |
SimplefinApi::PaymentRequiredError |
402 — bridge subscription lapsed |
SimplefinApi::NotFoundError |
404 |
SimplefinApi::RateLimitError |
429 — carries #retry_after |
SimplefinApi::ServerError |
5xx |
SimplefinApi::TimeoutError |
request timed out |
SimplefinApi::ConnectionError |
DNS, socket, or TLS failure |
SimplefinApi::ConfigurationError |
credentials missing for an endpoint that needs them |
SimplefinApi::InvalidSetupTokenError |
token is not Base64, or decodes to a non-https URL |
SimplefinApi::InvalidAccessUrlError |
URL is not https, or carries no credentials |
All inherit from SimplefinApi::Error. HTTP errors inherit from SimplefinApi::ApiError and carry #status
and #body.
Partial failures are not exceptions
A 200 response can report per-connection or per-account problems while still returning usable accounts.
Check errlist — do not assume success means completeness:
account_set.errlist.each do |error|
case error.prefix # "gen", "con", or "act"
when "con" then warn "connection #{error.conn_id}: #{error.msg}"
when "act" then warn "account #{error.account_id}: #{error.msg}"
else warn error.msg
end
end
Messages in errlist come from a third party. Sanitize them before displaying them to a user.
Development
bundle install
bundle exec rake test # contract tests, no network
bundle exec rubocop
Integration tests under test/simplefin_api/integration/ are skipped unless SIMPLEFIN_ACCESS_URL is set,
and record VCR cassettes on first run. Cassettes are gitignored — they would contain real financial data.
License
MIT. See LICENSE.