Ruby client for the SimpleFIN API
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
jared 8090f8ee82 Add simplefin-api gem source, tests, and docs
Onboards project structure into version control alongside the earlier
initial commit that set up .gitignore.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 10:39:20 -04:00
docs Add simplefin-api gem source, tests, and docs 2026-09-17 10:39:20 -04:00
lib Add simplefin-api gem source, tests, and docs 2026-09-17 10:39:20 -04:00
test Add simplefin-api gem source, tests, and docs 2026-09-17 10:39:20 -04:00
.gitignore Add simplefin-api gem source, tests, and docs 2026-09-17 10:39:20 -04:00
.graphifyignore Add simplefin-api gem source, tests, and docs 2026-09-17 10:39:20 -04:00
.rubocop.yml Add simplefin-api gem source, tests, and docs 2026-09-17 10:39:20 -04:00
CHANGELOG.md Add simplefin-api gem source, tests, and docs 2026-09-17 10:39:20 -04:00
Gemfile Add simplefin-api gem source, tests, and docs 2026-09-17 10:39:20 -04:00
LICENSE Add simplefin-api gem source, tests, and docs 2026-09-17 10:39:20 -04:00
Rakefile Add simplefin-api gem source, tests, and docs 2026-09-17 10:39:20 -04:00
README.md Add simplefin-api gem source, tests, and docs 2026-09-17 10:39:20 -04:00
simplefin_api.gemspec Add simplefin-api gem source, tests, and docs 2026-09-17 10:39:20 -04:00

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:

  1. Setup token — the user copies a Base64 string from their institution's /create page.
  2. Access URL — SimplefinApi.claim POSTs to the decoded token and returns an HTTPS URL carrying Basic-auth credentials. This exchange works once. A second attempt returns 403.
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.